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 comp := bindProtocol(t, c, "wl_compositor")
631 wm := bindProtocol(t, c, "xdg_wm_base")
632 shm := bindProtocol(t, c, "wl_shm")
633 registerProtocol(t, c, shm)
634 fd, err := unix.MemfdCreate("held-key-buffer", 0)
635 if err != nil {
636 t.Fatal(err)
637 }
638 defer unix.Close(fd)
639 if err := unix.Ftruncate(fd, 4); err != nil {
640 t.Fatal(err)
641 }
642 pool, buffer := c.AllocateID(), c.AllocateID()
643 registerProtocol(t, c, pool)
644 registerProtocol(t, c, buffer)
645 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
646 t.Fatal(err)
647 }
648 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
649 return func() (ports.WindowMapped, uint32, uint32) {
650 surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
651 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
652 registerProtocol(t, c, surf)
653 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
654 serials := make(chan uint32, 1)
655 xp := &configureProxy{serial: serials}
656 xp.SetID(xdg)
657 registerWireProxy(c, xp)
658 registerProtocol(t, c, top)
659 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
660 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
661 if err := c.Roundtrip(); err != nil {
662 t.Fatal(err)
663 }
664 select {
665 case serial := <-serials:
666 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
667 default:
668 t.Fatal("missing configure")
669 }
670 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
671 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
672 if err := c.Roundtrip(); err != nil {
673 t.Fatal(err)
674 }
675 return mapped(t, events, 2*time.Second), surf, xdg
676 }
677}
678
679type popupDoneProxy struct {
680 wlturbo.BaseProxy
681 done chan struct{}
682}
683
684func (p *popupDoneProxy) Dispatch(e *wlturbo.Event) {
685 if e.Opcode == uint16(xdgshell.PopupEventPopupDone) {
686 p.done <- struct{}{}
687 }
688}
689
690// xdgWindow maps a 1×1 xdg_surface; role sets its role before the first commit.
691func xdgWindow(t *testing.T, c *wlturbo.Display, comp, wm, buffer uint32, role func(xdg uint32)) (surf, xdg uint32, serials chan uint32) {
692 t.Helper()
693 surf, xdg = c.AllocateID(), c.AllocateID()
694 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
695 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
696 registerProtocol(t, c, surf)
697 serials = make(chan uint32, 4)
698 proxy := &configureProxy{serial: serials}
699 proxy.SetID(xdg)
700 registerWireProxy(c, proxy)
701 role(xdg)
702 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
703 if err := c.Roundtrip(); err != nil {
704 t.Fatal(err)
705 }
706 return surf, xdg, serials
707}
708
709func ackAndAttach(t *testing.T, c *wlturbo.Display, surf, xdg, buffer uint32, serials chan uint32) {
710 t.Helper()
711 deadline := time.Now().Add(2 * time.Second)
712 for len(serials) == 0 && time.Now().Before(deadline) {
713 if err := c.Roundtrip(); err != nil {
714 t.Fatal(err)
715 }
716 time.Sleep(10 * time.Millisecond)
717 }
718 select {
719 case serial := <-serials:
720 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
721 default:
722 t.Fatal("configure timeout")
723 }
724 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
725 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
726 if err := c.Roundtrip(); err != nil {
727 t.Fatal(err)
728 }
729}
730
731func TestPopupAndFloatingLifecycle(t *testing.T) {
732 s, events, commands, dir := lifecycleServer(t)
733 c := protocolClient(t, s, dir)
734 comp := bindProtocol(t, c, "wl_compositor")
735 wm := bindProtocol(t, c, "xdg_wm_base")
736 shm := bindProtocol(t, c, "wl_shm")
737 registerProtocol(t, c, shm)
738 fd, err := unix.MemfdCreate("popup-buffer", 0)
739 if err != nil {
740 t.Fatal(err)
741 }
742 defer unix.Close(fd)
743 if err := unix.Ftruncate(fd, 4); err != nil {
744 t.Fatal(err)
745 }
746 pool, buffer := c.AllocateID(), c.AllocateID()
747 registerProtocol(t, c, pool)
748 registerProtocol(t, c, buffer)
749 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
750 t.Fatal(err)
751 }
752 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
753
754 // A fixed-size toplevel (min == max) floats, like a splash screen.
755 surf, xdg, serials := xdgWindow(t, c, comp, wm, buffer, func(xdg uint32) {
756 top := c.AllocateID()
757 registerProtocol(t, c, top)
758 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
759 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMinSize, int32(1), int32(1))
760 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMaxSize, int32(1), int32(1))
761 })
762 ackAndAttach(t, c, surf, xdg, buffer, serials)
763 top := mapped(t, events, 2*time.Second)
764 if !top.Floating {
765 t.Fatalf("splash not floating: %+v", top)
766 }
767 if ev, ok := waitEvent(t, events, 2*time.Second).(ports.WindowResized); !ok || ev.Width != 1 || ev.Height != 1 {
768 t.Fatalf("splash size %+v", ev)
769 }
770
771 positioner := c.AllocateID()
772 requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
773 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
774 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
775 done := make(chan struct{}, 1)
776 psurf, pxdg, pserials := xdgWindow(t, c, comp, wm, buffer, func(pxdg uint32) {
777 popup := c.AllocateID()
778 proxy := &popupDoneProxy{done: done}
779 proxy.SetID(popup)
780 registerWireProxy(c, proxy)
781 requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, xdg, positioner)
782 })
783 req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
784 if !ok || req.Parent != top.ID || req.Positioner.Width != 1 {
785 t.Fatalf("popup request %+v", req)
786 }
787 commands <- ports.ConfigurePopup{ID: req.ID, Rect: ports.Rect{X: 2, Y: 3, W: 1, H: 1}}
788 ackAndAttach(t, c, psurf, pxdg, buffer, pserials)
789 if ev, ok := waitEvent(t, events, 2*time.Second).(ports.PopupMapped); !ok || ev.ID != req.ID {
790 t.Fatalf("popup mapped %+v", ev)
791 }
792 commands <- ports.ClosePopup{ID: req.ID}
793 for deadline := time.Now().Add(2 * time.Second); len(done) == 0; time.Sleep(10 * time.Millisecond) {
794 if time.Now().After(deadline) {
795 t.Fatal("popup_done timeout")
796 }
797 if err := c.Roundtrip(); err != nil {
798 t.Fatal(err)
799 }
800 }
801}
802
803func TestPopupIncompletePositioner(t *testing.T) {
804 s, _, _, dir := lifecycleServer(t)
805 c := protocolClient(t, s, dir)
806 comp := bindProtocol(t, c, "wl_compositor")
807 wm := bindProtocol(t, c, "xdg_wm_base")
808 surf, xdg := c.AllocateID(), c.AllocateID()
809 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
810 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
811 registerProtocol(t, c, xdg)
812 positioner := c.AllocateID()
813 requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
814 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(10), int32(10))
815 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, c.AllocateID(), uint32(0), positioner)
816 expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorInvalidPositioner))
817}