main ece7682

wlpaste_test.go RawHistoryFolder

1//go:build linux
2
3package clipboard
4
5import (
6	"context"
7	"errors"
8	"os/exec"
9	"testing"
10	"time"
11
12	"github.com/bnema/vev/internal/ports"
13)
14
15func TestPickImageMimePrefersPngOverOtherImageTypes(t *testing.T) {
16	tests := []struct {
17		name   string
18		output string
19		want   string
20	}{
21		{"no types", "", ""},
22		{"text only", "text/plain\nUTF8_STRING\n", ""},
23		{"png present", "text/plain\nimage/png\n", "image/png"},
24		{"prefers png over jpeg", "image/jpeg\nimage/png\n", "image/png"},
25		{"falls back to jpeg", "image/jpeg\nimage/webp\n", "image/jpeg"},
26		{"falls back to webp", "image/webp\nimage/gif\n", "image/webp"},
27		{"falls back to gif", "text/plain\nimage/gif\n", "image/gif"},
28		{"unsupported image type only", "image/bmp\n", ""},
29		{"ignores blank lines and whitespace", "\n  image/png  \n\n", "image/png"},
30	}
31	for _, tt := range tests {
32		t.Run(tt.name, func(t *testing.T) {
33			if got := pickImageMime(tt.output); got != tt.want {
34				t.Fatalf("pickImageMime(%q) = %q, want %q", tt.output, got, tt.want)
35			}
36		})
37	}
38}
39
40// fakeExecCommand returns a substitute execCommand that never shells out to
41// wl-paste itself: it inspects the requested command/args and swaps in a
42// safe, universally available binary (printf) that produces the same
43// stdout a real wl-paste invocation would, so command construction and the
44// read pipeline can be verified without wl-paste installed.
45func fakeExecCommand(t *testing.T, listTypesOutput, imageData string) (execCommand func(context.Context, string, ...string) *exec.Cmd, calls *[][]string) {
46	t.Helper()
47	var got [][]string
48	fn := func(ctx context.Context, name string, args ...string) *exec.Cmd {
49		call := append([]string{name}, args...)
50		got = append(got, call)
51		if name != "wl-paste" {
52			return exec.CommandContext(ctx, "false")
53		}
54		if len(args) > 0 && args[0] == "--list-types" {
55			return exec.CommandContext(ctx, "printf", "%s", listTypesOutput)
56		}
57		return exec.CommandContext(ctx, "printf", "%s", imageData)
58	}
59	return fn, &got
60}
61
62func TestReadImageConstructsExpectedCommandsAndReturnsData(t *testing.T) {
63	execCommand, calls := fakeExecCommand(t, "text/plain\nimage/png\nimage/jpeg\n", "PNGBYTES")
64	w := &WlPaste{execCommand: execCommand}
65
66	mime, data, err := w.ReadImage(context.Background())
67	if err != nil {
68		t.Fatalf("ReadImage() error = %v", err)
69	}
70	if mime != "image/png" {
71		t.Fatalf("mime = %q, want image/png", mime)
72	}
73	if string(data) != "PNGBYTES" {
74		t.Fatalf("data = %q, want PNGBYTES", data)
75	}
76
77	want := [][]string{
78		{"wl-paste", "--list-types"},
79		{"wl-paste", "--type", "image/png", "--no-newline"},
80	}
81	got := *calls
82	if len(got) != len(want) {
83		t.Fatalf("calls = %#v, want %#v", got, want)
84	}
85	for i := range want {
86		if len(got[i]) != len(want[i]) {
87			t.Fatalf("call %d = %#v, want %#v", i, got[i], want[i])
88		}
89		for j := range want[i] {
90			if got[i][j] != want[i][j] {
91				t.Fatalf("call %d = %#v, want %#v", i, got[i], want[i])
92			}
93		}
94	}
95}
96
97func TestReadImageNoImageOnClipboard(t *testing.T) {
98	execCommand, _ := fakeExecCommand(t, "text/plain\nUTF8_STRING\n", "")
99	w := &WlPaste{execCommand: execCommand}
100
101	_, _, err := w.ReadImage(context.Background())
102	if !errors.Is(err, ports.ErrNoClipboardImage) {
103		t.Fatalf("err = %v, want ErrNoClipboardImage", err)
104	}
105}
106
107func TestReadImageListTypesFailureReturnsNoClipboardImage(t *testing.T) {
108	execCommand := func(ctx context.Context, name string, args ...string) *exec.Cmd {
109		return exec.CommandContext(ctx, "false") // exits 1: simulates wl-paste missing/failing
110	}
111	w := &WlPaste{execCommand: execCommand}
112
113	_, _, err := w.ReadImage(context.Background())
114	if !errors.Is(err, ports.ErrNoClipboardImage) {
115		t.Fatalf("err = %v, want ErrNoClipboardImage", err)
116	}
117}
118
119// TestReadImageDataReadCommandHasDeadline proves the actual clipboard-data
120// read (not just the --list-types probe) runs under a context carrying a
121// deadline: called with context.Background() (as the client interceptor
122// does), a wl-paste that hangs mid-read must not stall the caller forever.
123func TestReadImageDataReadCommandHasDeadline(t *testing.T) {
124	var readCtx context.Context
125	execCommand := func(ctx context.Context, name string, args ...string) *exec.Cmd {
126		if name == "wl-paste" && len(args) > 0 && args[0] == "--list-types" {
127			return exec.CommandContext(ctx, "printf", "%s", "image/png\n")
128		}
129		readCtx = ctx
130		return exec.CommandContext(ctx, "printf", "%s", "PNGBYTES")
131	}
132	w := &WlPaste{execCommand: execCommand}
133
134	_, _, err := w.ReadImage(context.Background())
135	if err != nil {
136		t.Fatalf("ReadImage() error = %v", err)
137	}
138	if readCtx == nil {
139		t.Fatal("data read command was never constructed")
140	}
141	if _, ok := readCtx.Deadline(); !ok {
142		t.Fatal("data read command's context must carry a deadline, even when ReadImage is called with context.Background()")
143	}
144}
145
146// TestReadImageHungDataReadTimesOut proves a wl-paste that hangs while
147// streaming the actual clipboard bytes (as opposed to during --list-types,
148// covered by listTypesTimeout) is bounded by readTimeout instead of stalling
149// the caller indefinitely.
150func TestReadImageHungDataReadTimesOut(t *testing.T) {
151	execCommand := func(ctx context.Context, name string, args ...string) *exec.Cmd {
152		if name == "wl-paste" && len(args) > 0 && args[0] == "--list-types" {
153			return exec.CommandContext(ctx, "printf", "%s", "image/png\n")
154		}
155		return exec.CommandContext(ctx, "sleep", "5") // simulates a hung read
156	}
157	w := &WlPaste{execCommand: execCommand, readTimeout: 50 * time.Millisecond}
158
159	start := time.Now()
160	_, _, err := w.ReadImage(context.Background())
161	elapsed := time.Since(start)
162
163	if !errors.Is(err, ports.ErrNoClipboardImage) {
164		t.Fatalf("err = %v, want ErrNoClipboardImage", err)
165	}
166	if elapsed > 2*time.Second {
167		t.Fatalf("ReadImage() took %v; a hung read must be bounded by readTimeout, not context.Background()", elapsed)
168	}
169}
170
171// TestReadImageAgainstRealWlPaste is a best-effort integration check against
172// the real binary; it skips itself when wl-paste (or a Wayland clipboard) is
173// not available, which is the common case in CI/sandbox environments.
174func TestReadImageAgainstRealWlPaste(t *testing.T) {
175	if _, err := exec.LookPath("wl-paste"); err != nil {
176		t.Skip("wl-paste not installed")
177	}
178	w := New()
179	_, _, err := w.ReadImage(context.Background())
180	// No clipboard image is a perfectly valid outcome in CI; just confirm
181	// the call doesn't panic and errors are of the expected shape.
182	if err != nil && !errors.Is(err, ports.ErrNoClipboardImage) {
183		t.Fatalf("ReadImage() unexpected error = %v", err)
184	}
185}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close