1package neferclient_test
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/go-wayland-bindings/client/wayland"
9 "github.com/bnema/neferclient"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13 "golang.org/x/sys/unix"
14
15 neferclientmocks "github.com/bnema/neferclient/mocks"
16)
17
18// dispatchUntil dispatches whenever Wake fires until done reports true.
19func dispatchUntil(t *testing.T, c *neferclient.Conn, h neferclient.Handler, done func() bool) {
20 t.Helper()
21 timeout := time.After(10 * time.Second)
22 for {
23 require.NoError(t, c.Dispatch(h))
24 if done() {
25 return
26 }
27 select {
28 case <-c.Wake():
29 case <-timeout:
30 t.Fatal("timed out waiting for events")
31 }
32 }
33}
34
35func TestSocketPath(t *testing.T) {
36 t.Setenv("XDG_RUNTIME_DIR", "/run/user/1")
37 t.Setenv("WAYLAND_DISPLAY", "")
38 p, err := neferclient.SocketPath("")
39 require.NoError(t, err)
40 assert.Equal(t, "/run/user/1/wayland-0", p)
41 t.Setenv("WAYLAND_DISPLAY", "wayland-7")
42 p, _ = neferclient.SocketPath("")
43 assert.Equal(t, "/run/user/1/wayland-7", p)
44 p, _ = neferclient.SocketPath("x")
45 assert.Equal(t, "/run/user/1/x", p)
46 p, _ = neferclient.SocketPath("/tmp/sock")
47 assert.Equal(t, "/tmp/sock", p)
48 t.Setenv("XDG_RUNTIME_DIR", "")
49 _, err = neferclient.SocketPath("rel")
50 assert.Error(t, err)
51}
52
53func TestConnectInitialOutputs(t *testing.T) {
54 c, srv := connectWire(t,
55 []wireGlobal{{1, "wl_seat", 9}, {7, outputIface, 4}, {9, outputIface, 2}},
56 map[uint32]outputSpec{7: {"DP-1", 2, 1920, 1080}, 9: {"ignored-before-v4", 1, 800, 600}})
57 assert.Equal(t, []neferclient.Output{
58 {Global: 7, Name: "DP-1", Scale: 2, Width: 1920, Height: 1080},
59 {Global: 9, Name: "", Scale: 1, Width: 800, Height: 600},
60 }, c.Outputs())
61 assert.Equal(t, uint32(4), srv.waitBound(7).version, "bound at min(announced, 4)")
62 assert.Equal(t, uint32(2), srv.waitBound(9).version)
63
64 // Setup events are applied silently: nothing is left for the Handler. The
65 // roundtrip moves everything handled so far into the ring, so a leaked
66 // setup event would reach the strict mock here.
67 h := neferclientmocks.NewMockHandler(t)
68 require.NoError(t, c.Roundtrip())
69 require.NoError(t, c.Dispatch(h))
70}
71
72func TestConnectContextCancelled(t *testing.T) {
73 client, _ := socketPair(t) // peer never answers
74 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
75 defer cancel()
76 c, err := neferclient.ConnectConn(ctx, client)
77 require.Error(t, err)
78 assert.Nil(t, c)
79 assert.ErrorIs(t, err, context.DeadlineExceeded)
80
81 cancelled, stop := context.WithCancel(context.Background())
82 stop()
83 _, err = neferclient.Connect(cancelled, "")
84 assert.ErrorIs(t, err, context.Canceled)
85 //lint:ignore SA1012 a nil context is the case under test
86 _, err = neferclient.Connect(nil, "")
87 assert.Error(t, err)
88}
89
90func TestHotplug(t *testing.T) {
91 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}},
92 map[uint32]outputSpec{7: {"DP-1", 1, 1920, 1080}, 11: {"HDMI-A-1", 2, 3840, 2160}})
93 h := neferclientmocks.NewMockHandler(t)
94 var added []neferclient.Output
95 h.EXPECT().OutputAdded(mock.Anything).Run(func(out *neferclient.Output) { added = append(added, *out) }).Once()
96 srv.announce(wireGlobal{11, outputIface, 4})
97 dispatchUntil(t, c, h, func() bool { return len(added) == 1 })
98 assert.Equal(t, neferclient.Output{Global: 11, Name: "HDMI-A-1", Scale: 2, Width: 3840, Height: 2160}, added[0])
99 require.Len(t, c.Outputs(), 2)
100
101 h.EXPECT().OutputRemoved(uint32(7)).Once()
102 srv.remove(7)
103 dispatchUntil(t, c, h, func() bool { return len(c.Outputs()) == 1 })
104 assert.Equal(t, uint32(11), c.Outputs()[0].Global)
105}
106
107func TestOutputPropertyChangeIsSilent(t *testing.T) {
108 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
109 obj := srv.waitBound(7)
110 srv.write(frame(obj.id, 3, appendU32(nil, 3))) // scale 3
111 srv.write(frame(obj.id, 2, nil)) // done
112 h := neferclientmocks.NewMockHandler(t) // no expectations: any call fails
113 dispatchUntil(t, c, h, func() bool { return c.Outputs()[0].Scale == 3 })
114}
115
116func TestRingBackpressureKeepsOrder(t *testing.T) {
117 const total = 3 * neferclient.RingSize
118 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
119 obj := srv.waitBound(7)
120 var buf []byte
121 for i := 1; i <= total; i++ {
122 buf = append(buf, frame(obj.id, 3, appendU32(nil, uint32(i)))...)
123 buf = append(buf, frame(obj.id, 2, nil)...)
124 }
125 go srv.write(buf)
126 waitFor(t, func() bool { return c.QueueLen() == neferclient.RingSize }, "full ring")
127
128 // A pause (Roundtrip) must not deadlock against a producer blocked on the
129 // full ring, and must not reorder what it held back: the visible scale
130 // only ever grows, ending at total.
131 require.NoError(t, c.Roundtrip())
132 prev := int32(0)
133 dispatchUntil(t, c, nil, func() bool {
134 got := c.Outputs()[0].Scale
135 require.GreaterOrEqual(t, got, prev, "scale went backwards: events were reordered")
136 prev = got
137 return got == total
138 })
139}
140
141func TestRoundtripQueuesEventsForNextDispatch(t *testing.T) {
142 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
143 obj := srv.waitBound(7)
144 srv.write(frame(obj.id, 3, appendU32(nil, 4))) // scale 4
145 srv.write(frame(obj.id, 2, nil)) // done
146 require.NoError(t, c.Roundtrip())
147 // No Wake wait: the events handled during the roundtrip are already queued.
148 require.NoError(t, c.Dispatch(nil))
149 assert.Equal(t, int32(4), c.Outputs()[0].Scale)
150}
151
152func TestBindQueuesEventsForNextDispatch(t *testing.T) {
153 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}, {3, wayland.CompositorInterface, 6}},
154 map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
155 obj := srv.waitBound(7)
156 srv.write(frame(obj.id, 3, appendU32(nil, 2)))
157 srv.write(frame(obj.id, 2, nil))
158 // The bind pauses the reader; wait until the reader has the events so they
159 // are handled during the pause or before it, never after Bind returns.
160 waitFor(t, func() bool { return c.QueueLen() == 2 }, "events queued")
161 _, _, err := c.Bind[*wayland.Compositor](wayland.CompositorInterface, 4, wayland.NewCompositor)
162 require.NoError(t, err)
163 require.NoError(t, c.Dispatch(nil))
164 assert.Equal(t, int32(2), c.Outputs()[0].Scale)
165}
166
167func waitFor(t *testing.T, cond func() bool, what string) {
168 t.Helper()
169 deadline := time.Now().Add(10 * time.Second)
170 for !cond() {
171 if time.Now().After(deadline) {
172 t.Fatalf("timed out waiting for %s", what)
173 }
174 time.Sleep(time.Millisecond)
175 }
176}
177
178func TestBind(t *testing.T) {
179 c, srv := connectWire(t, []wireGlobal{{3, wayland.CompositorInterface, 6}}, nil)
180 comp, v, err := c.Bind[*wayland.Compositor](wayland.CompositorInterface, 4, wayland.NewCompositor)
181 require.NoError(t, err)
182 assert.NotNil(t, comp)
183 assert.Equal(t, uint32(4), v)
184 assert.Equal(t, uint32(4), srv.waitBound(3).version)
185
186 _, _, err = c.Bind[*wayland.Shm](wayland.ShmInterface, 1, wayland.NewShm)
187 assert.ErrorIs(t, err, neferclient.ErrGlobalNotFound)
188}
189
190func TestWatchFD(t *testing.T) {
191 c, _ := connectWire(t, nil, nil)
192 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
193 require.NoError(t, err)
194 defer unix.Close(efd)
195 require.NoError(t, c.WatchFD(efd, 42))
196 assert.Error(t, c.WatchFD(efd, 43), "double watch")
197
198 h := neferclientmocks.NewMockHandler(t)
199 got := 0
200 h.EXPECT().FDReady(uint64(42)).Run(func(uint64) {
201 got++
202 var b [8]byte
203 _, _ = unix.Read(efd, b[:])
204 }).Times(2)
205 signal := func() {
206 one := [8]byte{1}
207 _, err := unix.Write(efd, one[:])
208 require.NoError(t, err)
209 }
210 signal()
211 dispatchUntil(t, c, h, func() bool { return got == 1 })
212 signal() // re-armed after the first FDReady returned
213 dispatchUntil(t, c, h, func() bool { return got == 2 })
214
215 require.NoError(t, c.UnwatchFD(efd))
216 assert.Error(t, c.UnwatchFD(efd))
217}
218
219func TestUnwatchDropsQueuedNotification(t *testing.T) {
220 c, _ := connectWire(t, nil, nil)
221 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
222 require.NoError(t, err)
223 defer unix.Close(efd)
224 require.NoError(t, c.WatchFD(efd, 9))
225 one := [8]byte{1}
226 _, err = unix.Write(efd, one[:])
227 require.NoError(t, err)
228 waitFor(t, func() bool { return c.QueueLen() > 0 }, "queued fd notification")
229 require.NoError(t, c.UnwatchFD(efd))
230 h := neferclientmocks.NewMockHandler(t) // no FDReady expectation: a call fails
231 require.NoError(t, c.Dispatch(h))
232}
233
234func TestCloseInsideFDReady(t *testing.T) {
235 c, _ := connectWire(t, nil, nil)
236 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
237 require.NoError(t, err)
238 defer unix.Close(efd)
239 require.NoError(t, c.WatchFD(efd, 1))
240 h := neferclientmocks.NewMockHandler(t) // Error must not be called after Close
241 h.EXPECT().FDReady(uint64(1)).Run(func(uint64) { require.NoError(t, c.Close()) }).Once()
242 one := [8]byte{1}
243 _, _ = unix.Write(efd, one[:])
244 waitFor(t, func() bool { return c.QueueLen() > 0 }, "queued fd notification")
245 assert.ErrorIs(t, c.Dispatch(h), neferclient.ErrClosed)
246}
247
248func TestOutputVersionOneIgnored(t *testing.T) {
249 c, _ := connectWire(t, []wireGlobal{{5, outputIface, 1}, {7, outputIface, 2}},
250 map[uint32]outputSpec{5: {"old", 1, 10, 10}, 7: {"DP-1", 1, 100, 100}})
251 require.Len(t, c.Outputs(), 1)
252 assert.Equal(t, uint32(7), c.Outputs()[0].Global)
253}
254
255func TestUnwatchInsideHandler(t *testing.T) {
256 c, _ := connectWire(t, nil, nil)
257 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
258 require.NoError(t, err)
259 defer unix.Close(efd)
260 require.NoError(t, c.WatchFD(efd, 1))
261 h := neferclientmocks.NewMockHandler(t)
262 called := false
263 h.EXPECT().FDReady(uint64(1)).Run(func(uint64) {
264 called = true
265 require.NoError(t, c.UnwatchFD(efd))
266 }).Once()
267 one := [8]byte{1}
268 _, _ = unix.Write(efd, one[:])
269 dispatchUntil(t, c, h, func() bool { return called })
270}
271
272func TestTransportErrorSurfacesAfterQueuedEvents(t *testing.T) {
273 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
274 obj := srv.waitBound(7)
275 srv.write(frame(obj.id, 3, appendU32(nil, 5)))
276 srv.write(frame(obj.id, 2, nil))
277 srv.closeConn()
278 timeout := time.After(10 * time.Second)
279 var err error
280 for err == nil {
281 err = c.Dispatch(nil)
282 if err != nil {
283 break
284 }
285 select {
286 case <-c.Wake():
287 case <-timeout:
288 t.Fatal("transport error never surfaced")
289 }
290 }
291 assert.Equal(t, int32(5), c.Outputs()[0].Scale, "events queued before the failure are applied first")
292}
293
294func TestHandlerCannotReenterDispatch(t *testing.T) {
295 c, srv := connectWire(t, nil, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
296 h := neferclientmocks.NewMockHandler(t)
297 var inner error
298 h.EXPECT().OutputAdded(mock.Anything).Run(func(*neferclient.Output) { inner = c.Dispatch(h) }).Once()
299 srv.announce(wireGlobal{7, outputIface, 4})
300 dispatchUntil(t, c, h, func() bool { return inner != nil })
301 assert.Error(t, inner)
302}
303
304func TestClose(t *testing.T) {
305 c, _ := connectWire(t, nil, nil)
306 require.NoError(t, c.Close())
307 assert.NoError(t, c.Close())
308 assert.ErrorIs(t, c.Dispatch(nil), neferclient.ErrClosed)
309 assert.ErrorIs(t, c.Roundtrip(), neferclient.ErrClosed)
310 assert.ErrorIs(t, c.WatchFD(0, 1), neferclient.ErrClosed)
311 _, _, err := c.Bind[*wayland.Shm](wayland.ShmInterface, 1, wayland.NewShm)
312 assert.ErrorIs(t, err, neferclient.ErrClosed)
313 var nilConn *neferclient.Conn
314 assert.NoError(t, nilConn.Close())
315}
316
317func TestCloseWhileReaderBlockedOnFullRing(t *testing.T) {
318 c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
319 obj := srv.waitBound(7)
320 var buf []byte
321 for i := 0; i < 2*neferclient.RingSize; i++ {
322 buf = append(buf, frame(obj.id, 3, appendU32(nil, 2))...)
323 }
324 go srv.write(buf)
325 waitFor(t, func() bool { return c.QueueLen() == neferclient.RingSize }, "full ring")
326 done := make(chan struct{})
327 go func() { _ = c.Close(); close(done) }()
328 select {
329 case <-done:
330 case <-time.After(5 * time.Second):
331 t.Fatal("Close hung with a producer blocked on the full ring")
332 }
333}