1package wayland
2
3import (
4 "context"
5 "encoding/binary"
6 "io"
7 "net"
8 "os"
9 "slices"
10 "testing"
11 "time"
12
13 "github.com/bnema/neferafk/internal/ports"
14 "github.com/bnema/zerowrap"
15 "golang.org/x/sys/unix"
16)
17
18func sockets(t *testing.T) (net.Conn, *net.UnixConn) {
19 t.Helper()
20 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
21 if err != nil {
22 t.Fatal(err)
23 }
24 files := []*os.File{os.NewFile(uintptr(fds[0]), "client"), os.NewFile(uintptr(fds[1]), "peer")}
25 a, err := net.FileConn(files[0])
26 files[0].Close()
27 if err != nil {
28 t.Fatal(err)
29 }
30 b, err := net.FileConn(files[1])
31 files[1].Close()
32 if err != nil {
33 t.Fatal(err)
34 }
35 t.Cleanup(func() { a.Close(); b.Close() })
36 return a, b.(*net.UnixConn)
37}
38func packet(id uint32, op uint16, body []byte) []byte {
39 b := make([]byte, 8+len(body))
40 binary.NativeEndian.PutUint32(b, id)
41 binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
42 copy(b[8:], body)
43 return b
44}
45func words(v ...uint32) []byte {
46 b := make([]byte, 4*len(v))
47 for i, n := range v {
48 binary.NativeEndian.PutUint32(b[i*4:], n)
49 }
50 return b
51}
52func wireString(s string) []byte {
53 b := make([]byte, 4+(len(s)+4)&^3)
54 binary.NativeEndian.PutUint32(b, uint32(len(s)+1))
55 copy(b[4:], s)
56 return b
57}
58func readWire(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
59 t.Helper()
60 p.SetReadDeadline(time.Now().Add(2 * time.Second))
61 h := make([]byte, 8)
62 if _, err := io.ReadFull(p, h); err != nil {
63 t.Fatal(err)
64 }
65 size := binary.NativeEndian.Uint32(h[4:]) >> 16
66 b := make([]byte, size-8)
67 if _, err := io.ReadFull(p, b); err != nil {
68 t.Fatal(err)
69 }
70 return binary.NativeEndian.Uint32(h), uint16(binary.NativeEndian.Uint32(h[4:])), b
71}
72func sendWire(t *testing.T, p *net.UnixConn, id uint32, op uint16, b []byte) {
73 t.Helper()
74 if _, err := p.Write(packet(id, op, b)); err != nil {
75 t.Fatal(err)
76 }
77}
78func global(t *testing.T, p *net.UnixConn, registry, name uint32, iface string, v uint32) {
79 b := append(words(name), wireString(iface)...)
80 b = append(b, words(v)...)
81 sendWire(t, p, registry, 0, b)
82}
83func synchronize(t *testing.T, p *net.UnixConn, body []byte) {
84 id := binary.NativeEndian.Uint32(body)
85 sendWire(t, p, id, 0, words(1))
86 sendWire(t, p, 1, 1, words(id))
87}
88func capability(t *testing.T, out <-chan ports.WaylandEvent) ports.Capabilities {
89 t.Helper()
90 for {
91 select {
92 case e := <-out:
93 if c, ok := e.(ports.WaylandCapabilities); ok {
94 return c.Capabilities
95 }
96 case <-time.After(2 * time.Second):
97 t.Fatal("capability timeout")
98 }
99 }
100}
101func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent {
102 t.Helper()
103 select {
104 case e := <-out:
105 return e
106 case <-time.After(2 * time.Second):
107 t.Fatal("event timeout")
108 return nil
109 }
110}
111
112// started is a client running against a fake compositor that announced
113// its globals and answered the initial bindings (one output, HEADLESS-1).
114type started struct {
115 peer *net.UnixConn
116 registry, wake, power uint32
117 ids map[string]uint32 // bound ID by interface
118 notifications map[uint32]uint32 // idle notification by timeout (ms)
119 commands chan ports.WaylandCommand
120 out chan ports.WaylandEvent
121 done chan error
122}
123
124// extraGlobal is one more global announced at start.
125type extraGlobal struct {
126 name uint32
127 iface string
128 version uint32
129}
130
131// start runs a client and the initial discovery; extra globals follow
132// wl_seat, wl_output, ext_idle_notifier_v1 and zwlr_output_power_manager_v1.
133func start(t *testing.T, ctx context.Context, options Options, extra ...extraGlobal) *started {
134 t.Helper()
135 conn, peer := sockets(t)
136 c, err := New(conn, options, zerowrap.New(zerowrap.Config{Output: io.Discard}))
137 if err != nil {
138 t.Fatal(err)
139 }
140 _, _, body := readWire(t, peer)
141 s := &started{peer: peer, registry: binary.NativeEndian.Uint32(body), ids: map[string]uint32{}, notifications: map[uint32]uint32{}, commands: make(chan ports.WaylandCommand, 8), out: make(chan ports.WaylandEvent, 64), done: make(chan error, 1)}
142 go func() { s.done <- c.Run(ctx, s.commands, s.out) }()
143 _, _, body = readWire(t, peer)
144 global(t, peer, s.registry, 10, "wl_seat", 5)
145 global(t, peer, s.registry, 11, "wl_output", 4)
146 global(t, peer, s.registry, 12, "ext_idle_notifier_v1", 2)
147 global(t, peer, s.registry, 13, "zwlr_output_power_manager_v1", 1)
148 for _, g := range extra {
149 global(t, peer, s.registry, g.name, g.iface, g.version)
150 }
151 synchronize(t, peer, body)
152 for {
153 id, op, b := readWire(t, peer)
154 if id == 1 {
155 synchronize(t, peer, b)
156 break
157 }
158 if id == s.registry {
159 n := int(binary.NativeEndian.Uint32(b[4:]))
160 iface := string(b[8 : 8+n-1])
161 s.ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
162 if iface == "wl_output" {
163 sendWire(t, peer, s.ids[iface], 4, wireString("HEADLESS-1"))
164 }
165 continue
166 }
167 if id == s.ids["ext_idle_notifier_v1"] {
168 child := binary.NativeEndian.Uint32(b)
169 if op == 2 {
170 s.wake = child
171 } else {
172 s.notifications[binary.NativeEndian.Uint32(b[4:])] = child
173 }
174 }
175 if id == s.ids["zwlr_output_power_manager_v1"] {
176 s.power = binary.NativeEndian.Uint32(b)
177 sendWire(t, peer, s.power, 0, words(1))
178 }
179 }
180 return s
181}
182
183func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
184 ctx, cancel := context.WithCancel(context.Background())
185 defer cancel()
186 s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, extraGlobal{14, "ext_session_lock_manager_v1", 1})
187 peer, registry, ids, notifications, wake, power := s.peer, s.registry, s.ids, s.notifications, s.wake, s.power
188 commands, out, done := s.commands, s.out, s.done
189 caps := capability(t, out)
190 if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {
191 t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)
192 }
193 if !slices.Contains(caps.Missing, "zwp_linux_dmabuf_v1 >= 4") || slices.Contains(caps.Missing, "ext_session_lock_manager_v1 >= 1") {
194 t.Fatal(caps.Missing)
195 }
196 lifetime := caps.Outputs[0].Lifetime
197 sendWire(t, peer, notifications[1000], 0, nil)
198 idle, ok := nextEvent(t, out).(ports.WaylandIdle)
199 if !ok || idle.Generation != 1 || idle.After != time.Second || idle.Cycle != 1 {
200 t.Fatal(idle, ok)
201 }
202 // Reload retains the one-second proxy and removes only the two-second one.
203 commands <- ports.SetDeadlines{Generation: 2, Deadlines: []time.Duration{time.Second, 3 * time.Second}}
204 var added uint32
205 for i := 0; i < 2; i++ {
206 id, op, b := readWire(t, peer)
207 if id == notifications[2000] && op == 0 {
208 continue
209 }
210 if id == ids["ext_idle_notifier_v1"] && op == 1 && binary.NativeEndian.Uint32(b[4:]) == 3000 {
211 added = binary.NativeEndian.Uint32(b)
212 continue
213 }
214 t.Fatalf("reload request %d/%d %x", id, op, b)
215 }
216 if added == 0 {
217 t.Fatal("new deadline missing")
218 }
219 capability(t, out)
220 // An in-flight event for the deleted notification must never become policy.
221 sendWire(t, peer, notifications[2000], 0, nil)
222 sendWire(t, peer, notifications[1000], 1, nil)
223 sendWire(t, peer, notifications[1000], 0, nil)
224 idle, ok = nextEvent(t, out).(ports.WaylandIdle)
225 if !ok || idle.Generation != 2 || idle.Proxy == 0 || idle.Cycle != 2 {
226 t.Fatal(idle, ok)
227 }
228 commands <- ports.OutputPower{On: false}
229 id, op, b := readWire(t, peer)
230 if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
231 t.Fatalf("off %d/%d %x", id, op, b)
232 }
233 sendWire(t, peer, wake, 0, nil)
234 sendWire(t, peer, wake, 1, nil)
235 id, op, b = readWire(t, peer)
236 if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 1 {
237 t.Fatalf("wake %d/%d %x", id, op, b)
238 }
239 if _, ok := nextEvent(t, out).(ports.WaylandActivity); !ok {
240 t.Fatal("missing activity")
241 }
242 // Removing/re-adding the same global number creates a new output lifetime.
243 sendWire(t, peer, registry, 1, words(11))
244 for i := 0; i < 2; i++ {
245 readWire(t, peer)
246 }
247 caps = capability(t, out)
248 if len(caps.Outputs) != 0 {
249 t.Fatal(caps)
250 }
251 global(t, peer, registry, 11, "wl_output", 4)
252 var newPower uint32
253 for i := 0; i < 2; i++ {
254 id, _, b = readWire(t, peer)
255 if id == registry {
256 ids["new-output"] = binary.NativeEndian.Uint32(b[len(b)-4:])
257 } else {
258 newPower = binary.NativeEndian.Uint32(b)
259 }
260 }
261 caps = capability(t, out)
262 if len(caps.Outputs) != 1 || caps.Outputs[0].Lifetime == lifetime {
263 t.Fatal(caps)
264 }
265 // A late failed event from the previous output cannot disable its successor.
266 sendWire(t, peer, power, 1, nil)
267 sendWire(t, peer, newPower, 1, nil)
268 id, op, _ = readWire(t, peer)
269 if id != newPower || op != 1 {
270 t.Fatal("failed control not destroyed", id, op)
271 }
272 if failure, ok := nextEvent(t, out).(ports.WaylandPowerFailed); !ok || failure.Output != caps.Outputs[0].Lifetime {
273 t.Fatal(failure, ok)
274 }
275 caps = capability(t, out)
276 if caps.Outputs[0].PowerSupported || len(caps.Missing) != 4 {
277 t.Fatal(caps)
278 }
279 cancel()
280 select {
281 case err := <-done:
282 if err != nil {
283 t.Fatal(err)
284 }
285 case <-time.After(2 * time.Second):
286 t.Fatal("Run failed to join reader")
287 }
288}
289
290func TestOutputAddedWhileOffIsTurnedOffAndWakesWithTheOthers(t *testing.T) {
291 ctx, cancel := context.WithCancel(context.Background())
292 defer cancel()
293 s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true}, Generation: 1, Deadlines: []time.Duration{time.Second}})
294 peer, registry, wake, power := s.peer, s.registry, s.wake, s.power
295 commands, out, done := s.commands, s.out, s.done
296 manager := s.ids["zwlr_output_power_manager_v1"]
297 capability(t, out)
298 if manager == 0 || wake == 0 || power == 0 {
299 t.Fatal(manager, wake, power)
300 }
301 commands <- ports.OutputPower{On: false}
302 if id, op, b := readWire(t, peer); id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
303 t.Fatalf("off %d/%d %x", id, op, b)
304 }
305 // addOutput announces an output and answers its power control's initial
306 // mode; it returns that control.
307 addOutput := func(name, mode uint32) uint32 {
308 global(t, peer, registry, name, "wl_output", 4)
309 var p uint32
310 for i := 0; i < 2; i++ {
311 if id, _, b := readWire(t, peer); id == manager {
312 p = binary.NativeEndian.Uint32(b)
313 }
314 }
315 sendWire(t, peer, p, 0, words(mode))
316 return p
317 }
318 // A display reconnecting from deep sleep comes back on: turned off.
319 on := addOutput(14, 1)
320 if id, op, b := readWire(t, peer); id != on || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
321 t.Fatalf("new output off %d/%d %x", id, op, b)
322 }
323 // One that comes back off needs no request, but is woken with the rest.
324 off := addOutput(15, 0)
325 sendWire(t, peer, wake, 0, nil)
326 sendWire(t, peer, wake, 1, nil)
327 woken := map[uint32]bool{}
328 for range 3 {
329 id, op, b := readWire(t, peer)
330 if op != 0 || binary.NativeEndian.Uint32(b) != 1 {
331 t.Fatalf("wake %d/%d %x", id, op, b)
332 }
333 woken[id] = true
334 }
335 if !woken[power] || !woken[on] || !woken[off] {
336 t.Fatal(woken)
337 }
338 cancel()
339 if err := <-done; err != nil {
340 t.Fatal(err)
341 }
342}
343
344func TestMissingAndDisabledCapabilities(t *testing.T) {
345 conn, peer := sockets(t)
346 c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
347 if err != nil {
348 t.Fatal(err)
349 }
350 _, _, b := readWire(t, peer)
351 registry := binary.NativeEndian.Uint32(b)
352 ctx, cancel := context.WithCancel(context.Background())
353 defer cancel()
354 out := make(chan ports.WaylandEvent, 8)
355 done := make(chan error, 1)
356 go func() { done <- c.Run(ctx, nil, out) }()
357 _, _, b = readWire(t, peer)
358 global(t, peer, registry, 10, "ext_idle_notifier_v1", 1)
359 global(t, peer, registry, 11, "zwlr_output_power_manager_v1", 1)
360 synchronize(t, peer, b)
361 id, _, b := readWire(t, peer)
362 if id != 1 {
363 t.Fatalf("disabled optional global bound: %d", id)
364 }
365 synchronize(t, peer, b)
366 caps := capability(t, out)
367 if len(caps.Missing) != 5 || slices.Contains(caps.Missing, "ext_idle_notifier_v1 >= 2") {
368 t.Fatal(caps)
369 }
370 cancel()
371 if err := <-done; err != nil {
372 t.Fatal(err)
373 }
374}
375
376func TestDeadlineValidation(t *testing.T) {
377 for _, d := range []time.Duration{-time.Second, time.Nanosecond, time.Duration(1<<32) * time.Millisecond} {
378 if _, err := normalize([]time.Duration{d}); err == nil {
379 t.Fatal(d)
380 }
381 }
382 ds, err := normalize([]time.Duration{0, time.Second, time.Second})
383 if err != nil || len(ds) != 1 {
384 t.Fatal(ds, err)
385 }
386}
387
388func TestCancelBlockedStartup(t *testing.T) {
389 conn, peer := sockets(t)
390 c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
391 if err != nil {
392 t.Fatal(err)
393 }
394 readWire(t, peer)
395 ctx, cancel := context.WithCancel(context.Background())
396 done := make(chan error, 1)
397 go func() { done <- c.Run(ctx, nil, nil) }()
398 readWire(t, peer) // sync request deliberately unanswered
399 cancel()
400 select {
401 case err := <-done:
402 if err != nil {
403 t.Fatal(err)
404 }
405 case <-time.After(time.Second):
406 t.Fatal("cancel did not close socket")
407 }
408}
409
410func TestBoundedCallbackQueueFailsClosed(t *testing.T) {
411 conn, peer := sockets(t)
412 c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
413 if err != nil {
414 t.Fatal(err)
415 }
416 readWire(t, peer)
417 for i := 0; i <= queueSize; i++ {
418 c.enqueue(notice{kind: 1})
419 }
420 select {
421 case <-c.overflow:
422 default:
423 t.Fatal("overflow not reported")
424 }
425}