1package wayland
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "github.com/bnema/neferwl/internal/ports"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12
13 "github.com/bnema/purego-libwayland/protocol/wayland"
14 "github.com/bnema/purego-libwayland/protocol/xdgshell"
15 "github.com/bnema/wlturbo"
16 "golang.org/x/sys/unix"
17)
18
19func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display {
20 t.Helper()
21 c, err := wlturbo.Connect(filepath.Join(dir, s.SocketName()))
22 if err != nil {
23 t.Fatal(err)
24 }
25 t.Cleanup(func() { _ = c.Close() })
26 if err := c.Roundtrip(); err != nil {
27 t.Fatal(err)
28 }
29 return c
30}
31
32func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {
33 t.Helper()
34 return bindVersion(t, c, iface, 1)
35}
36
37func bindVersion(t *testing.T, c *wlturbo.Display, iface string, version uint32) uint32 {
38 t.Helper()
39 g, ok := c.Registry().FindGlobal(iface)
40 if !ok {
41 t.Fatalf("missing %s", iface)
42 }
43 id, err := c.Registry().BindID(g.Name, g.Interface, version)
44 if err != nil {
45 t.Fatal(err)
46 }
47 return id
48}
49
50func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, args ...any) {
51 t.Helper()
52 if err := c.SendRequest(id, uint16(op), args...); err != nil {
53 t.Fatal(err)
54 }
55}
56
57func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) {
58 t.Helper()
59 done := make(chan error, 1)
60 go func() {
61 for {
62 if err := c.Dispatch(); err != nil {
63 done <- err
64 return
65 }
66 }
67 }()
68 select {
69 case err := <-done:
70 var displayErr *wlturbo.DisplayError
71 if !errors.As(err, &displayErr) {
72 t.Fatalf("expected display error, got %v", err)
73 }
74 if displayErr.ObjectID != object || displayErr.Code != code {
75 t.Fatalf("protocol error = %v, want object=%d code=%d", displayErr, object, code)
76 }
77 case <-time.After(2 * time.Second):
78 _ = c.Close()
79 t.Fatal("protocol error timeout")
80 }
81}
82
83type protocolProxy struct{ wlturbo.BaseProxy }
84
85func (*protocolProxy) Dispatch(*wlturbo.Event) {}
86
87func registerProtocol(t *testing.T, c *wlturbo.Display, id uint32) {
88 t.Helper()
89 p := &protocolProxy{}
90 p.SetID(id)
91 c.Context().Register(p)
92}
93
94func TestXDGProtocolErrors(t *testing.T) {
95 for _, scenario := range []string{"bogus ack", "buffer before ack", "subsurface role", "defunct toplevel"} {
96 t.Run(scenario, func(t *testing.T) {
97 s, _, _, dir := lifecycleServer(t)
98 c := protocolClient(t, s, dir)
99 compositor := bindProtocol(t, c, "wl_compositor")
100 wm := bindProtocol(t, c, "xdg_wm_base")
101 surface := c.AllocateID()
102 requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, surface)
103 registerProtocol(t, c, surface)
104 if scenario == "subsurface role" {
105 parent := c.AllocateID()
106 requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, parent)
107 registerProtocol(t, c, parent)
108 subcomp := bindProtocol(t, c, "wl_subcompositor")
109 requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, c.AllocateID(), surface, parent)
110 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surface)
111 expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole))
112 return
113 }
114 xdg := c.AllocateID()
115 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surface)
116 top := c.AllocateID()
117 registerProtocol(t, c, xdg)
118 registerProtocol(t, c, top)
119 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
120 switch scenario {
121 case "bogus ack":
122 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0))
123 expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial))
124 case "buffer before ack":
125 shm := bindProtocol(t, c, "wl_shm")
126 fd, err := unix.MemfdCreate("protocol-buffer", 0)
127 if err != nil {
128 t.Fatal(err)
129 }
130 defer unix.Close(fd)
131 if err := unix.Ftruncate(fd, 4); err != nil {
132 t.Fatal(err)
133 }
134 pool := c.AllocateID()
135 registerProtocol(t, c, shm)
136 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
137 t.Fatal(err)
138 }
139 buffer := c.AllocateID()
140 registerProtocol(t, c, pool)
141 registerProtocol(t, c, buffer)
142 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
143 requestProtocol(t, c, surface, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
144 requestProtocol(t, c, surface, wayland.SurfaceRequestCommit)
145 expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorUnconfiguredBuffer))
146 case "defunct toplevel":
147 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestDestroy)
148 expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorDefunctRoleObject))
149 }
150 })
151 }
152}
153
154func TestPopupBlocksSecondRole(t *testing.T) {
155 s, _, _, dir := lifecycleServer(t)
156 c := protocolClient(t, s, dir)
157 comp := bindProtocol(t, c, "wl_compositor")
158 wm := bindProtocol(t, c, "xdg_wm_base")
159 surf, xdg := c.AllocateID(), c.AllocateID()
160 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
161 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
162 registerProtocol(t, c, xdg)
163 registerProtocol(t, c, surf)
164 positioner := c.AllocateID()
165 requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
166 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(10), int32(10))
167 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
168 if err := c.Roundtrip(); err != nil {
169 t.Fatal(err)
170 }
171 popup := c.AllocateID()
172 registerProtocol(t, c, popup)
173 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner)
174 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, c.AllocateID())
175 expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorAlreadyConstructed))
176}
177
178type configureProxy struct {
179 wlturbo.BaseProxy
180 serial chan uint32
181}
182
183func (p *configureProxy) Dispatch(e *wlturbo.Event) {
184 if e.Opcode == uint16(xdgshell.SurfaceEventConfigure) {
185 p.serial <- e.Uint32()
186 }
187}
188
189func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) {
190 s, events, _, dir := lifecycleServer(t)
191 c := protocolClient(t, s, dir)
192 comp := bindProtocol(t, c, "wl_compositor")
193 wm := bindProtocol(t, c, "xdg_wm_base")
194 shm := bindProtocol(t, c, "wl_shm")
195 surf, xdg := c.AllocateID(), c.AllocateID()
196 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
197 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
198 serials := make(chan uint32, 4)
199 proxy := &configureProxy{serial: serials}
200 proxy.SetID(xdg)
201 c.Context().Register(proxy)
202 fd, err := unix.MemfdCreate("recreated-buffer", 0)
203 if err != nil {
204 t.Fatal(err)
205 }
206 defer unix.Close(fd)
207 if err := unix.Ftruncate(fd, 4); err != nil {
208 t.Fatal(err)
209 }
210 registerProtocol(t, c, shm)
211 pool, buffer := c.AllocateID(), c.AllocateID()
212 registerProtocol(t, c, pool)
213 registerProtocol(t, c, buffer)
214 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
215 t.Fatal(err)
216 }
217 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
218 top := c.AllocateID()
219 registerProtocol(t, c, top)
220 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
221 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
222 if err := c.Roundtrip(); err != nil {
223 t.Fatal(err)
224 }
225 select {
226 case serial := <-serials:
227 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
228 case <-time.After(2 * time.Second):
229 t.Fatal("initial configure timeout")
230 }
231 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
232 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
233 if err := c.Roundtrip(); err != nil {
234 t.Fatal(err)
235 }
236 mapped(t, events, 2*time.Second)
237 requestProtocol(t, c, top, xdgshell.ToplevelRequestDestroy)
238 if err := c.Roundtrip(); err != nil {
239 t.Fatal(err)
240 }
241 select {
242 case ev := <-events:
243 if _, ok := ev.(ports.WindowUnmapped); !ok {
244 t.Fatalf("expected unmap, got %T", ev)
245 }
246 case <-time.After(2 * time.Second):
247 t.Fatal("unmap timeout")
248 }
249 top = c.AllocateID()
250 registerProtocol(t, c, top)
251 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
252 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
253 if err := c.Roundtrip(); err != nil {
254 t.Fatal(err)
255 }
256 select {
257 case serial := <-serials:
258 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
259 case <-time.After(2 * time.Second):
260 t.Fatal("recreated configure timeout")
261 }
262 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
263 if err := c.Roundtrip(); err != nil {
264 t.Fatal(err)
265 }
266 select {
267 case ev := <-events:
268 t.Fatalf("mapped without new buffer: %T", ev)
269 case <-time.After(300 * time.Millisecond):
270 }
271 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
272 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
273 if err := c.Roundtrip(); err != nil {
274 t.Fatal(err)
275 }
276 mapped(t, events, 2*time.Second)
277}
278
279func TestSHMInvalidStride(t *testing.T) {
280 s, _, _, dir := lifecycleServer(t)
281 c := protocolClient(t, s, dir)
282 shm := bindProtocol(t, c, "wl_shm")
283 fd, err := unix.MemfdCreate("invalid-stride", 0)
284 if err != nil {
285 t.Fatal(err)
286 }
287 defer unix.Close(fd)
288 if err := unix.Ftruncate(fd, 16); err != nil {
289 t.Fatal(err)
290 }
291 pool := c.AllocateID()
292 registerProtocol(t, c, shm)
293 registerProtocol(t, c, pool)
294 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
295 t.Fatal(err)
296 }
297 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888))
298 expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride))
299}
300
301func TestSHMUnpaddedLastRow(t *testing.T) {
302 for _, tc := range []struct {
303 name string
304 size int32
305 valid bool
306 }{{"exact", 28, true}, {"short", 27, false}} {
307 t.Run(tc.name, func(t *testing.T) {
308 s, _, _, dir := lifecycleServer(t)
309 c := protocolClient(t, s, dir)
310 shm := bindProtocol(t, c, "wl_shm")
311 fd, err := unix.MemfdCreate("unpadded-last-row", 0)
312 if err != nil {
313 t.Fatal(err)
314 }
315 defer unix.Close(fd)
316 if err := unix.Ftruncate(fd, int64(tc.size)); err != nil {
317 t.Fatal(err)
318 }
319 pool := c.AllocateID()
320 registerProtocol(t, c, shm)
321 registerProtocol(t, c, pool)
322 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, tc.size); err != nil {
323 t.Fatal(err)
324 }
325 buffer := c.AllocateID()
326 registerProtocol(t, c, buffer)
327 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(4), int32(2), int32(2), int32(16), uint32(wayland.ShmFormatXrgb8888))
328 if !tc.valid {
329 expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride))
330 return
331 }
332 if err := c.Roundtrip(); err != nil {
333 t.Fatal(err)
334 }
335 })
336 }
337}
338
339// A buffer whose pool file was shrunk below it has no content.
340func TestBufferContentTruncatedPool(t *testing.T) {
341 fd, err := unix.MemfdCreate("truncated-pool", unix.MFD_CLOEXEC)
342 if err != nil {
343 t.Fatal(err)
344 }
345 f := os.NewFile(uintptr(fd), "truncated-pool")
346 defer f.Close()
347 if err := f.Truncate(28); err != nil {
348 t.Fatal(err)
349 }
350 b := &buffer{pool: &pool{id: 3, file: f, size: 28}, offset: 4, width: 2, height: 2, stride: 16}
351 c, ok := b.content(1)
352 if !ok || c.SHM == nil || c.SHM.Pool != 3 || c.SHM.Offset != 4 || c.SHM.Stride != 16 {
353 t.Fatalf("content ok=%v %+v", ok, c.SHM)
354 }
355 if err := f.Truncate(27); err != nil {
356 t.Fatal(err)
357 }
358 if _, ok := b.content(1); ok {
359 t.Fatal("content from a truncated pool")
360 }
361}
362
363type releaseProxy struct {
364 wlturbo.BaseProxy
365 released chan uint32
366}
367
368func (p *releaseProxy) Dispatch(e *wlturbo.Event) {
369 if e.Opcode == uint16(wayland.BufferEventRelease) {
370 p.released <- p.ID()
371 }
372}
373
374// wl_shm buffers are read in place: released when replaced or when their
375// surface is destroyed, never on commit.
376func TestSHMBufferReleasedOnReplaceAndDestroy(t *testing.T) {
377 s, _, _, dir := lifecycleServer(t)
378 c := protocolClient(t, s, dir)
379 comp := bindProtocol(t, c, "wl_compositor")
380 shm := bindProtocol(t, c, "wl_shm")
381 fd, err := unix.MemfdCreate("release", unix.MFD_CLOEXEC)
382 if err != nil {
383 t.Fatal(err)
384 }
385 defer unix.Close(fd)
386 if err := unix.Ftruncate(fd, 64); err != nil {
387 t.Fatal(err)
388 }
389 pool := c.AllocateID()
390 registerProtocol(t, c, shm)
391 registerProtocol(t, c, pool)
392 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil {
393 t.Fatal(err)
394 }
395 released := make(chan uint32, 4)
396 bufs := make([]uint32, 2)
397 for i := range bufs {
398 bufs[i] = c.AllocateID()
399 p := &releaseProxy{released: released}
400 p.SetID(bufs[i])
401 c.Context().Register(p)
402 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, bufs[i], int32(i*32), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
403 }
404 surf := c.AllocateID()
405 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
406 registerProtocol(t, c, surf)
407 expect := func(want []uint32) {
408 t.Helper()
409 if err := c.Roundtrip(); err != nil {
410 t.Fatal(err)
411 }
412 var got []uint32
413 for len(released) > 0 {
414 got = append(got, <-released)
415 }
416 if len(got) != len(want) || (len(want) == 1 && got[0] != want[0]) {
417 t.Fatalf("released %v, want %v", got, want)
418 }
419 }
420 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
421 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
422 expect(nil)
423 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
424 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
425 expect([]uint32{bufs[0]})
426 requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
427 expect([]uint32{bufs[1]})
428}
429
430type pointerEvents struct {
431 wlturbo.BaseProxy
432 enters chan [2]float64
433 buttons chan uint32
434 axes chan string
435}
436
437func (p *pointerEvents) Dispatch(e *wlturbo.Event) {
438 switch e.Opcode {
439 case uint16(wayland.PointerEventEnter):
440 _ = e.Uint32()
441 _ = e.Uint32()
442 p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()}
443 case uint16(wayland.PointerEventButton):
444 _ = e.Uint32()
445 _ = e.Uint32()
446 p.buttons <- e.Uint32()
447 case uint16(wayland.PointerEventAxisSource):
448 p.axes <- fmt.Sprint("source ", e.Uint32())
449 case uint16(wayland.PointerEventAxisValue120):
450 p.axes <- fmt.Sprint("v120 ", e.Uint32(), " ", e.Int32())
451 case uint16(wayland.PointerEventAxis):
452 _ = e.Uint32()
453 p.axes <- fmt.Sprint("axis ", e.Uint32(), " ", e.Fixed().Float64())
454 case uint16(wayland.PointerEventAxisStop):
455 _ = e.Uint32()
456 p.axes <- fmt.Sprint("stop ", e.Uint32())
457 }
458}
459
460func TestPointerProtocol(t *testing.T) {
461 s, events, commands, dir := lifecycleServer(t)
462 c := protocolClient(t, s, dir)
463 seat := bindVersion(t, c, "wl_seat", 8)
464 registerProtocol(t, c, seat)
465 pointer := c.AllocateID()
466 p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 8), axes: make(chan string, 16)}
467 p.SetID(pointer)
468 c.Context().Register(p)
469 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
470 comp := bindProtocol(t, c, "wl_compositor")
471 wm := bindProtocol(t, c, "xdg_wm_base")
472 shm := bindProtocol(t, c, "wl_shm")
473 registerProtocol(t, c, shm)
474 surf, xdg := c.AllocateID(), c.AllocateID()
475 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
476 registerProtocol(t, c, surf)
477 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
478 serials := make(chan uint32, 1)
479 xp := &configureProxy{serial: serials}
480 xp.SetID(xdg)
481 c.Context().Register(xp)
482 top := c.AllocateID()
483 registerProtocol(t, c, top)
484 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
485 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
486 if err := c.Roundtrip(); err != nil {
487 t.Fatal(err)
488 }
489 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
490 fd, err := unix.MemfdCreate("pointer-buffer", 0)
491 if err != nil {
492 t.Fatal(err)
493 }
494 defer unix.Close(fd)
495 if err := unix.Ftruncate(fd, 4); err != nil {
496 t.Fatal(err)
497 }
498 pool, buffer := c.AllocateID(), c.AllocateID()
499 registerProtocol(t, c, pool)
500 registerProtocol(t, c, buffer)
501 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
502 t.Fatal(err)
503 }
504 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
505 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
506 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
507 if err := c.Roundtrip(); err != nil {
508 t.Fatal(err)
509 }
510 w := mapped(t, events, 2*time.Second)
511 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}
512 commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}
513 // Middle, side and extra buttons pass through as evdev codes.
514 for _, b := range []uint32{0x112, 0x113, 0x114} {
515 commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1}
516 }
517 commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}}
518 commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}}
519 deadline := time.After(2 * time.Second)
520 dispatched := make(chan error, 1)
521 go func() {
522 for {
523 if err := c.Dispatch(); err != nil {
524 dispatched <- err
525 return
526 }
527 }
528 }()
529 for len(p.enters) == 0 || len(p.buttons) < 4 || len(p.axes) < 5 {
530 select {
531 case err := <-dispatched:
532 t.Fatal(err)
533 case <-deadline:
534 t.Fatal("pointer events timeout")
535 case <-time.After(time.Millisecond):
536 }
537 }
538 select {
539 case coords := <-p.enters:
540 if coords != [2]float64{10, 20} {
541 t.Fatalf("enter: %v", coords)
542 }
543 default:
544 t.Fatal("missing pointer enter")
545 }
546 for _, want := range []uint32{0x110, 0x112, 0x113, 0x114} {
547 if b := <-p.buttons; b != want {
548 t.Fatalf("button: %#x, want %#x", b, want)
549 }
550 }
551 for _, want := range []string{"source 0", "v120 0 120", "axis 0 15", "source 1", "stop 1"} {
552 if got := <-p.axes; got != want {
553 t.Fatalf("axis event %q, want %q", got, want)
554 }
555 }
556}
557
558type heldKeyProxy struct {
559 wlturbo.BaseProxy
560 enters chan []byte
561}
562
563func (p *heldKeyProxy) Dispatch(e *wlturbo.Event) {
564 if e.Opcode == uint16(wayland.KeyboardEventEnter) {
565 _ = e.Uint32()
566 _ = e.Uint32()
567 p.enters <- e.Array()
568 }
569}
570
571func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) {
572 s, events, commands, dir := keyboardServer(t)
573 c := protocolClient(t, s, dir)
574 seat := bindProtocol(t, c, "wl_seat")
575 registerProtocol(t, c, seat)
576 keyboard := c.AllocateID()
577 kp := &heldKeyProxy{enters: make(chan []byte, 4)}
578 kp.SetID(keyboard)
579 c.Context().Register(kp)
580 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboard)
581 mapWindow := toplevelMapper(t, c, events)
582 a := mapWindow().ID
583 b := mapWindow().ID
584 // Dispatch without sending a new request after focus changes.
585 dispatched := make(chan error, 1)
586 go func() {
587 for {
588 if err := c.Dispatch(); err != nil {
589 dispatched <- err
590 return
591 }
592 }
593 }()
594 enter := func(want []byte) {
595 t.Helper()
596 select {
597 case got := <-kp.enters:
598 if !bytes.Equal(got, want) {
599 t.Fatalf("keyboard enter keys = %v, want %v", got, want)
600 }
601 case err := <-dispatched:
602 t.Fatal(err)
603 case <-time.After(2 * time.Second):
604 t.Fatal("keyboard enter timeout")
605 }
606 }
607 commands <- ports.FocusWindow{ID: a}
608 enter(nil)
609 commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}
610 commands <- ports.FocusWindow{ID: b}
611 enter([]byte{30, 0, 0, 0})
612 commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}}
613 commands <- ports.FocusWindow{ID: a}
614 enter(nil)
615}
616
617// toplevelMapper returns a function that maps a new 1x1 xdg toplevel on c and
618// returns the WindowMapped event it produced.
619func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() ports.WindowMapped {
620 t.Helper()
621 m := surfaceMapper(t, c, events)
622 return func() ports.WindowMapped { w, _, _ := m(); return w }
623}
624
625// surfaceMapper is toplevelMapper also returning the wl_surface and
626// xdg_surface IDs.
627func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) {
628 t.Helper()
629 comp := bindProtocol(t, c, "wl_compositor")
630 wm := bindProtocol(t, c, "xdg_wm_base")
631 shm := bindProtocol(t, c, "wl_shm")
632 registerProtocol(t, c, shm)
633 fd, err := unix.MemfdCreate("held-key-buffer", 0)
634 if err != nil {
635 t.Fatal(err)
636 }
637 defer unix.Close(fd)
638 if err := unix.Ftruncate(fd, 4); err != nil {
639 t.Fatal(err)
640 }
641 pool, buffer := c.AllocateID(), c.AllocateID()
642 registerProtocol(t, c, pool)
643 registerProtocol(t, c, buffer)
644 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
645 t.Fatal(err)
646 }
647 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
648 return func() (ports.WindowMapped, uint32, uint32) {
649 surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
650 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
651 registerProtocol(t, c, surf)
652 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
653 serials := make(chan uint32, 1)
654 xp := &configureProxy{serial: serials}
655 xp.SetID(xdg)
656 c.Context().Register(xp)
657 registerProtocol(t, c, top)
658 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
659 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
660 if err := c.Roundtrip(); err != nil {
661 t.Fatal(err)
662 }
663 select {
664 case serial := <-serials:
665 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
666 default:
667 t.Fatal("missing configure")
668 }
669 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
670 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
671 if err := c.Roundtrip(); err != nil {
672 t.Fatal(err)
673 }
674 return mapped(t, events, 2*time.Second), surf, xdg
675 }
676}
677
678type popupDoneProxy struct {
679 wlturbo.BaseProxy
680 done chan struct{}
681}
682
683func (p *popupDoneProxy) Dispatch(e *wlturbo.Event) {
684 if e.Opcode == uint16(xdgshell.PopupEventPopupDone) {
685 p.done <- struct{}{}
686 }
687}
688
689// xdgWindow maps a 1×1 xdg_surface; role sets its role before the first commit.
690func xdgWindow(t *testing.T, c *wlturbo.Display, comp, wm, buffer uint32, role func(xdg uint32)) (surf, xdg uint32, serials chan uint32) {
691 t.Helper()
692 surf, xdg = c.AllocateID(), c.AllocateID()
693 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
694 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
695 registerProtocol(t, c, surf)
696 serials = make(chan uint32, 4)
697 proxy := &configureProxy{serial: serials}
698 proxy.SetID(xdg)
699 c.Context().Register(proxy)
700 role(xdg)
701 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
702 if err := c.Roundtrip(); err != nil {
703 t.Fatal(err)
704 }
705 return surf, xdg, serials
706}
707
708func ackAndAttach(t *testing.T, c *wlturbo.Display, surf, xdg, buffer uint32, serials chan uint32) {
709 t.Helper()
710 deadline := time.Now().Add(2 * time.Second)
711 for len(serials) == 0 && time.Now().Before(deadline) {
712 if err := c.Roundtrip(); err != nil {
713 t.Fatal(err)
714 }
715 time.Sleep(10 * time.Millisecond)
716 }
717 select {
718 case serial := <-serials:
719 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
720 default:
721 t.Fatal("configure timeout")
722 }
723 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
724 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
725 if err := c.Roundtrip(); err != nil {
726 t.Fatal(err)
727 }
728}
729
730func TestPopupAndFloatingLifecycle(t *testing.T) {
731 s, events, commands, dir := lifecycleServer(t)
732 c := protocolClient(t, s, dir)
733 comp := bindProtocol(t, c, "wl_compositor")
734 wm := bindProtocol(t, c, "xdg_wm_base")
735 shm := bindProtocol(t, c, "wl_shm")
736 registerProtocol(t, c, shm)
737 fd, err := unix.MemfdCreate("popup-buffer", 0)
738 if err != nil {
739 t.Fatal(err)
740 }
741 defer unix.Close(fd)
742 if err := unix.Ftruncate(fd, 4); err != nil {
743 t.Fatal(err)
744 }
745 pool, buffer := c.AllocateID(), c.AllocateID()
746 registerProtocol(t, c, pool)
747 registerProtocol(t, c, buffer)
748 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
749 t.Fatal(err)
750 }
751 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
752
753 // A fixed-size toplevel (min == max) floats, like a splash screen.
754 surf, xdg, serials := xdgWindow(t, c, comp, wm, buffer, func(xdg uint32) {
755 top := c.AllocateID()
756 registerProtocol(t, c, top)
757 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
758 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMinSize, int32(1), int32(1))
759 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMaxSize, int32(1), int32(1))
760 })
761 ackAndAttach(t, c, surf, xdg, buffer, serials)
762 top := mapped(t, events, 2*time.Second)
763 if !top.Floating {
764 t.Fatalf("splash not floating: %+v", top)
765 }
766 if ev, ok := waitEvent(t, events, 2*time.Second).(ports.WindowResized); !ok || ev.Width != 1 || ev.Height != 1 {
767 t.Fatalf("splash size %+v", ev)
768 }
769
770 positioner := c.AllocateID()
771 requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
772 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
773 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
774 done := make(chan struct{}, 1)
775 psurf, pxdg, pserials := xdgWindow(t, c, comp, wm, buffer, func(pxdg uint32) {
776 popup := c.AllocateID()
777 proxy := &popupDoneProxy{done: done}
778 proxy.SetID(popup)
779 c.Context().Register(proxy)
780 requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, xdg, positioner)
781 })
782 req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
783 if !ok || req.Parent != top.ID || req.Positioner.Width != 1 {
784 t.Fatalf("popup request %+v", req)
785 }
786 commands <- ports.ConfigurePopup{ID: req.ID, Rect: ports.Rect{X: 2, Y: 3, W: 1, H: 1}}
787 ackAndAttach(t, c, psurf, pxdg, buffer, pserials)
788 if ev, ok := waitEvent(t, events, 2*time.Second).(ports.PopupMapped); !ok || ev.ID != req.ID {
789 t.Fatalf("popup mapped %+v", ev)
790 }
791 commands <- ports.ClosePopup{ID: req.ID}
792 for deadline := time.Now().Add(2 * time.Second); len(done) == 0; time.Sleep(10 * time.Millisecond) {
793 if time.Now().After(deadline) {
794 t.Fatal("popup_done timeout")
795 }
796 if err := c.Roundtrip(); err != nil {
797 t.Fatal(err)
798 }
799 }
800}
801
802func TestPopupIncompletePositioner(t *testing.T) {
803 s, _, _, dir := lifecycleServer(t)
804 c := protocolClient(t, s, dir)
805 comp := bindProtocol(t, c, "wl_compositor")
806 wm := bindProtocol(t, c, "xdg_wm_base")
807 surf, xdg := c.AllocateID(), c.AllocateID()
808 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
809 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
810 registerProtocol(t, c, xdg)
811 positioner := c.AllocateID()
812 requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
813 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(10), int32(10))
814 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, c.AllocateID(), uint32(0), positioner)
815 expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorInvalidPositioner))
816}