1//go:build linux
2
3package wayland
4
5import (
6 "context"
7 "encoding/binary"
8 "errors"
9 "fmt"
10 "github.com/bnema/wlturbo/protocol/linuxdmabuf"
11 "golang.org/x/sys/unix"
12 "net"
13 "os"
14 "strings"
15 "syscall"
16 "testing"
17 "time"
18
19 "github.com/bnema/wlturbo"
20 "github.com/bnema/wlturbo/protocol/core"
21 "github.com/bnema/wlturbo/protocol/cursorshape"
22)
23
24func TestSharedConnectionKeyboardRoutesOnlyFocusedWindow(t *testing.T) {
25 ca, server := inputPair(t)
26 d, err := wlturbo.ConnectFromConn(ca)
27 if err != nil {
28 t.Fatal(err)
29 }
30 defer d.Close()
31 inputRequest(t, server)
32 c := &Connection{Display: d, events: make(chan Event, 32), stop: make(chan struct{}), windows: make(map[uint32]*Window)}
33 host := &Window{connection: c, seatVersion: 9}
34 seat := core.NewSeat(d.Context())
35 if err := d.Registry().Bind(1, core.SeatInterface, 9, seat); err != nil {
36 t.Fatal(err)
37 }
38 inputRequest(t, server)
39 keyboard, err := seat.GetKeyboard()
40 if err != nil {
41 t.Fatal(err)
42 }
43 inputRequest(t, server)
44 first, second := &Window{connection: c}, &Window{connection: c}
45 c.routes.Store(uint32(30), first)
46 c.routes.Store(uint32(31), second)
47 host.watchSharedKeyboard(keyboard)
48 // enter includes a length-prefixed empty key array, then key down.
49 for _, msg := range [][]byte{inputFrame(keyboard.ID(), 1, 1, 30, 0), inputFrame(keyboard.ID(), 3, 2, 0, 30, 1), inputFrame(keyboard.ID(), 2, 3, 30), inputFrame(keyboard.ID(), 1, 4, 31, 0), inputFrame(keyboard.ID(), 3, 5, 0, 48, 1)} {
50 inputSend(t, server, msg)
51 if err := d.Dispatch(); err != nil {
52 t.Fatal(err)
53 }
54 }
55 var keys []Event
56 for len(c.events) > 0 {
57 ev := <-c.events
58 if ev.Kind == InputKey {
59 keys = append(keys, ev)
60 }
61 }
62 if len(keys) != 2 || keys[0].Window != first || keys[0].Code != 30 || keys[1].Window != second || keys[1].Code != 48 {
63 t.Fatalf("keys: %+v", keys)
64 }
65}
66
67func TestHarnessConnectionLock(t *testing.T) {
68 out, _ := runLayerHarness(t, "TestHarnessConnectionLockClient")
69 for _, want := range []string{"LOCK acquired", "LOCK exact configure", "LOCK shared display", "LOCK sibling survived", "LOCK unlock", "LOCK manager reused", "LOCK named layer sibling"} {
70 if !strings.Contains(out, want) {
71 t.Fatalf("missing %q in %s", want, out)
72 }
73 }
74}
75func TestHarnessConnectionLockClient(t *testing.T) {
76 harnessChild(t)
77 ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
78 defer cancel()
79 c, err := ConnectConnection(ctx, "")
80 if err != nil {
81 t.Fatal(err)
82 }
83 defer c.Close()
84 outputs := c.Outputs()
85 if len(outputs) != 1 {
86 t.Fatalf("outputs %+v", outputs)
87 }
88 if err := c.AcquireLock(); err != nil {
89 t.Fatal(err)
90 }
91 events := c.StartReader()
92 wait := func(want EventKind) {
93 t.Helper()
94 for {
95 select {
96 case ev := <-events:
97 if err := c.Apply(ev); err != nil {
98 t.Fatal(err)
99 }
100 if ev.Kind == want {
101 return
102 }
103 case <-ctx.Done():
104 t.Fatal("event timeout")
105 }
106 }
107 }
108 wait(LockAcquired)
109 fmt.Println("LOCK acquired")
110 first, err := c.NewWindow(ctx, 1, 1, false, SurfaceOptions{Lock: &LockOptions{Output: outputs[0].Global}})
111 if err != nil {
112 t.Fatal(err)
113 }
114 for !first.Configured || !first.FeedbackDone || first.MainDevice == 0 || len(first.Tranches) == 0 {
115 select {
116 case ev := <-events:
117 if err := c.Apply(ev); err != nil {
118 t.Fatal(err)
119 }
120 case <-ctx.Done():
121 t.Fatal("lock configure timeout")
122 }
123 }
124 if first.Width != 400 || first.Height != 300 || first.LockSurface == nil || first.XdgSurface != nil || first.LayerSurface != nil {
125 t.Fatalf("lock geometry/roles: %dx%d", first.Width, first.Height)
126 }
127 fmt.Println("LOCK exact configure")
128 // An ordinary sibling shares the transport, but cannot acquire another lock.
129 second, err := c.NewWindow(ctx, 20, 20, false, SurfaceOptions{})
130 if err != nil {
131 t.Fatal(err)
132 }
133 if first.Display != second.Display || first.Display != c.Display || first.Seat != nil {
134 t.Fatal("independent display or lock exposed clipboard/IME seat")
135 }
136 fmt.Println("LOCK shared display")
137 if err := first.Close(); err != nil {
138 t.Fatal(err)
139 }
140 if second.Display == nil || c.closed {
141 t.Fatal("window close destroyed shared connection")
142 }
143 if err := second.SetTitle("sibling survives"); err != nil {
144 t.Fatal(err)
145 }
146 fmt.Println("LOCK sibling survived")
147 if err := c.UnlockAndDestroy(); err != nil {
148 t.Fatal(err)
149 }
150 fmt.Println("LOCK unlock")
151 manager := c.lockManager
152 for range 2 {
153 if err := c.AcquireLock(); err != nil {
154 t.Fatal(err)
155 }
156 wait(LockAcquired)
157 if c.lockManager != manager {
158 t.Fatal("lock manager leaked across cycles")
159 }
160 if err := c.UnlockAndDestroy(); err != nil {
161 t.Fatal(err)
162 }
163 }
164 fmt.Println("LOCK manager reused")
165 layer, err := c.NewWindow(ctx, 20, 20, true, SurfaceOptions{Layer: &LayerOptions{Output: outputs[0].Name, Layer: LayerTop, Namespace: "named-sibling"}})
166 if err != nil {
167 t.Fatal(err)
168 }
169 for !layer.Configured || !layer.FeedbackDone {
170 select {
171 case ev := <-events:
172 if err := c.Apply(ev); err != nil {
173 t.Fatal(err)
174 }
175 case <-ctx.Done():
176 t.Fatal("named sibling timeout")
177 }
178 }
179 if layer.layerOutputGlobal != outputs[0].Global {
180 t.Fatal("named sibling selected wrong output")
181 }
182 fmt.Println("LOCK named layer sibling")
183}
184
185func TestOwnerFocusResetWithFullReaderQueue(t *testing.T) {
186 c := &Connection{events: make(chan Event, 256), stop: make(chan struct{}), windows: map[uint32]*Window{1: {}}}
187 for range cap(c.events) {
188 c.events <- Event{Kind: LockAcquired}
189 }
190 done := make(chan error, 1)
191 go func() { done <- c.Apply(Event{Kind: InputFocusOut}) }()
192 select {
193 case err := <-done:
194 if err != nil {
195 t.Fatal(err)
196 }
197 case <-time.After(time.Second):
198 close(c.stop)
199 t.Fatal("owner reset self-posted into full reader queue")
200 }
201 if len(c.events) != cap(c.events) {
202 t.Fatal("owner reset consumed reader events")
203 }
204 inputs := c.windows[1].DrainInput(nil)
205 if len(inputs) != 1 || inputs[0].Kind != "focus-out" {
206 t.Fatalf("inputs %+v", inputs)
207 }
208}
209
210func TestKeyboardMapCacheRetiresEveryGeneration(t *testing.T) {
211 for range 4 {
212 f, err := os.CreateTemp(t.TempDir(), "keymap")
213 if err != nil {
214 t.Fatal(err)
215 }
216 fd, err := syscall.Dup(int(f.Fd()))
217 if err != nil {
218 t.Fatal(err)
219 }
220 f.Close()
221 cache := &keyboardMapCache{fd: -1}
222 cache.replace(fd)
223 duplicate, err := cache.duplicate()
224 if err != nil || duplicate < 0 {
225 t.Fatalf("duplicate %d %v", duplicate, err)
226 }
227 if flags, err := unix.FcntlInt(uintptr(duplicate), unix.F_GETFD, 0); err != nil || flags&unix.FD_CLOEXEC == 0 {
228 t.Fatalf("delivered keymap duplicate is inheritable: flags %d err %v", flags, err)
229 }
230 cache.close()
231 cache.close()
232 if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); !errors.Is(err, syscall.EBADF) {
233 t.Fatalf("retired map FD still open: %v", err)
234 }
235 if _, err := unix.FcntlInt(uintptr(duplicate), unix.F_GETFD, 0); err != nil {
236 t.Fatal("retired source closed delivered duplicate")
237 }
238 syscall.Close(duplicate)
239 if got, err := cache.duplicate(); got != -1 || err != nil {
240 t.Fatal("retired callback duplicated map")
241 }
242 }
243}
244
245func TestFeedbackReadinessWaitsForCompleteEvent(t *testing.T) {
246 w := &Window{Configured: true}
247 if err := w.Apply(Event{Kind: FeedbackMainDevice, Device: 1}); err != nil {
248 t.Fatal(err)
249 }
250 if err := w.Apply(Event{Kind: FeedbackTranche, Tranche: []linuxdmabuf.FormatEntry{{}}}); err != nil {
251 t.Fatal(err)
252 }
253 if w.FeedbackDone {
254 t.Fatal("partial feedback treated as complete")
255 }
256 if err := w.Apply(Event{Kind: FeedbackFormats, Formats: []linuxdmabuf.FormatEntry{{}}}); err != nil {
257 t.Fatal(err)
258 }
259 if w.FeedbackDone {
260 t.Fatal("table alone treated as complete")
261 }
262 if err := w.Apply(Event{Kind: FeedbackComplete}); err != nil || !w.FeedbackDone {
263 t.Fatal("done not applied")
264 }
265}
266
267// sharedSeat builds a Connection whose seat host is bound over a socketpair.
268func sharedSeat(t *testing.T) (*Connection, *wlturbo.Display, *net.UnixConn) {
269 t.Helper()
270 ca, server := inputPair(t)
271 d, err := wlturbo.ConnectFromConn(ca)
272 if err != nil {
273 t.Fatal(err)
274 }
275 t.Cleanup(func() { _ = d.Close() })
276 inputRequest(t, server)
277 c := &Connection{Display: d, events: make(chan Event, 256), stop: make(chan struct{}), windows: make(map[uint32]*Window)}
278 c.seat = &Window{Display: d, connection: c, seatVersion: 9}
279 c.seat.Seat = core.NewSeat(d.Context())
280 if err := d.Registry().Bind(1, core.SeatInterface, 9, c.seat.Seat); err != nil {
281 t.Fatal(err)
282 }
283 inputRequest(t, server)
284 return c, d, server
285}
286
287func fdOpen(fd int) bool {
288 _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
289 return err == nil
290}
291
292func TestKeyboardGenerationKeymapClosedExactlyOnce(t *testing.T) {
293 c, d, server := sharedSeat(t)
294 for generation := range 3 {
295 if err := c.Apply(Event{Kind: SeatCapabilities, Capabilities: 2}); err != nil {
296 t.Fatal(err)
297 }
298 inputRequest(t, server) // get_keyboard
299 cache := c.seat.keymapCache
300 if cache == nil || c.seat.WLKeyboard == nil {
301 t.Fatalf("generation %d: no keyboard cache", generation)
302 }
303 f, err := os.CreateTemp(t.TempDir(), "keymap")
304 if err != nil {
305 t.Fatal(err)
306 }
307 var taken []int
308 for range 2 { // a second keymap in one generation replaces the first
309 inputSend(t, server, inputFrame(c.seat.WLKeyboard.ID(), 0, 1, 4), int(f.Fd()))
310 if err = d.Dispatch(); err != nil {
311 t.Fatal(err)
312 }
313 cache.mu.Lock()
314 taken = append(taken, cache.fd)
315 cache.mu.Unlock()
316 }
317 f.Close()
318 if taken[0] == taken[1] || fdOpen(taken[0]) {
319 t.Fatalf("replaced keymap FD %d not closed", taken[0])
320 }
321 if !fdOpen(taken[1]) {
322 t.Fatalf("generation %d: live keymap FD closed", generation)
323 }
324 if err := c.Apply(Event{Kind: SeatCapabilities, Capabilities: 0}); err != nil {
325 t.Fatal(err)
326 }
327 inputRequest(t, server) // release
328 if fdOpen(taken[1]) {
329 t.Fatalf("generation %d: keymap FD %d leaked after capability loss", generation, taken[1])
330 }
331 if c.seat.keymapCache != nil || c.seat.WLKeyboard != nil {
332 t.Fatal("keyboard generation not retired")
333 }
334 cache.close() // a retired cache closes nothing twice
335 }
336}
337
338func TestPointerReplacementResetsSiblingCursor(t *testing.T) {
339 c, d, server := sharedSeat(t)
340 ctx := d.Context()
341 shapes := cursorshape.NewWpCursorShapeManager(ctx)
342 if err := d.Registry().Bind(2, cursorshape.WpCursorShapeManagerInterface, 1, shapes); err != nil {
343 t.Fatal(err)
344 }
345 inputRequest(t, server)
346 sibling := &Window{connection: c, CursorShapes: shapes, seatVersion: 9}
347 c.windows[1] = sibling
348 if err := c.Apply(Event{Kind: SeatCapabilities, Capabilities: 1}); err != nil {
349 t.Fatal(err)
350 }
351 if id, op, _ := readRequest(t, server); id != c.seat.Seat.ID() || op != 0 {
352 t.Fatalf("get_pointer %d %d", id, op)
353 }
354 first := sibling.Pointer
355 if first == nil || sibling.pointerGen != 1 {
356 t.Fatalf("sibling pointer/gen %v %d", first, sibling.pointerGen)
357 }
358 sibling.PointerEntered(7, 1)
359 if err := sibling.SetCursor(CursorPointer); err != nil {
360 t.Fatal(err)
361 }
362 _, _, body := readRequest(t, server) // shape device
363 device := binary.LittleEndian.Uint32(body)
364 readRequest(t, server) // set_shape
365 // Pointer capability loss: device destroyed before the pointer, serial gone.
366 if err := c.Apply(Event{Kind: SeatCapabilities, Capabilities: 0}); err != nil {
367 t.Fatal(err)
368 }
369 if id, op, _ := readRequest(t, server); id != device || op != 0 {
370 t.Fatalf("shape device not destroyed first: %d %d", id, op)
371 }
372 if id, _, _ := readRequest(t, server); id != first.ID() {
373 t.Fatalf("pointer not released after device: %d", id)
374 }
375 if sibling.Pointer != nil || sibling.cursor.serial != 0 || sibling.cursor.device != nil {
376 t.Fatal("sibling kept stale pointer state")
377 }
378 // Replacement: a late enter of the released generation is ignored.
379 if err := c.Apply(Event{Kind: SeatCapabilities, Capabilities: 1}); err != nil {
380 t.Fatal(err)
381 }
382 readRequest(t, server) // get_pointer
383 if sibling.pointerGen != 2 || sibling.Pointer == nil || sibling.Pointer == first {
384 t.Fatalf("replacement not adopted: gen %d", sibling.pointerGen)
385 }
386 sibling.PointerEntered(9, 1)
387 if err := sibling.SetCursor(CursorPointer); err != nil {
388 t.Fatal(err)
389 }
390 noRequest(t, server)
391 sibling.PointerEntered(11, 2)
392 if err := sibling.SetCursor(CursorPointer); err != nil {
393 t.Fatal(err)
394 }
395 _, _, body = readRequest(t, server)
396 if binary.LittleEndian.Uint32(body[4:]) != sibling.Pointer.ID() {
397 t.Fatal("shape device bound to a stale pointer")
398 }
399 if _, _, body = readRequest(t, server); binary.LittleEndian.Uint32(body) != 11 {
400 t.Fatalf("set_shape used stale serial: %x", body)
401 }
402}
403
404func TestOwnerSeatRemovalWithFullReaderQueue(t *testing.T) {
405 c := &Connection{events: make(chan Event, 256), stop: make(chan struct{}), windows: map[uint32]*Window{}}
406 c.seat = &Window{connection: c}
407 lock := &Window{LockSurface: nil, rawInput: []Event{{Kind: InputKey}}}
408 c.windows[1], c.windows[2] = &Window{}, lock
409 for range cap(c.events) {
410 c.events <- Event{Kind: LockAcquired}
411 }
412 done := make(chan error, 1)
413 go func() { done <- c.Apply(Event{Kind: SeatRemoved}) }()
414 select {
415 case err := <-done:
416 if err != nil {
417 t.Fatal(err)
418 }
419 case <-time.After(time.Second):
420 close(c.stop)
421 t.Fatal("seat removal blocked on the full reader queue")
422 }
423 if len(c.events) != cap(c.events) {
424 t.Fatal("owner apply consumed or added reader events")
425 }
426}
427
428func TestReaderPauseNeverBlocksOnFullQueueAndResumes(t *testing.T) {
429 ca, server := inputPair(t)
430 d, err := wlturbo.ConnectFromConn(ca)
431 if err != nil {
432 t.Fatal(err)
433 }
434 inputRequest(t, server)
435 c := &Connection{Display: d, socket: ca, events: make(chan Event, 1), stop: make(chan struct{}), windows: map[uint32]*Window{}, pauseSig: make(chan struct{}), pauseAck: make(chan struct{}, 1), resume: make(chan struct{}, 1)}
436 seat := core.NewSeat(d.Context())
437 if err := d.Registry().Bind(1, core.SeatInterface, 9, seat); err != nil {
438 t.Fatal(err)
439 }
440 inputRequest(t, server)
441 seat.OnCapabilities(func(caps uint32) { c.post(Event{Kind: SeatCapabilities, Capabilities: caps}) })
442 c.StartReader()
443 defer func() { _ = c.Close() }()
444 // Fill the one-slot queue, then a second event blocks the reader in post.
445 inputSend(t, server, inputFrame(seat.ID(), 0, 1))
446 inputSend(t, server, inputFrame(seat.ID(), 0, 2))
447 time.Sleep(50 * time.Millisecond)
448 paused := make(chan func(), 1)
449 go func() { paused <- c.pauseReader() }()
450 var resume func()
451 select {
452 case resume = <-paused:
453 case <-time.After(2 * time.Second):
454 t.Fatal("pause deadlocked against the full reader queue")
455 }
456 if first := <-c.events; first.Capabilities != 1 {
457 t.Fatalf("first event %+v", first)
458 }
459 resume()
460 select {
461 case ev := <-c.events:
462 if ev.Capabilities != 2 {
463 t.Fatalf("parked event lost or reordered: %+v", ev)
464 }
465 case <-time.After(2 * time.Second):
466 t.Fatal("parked event not re-posted after resume")
467 }
468 // A pause with an idle blocked read is interrupted too, and reading resumes.
469 resume2 := c.pauseReader()
470 resume2()
471 inputSend(t, server, inputFrame(seat.ID(), 0, 4))
472 select {
473 case ev := <-c.events:
474 if ev.Capabilities != 4 {
475 t.Fatalf("event after resume %+v", ev)
476 }
477 case <-time.After(2 * time.Second):
478 t.Fatal("reader did not resume")
479 }
480}
481
482func TestReadyRequiresConfigureFeedbackAndFrame(t *testing.T) {
483 w := &Window{Configured: true, FrameReady: true}
484 if w.Ready() {
485 t.Fatal("ready before dmabuf feedback completed")
486 }
487 w.FeedbackDone = true
488 if !w.Ready() {
489 t.Fatal("not ready")
490 }
491 // A later feedback round invalidates readiness until its own done.
492 if err := w.Apply(Event{Kind: FeedbackMainDevice, Device: 2}); err != nil {
493 t.Fatal(err)
494 }
495 if w.Ready() || len(w.Tranches) != 0 {
496 t.Fatal("stale feedback kept across rounds")
497 }
498 if err := w.Present(nil, nil, nil, 0, 0); err == nil {
499 t.Fatal("present without buffer accepted")
500 }
501}
502
503func TestDrainStartupHoldsLockAndOutputEventsInOrder(t *testing.T) {
504 c := &Connection{events: make(chan Event, 16), stop: make(chan struct{}), windows: map[uint32]*Window{}, outputs: map[uint32]Output{}}
505 c.events <- Event{Kind: OutputAdded, Output: Output{Global: 3}}
506 c.events <- Event{Kind: LockAcquired}
507 c.events <- Event{Kind: OutputRemoved, Output: Output{Global: 3}}
508 if err := c.drainStartup(true); err != nil {
509 t.Fatal(err)
510 }
511 if c.Locked() || c.lockDone {
512 t.Fatal("startup drain applied `locked`, hiding it from the owner")
513 }
514 held := c.TakeHeld()
515 if len(held) != 3 || held[0].Kind != OutputAdded || held[1].Kind != LockAcquired || held[2].Kind != OutputRemoved {
516 t.Fatalf("held %+v", held)
517 }
518 if len(c.TakeHeld()) != 0 {
519 t.Fatal("held events delivered twice")
520 }
521 // The owner applies them itself; unlock then becomes possible.
522 for _, ev := range held {
523 if err := c.Apply(ev); err != nil {
524 t.Fatal(err)
525 }
526 }
527 if !c.Locked() {
528 t.Fatal("locked not recorded once the owner applied it")
529 }
530}
531
532func TestDrainStartupFinishedWrapsSentinelAndKeepsEvent(t *testing.T) {
533 c := &Connection{events: make(chan Event, 16), stop: make(chan struct{}), windows: map[uint32]*Window{}}
534 c.events <- Event{Kind: LockFinished}
535 err := c.drainStartup(true)
536 if !errors.Is(err, ErrLockFinished) {
537 t.Fatalf("err %v", err)
538 }
539 if held := c.TakeHeld(); len(held) != 1 || held[0].Kind != LockFinished {
540 t.Fatalf("finished event lost: %+v", held)
541 }
542 // Window-tagged and ordinary events are still applied during setup.
543 c.events <- Event{Kind: BufferRelease, BufferID: 1}
544 if err := c.drainStartup(true); err == nil || errors.Is(err, ErrLockFinished) {
545 t.Logf("non-lock event handled: %v", err)
546 }
547}
548
549// A closing session must never read the shared queue: it carries other
550// windows' releases, lock state, output changes and transport errors.
551func TestSharedWindowCloseLeavesReaderQueueAlone(t *testing.T) {
552 c := &Connection{events: make(chan Event, 16), stop: make(chan struct{}), windows: map[uint32]*Window{}}
553 closing, other := &Window{connection: c, events: c.events}, &Window{connection: c, events: c.events}
554 if closing.ReaderEvents() != nil {
555 t.Fatal("shared window exposes the connection queue to its session")
556 }
557 if (&Window{events: make(chan Event)}).ReaderEvents() == nil {
558 t.Fatal("standalone window lost its own queue")
559 }
560 queued := []Event{
561 {Kind: BufferRelease, BufferID: 1, Window: other},
562 {Kind: LockAcquired},
563 {Kind: OutputRemoved, Output: Output{Global: 9}},
564 {Kind: TransportError, Err: errors.New("gone")},
565 {Kind: InputKey, Code: 30, Window: other},
566 }
567 for _, ev := range queued {
568 c.events <- ev
569 }
570 if err := closing.Close(); err != nil {
571 t.Fatal(err)
572 }
573 if len(c.events) != len(queued) {
574 t.Fatalf("close consumed %d shared events", len(queued)-len(c.events))
575 }
576 for i, want := range queued {
577 if got := <-c.events; got.Kind != want.Kind || got.Window != want.Window {
578 t.Fatalf("event %d reordered: %+v", i, got)
579 }
580 }
581}