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