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}