1//go:build linux
2
3package client_test
4
5import (
6 "encoding/binary"
7 "errors"
8 "net"
9 "os"
10 "slices"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/bnema/go-wayland-bindings/client/cursorshape"
16 "github.com/bnema/go-wayland-bindings/client/fractionalscale"
17 "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
18 drmsyncobj "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
19 "github.com/bnema/go-wayland-bindings/client/textinput"
20 "github.com/bnema/go-wayland-bindings/client/viewporter"
21 "github.com/bnema/go-wayland-bindings/client/wayland"
22 "github.com/bnema/go-wayland-bindings/client/xdgshell"
23 "github.com/bnema/wlturbo"
24 "golang.org/x/sys/unix"
25)
26
27func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
28 t.Helper()
29 fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
30 if e != nil {
31 t.Fatal(e)
32 }
33 a := os.NewFile(uintptr(fds[0]), "a")
34 b := os.NewFile(uintptr(fds[1]), "b")
35 ca, e := net.FileConn(a)
36 a.Close()
37 if e != nil {
38 t.Fatal(e)
39 }
40 cb, e := net.FileConn(b)
41 b.Close()
42 if e != nil {
43 t.Fatal(e)
44 }
45 t.Cleanup(func() { ca.Close(); cb.Close() })
46 return ca.(*net.UnixConn), cb.(*net.UnixConn)
47}
48func msg(id uint32, op uint16, body ...uint32) []byte {
49 b := make([]byte, 8+4*len(body))
50 binary.NativeEndian.PutUint32(b, id)
51 binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
52 for i, v := range body {
53 binary.NativeEndian.PutUint32(b[8+i*4:], v)
54 }
55 return b
56}
57func payload(id uint32, op uint16, b ...byte) []byte {
58 p := make([]byte, 8+len(b))
59 copy(p, msg(id, op))
60 copy(p[8:], b)
61 binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
62 return p
63}
64func arr(b []byte) []byte {
65 out := make([]byte, 4+(len(b)+3)&^3)
66 binary.NativeEndian.PutUint32(out, uint32(len(b)))
67 copy(out[4:], b)
68 return out
69}
70func str(s string) []byte { return arr(append([]byte(s), 0)) }
71func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) {
72 t.Helper()
73 var rights []byte
74 if len(fd) > 0 {
75 rights = unix.UnixRights(fd...)
76 }
77 if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil {
78 t.Fatal(e)
79 }
80}
81func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
82 t.Helper()
83 p.SetReadDeadline(time.Now().Add(2 * time.Second))
84 b := make([]byte, 4096)
85 n, e := p.Read(b)
86 if e != nil {
87 t.Fatal(e)
88 }
89 if n < 8 {
90 t.Fatal("short request")
91 }
92 return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n]
93}
94func noerr(t *testing.T, e error) {
95 t.Helper()
96 if e != nil {
97 t.Fatal(e)
98 }
99}
100func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, announced uint32, supported uint32, proxy wlturbo.Proxy) {
101 t.Helper()
102 // A fake server must advertise a global before clients can negotiate it.
103 announcement := append(msg(0, 0, name)[8:], str(iface)...)
104 announcement = append(announcement, msg(0, 0, announced)[8:]...)
105 send(t, p, payload(d.Registry().ID(), 0, announcement...))
106 noerr(t, d.Dispatch())
107 v, err := d.Registry().BindNegotiated(iface, supported, proxy)
108 noerr(t, err)
109 want := min(announced, supported)
110 _, op, b := request(t, p)
111 if op != 0 || v != want || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != want || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
112 t.Fatalf("bad bind %s announced=%d supported=%d negotiated=%d wire=%x", iface, announced, supported, v, b)
113 }
114}
115
116// serverID is the first server-allocated object ID on the wire.
117const serverID = 0xff000000
118
119// wordBytes encodes 32-bit words as a message body in native byte order.
120func wordBytes(v ...uint32) []byte { return msg(0, 0, v...)[8:] }
121
122// cat concatenates message body fragments.
123func cat(parts ...[]byte) []byte {
124 var out []byte
125 for _, p := range parts {
126 out = append(out, p...)
127 }
128 return out
129}
130
131// decodeWords splits a message body into 32-bit words.
132func decodeWords(b []byte) []uint32 {
133 w := make([]uint32, len(b)/4)
134 for i := range w {
135 w[i] = binary.NativeEndian.Uint32(b[i*4:])
136 }
137 return w
138}
139
140// wantWords requires a request body to be exactly the given 32-bit words.
141func wantWords(t *testing.T, what string, b []byte, want ...uint32) {
142 t.Helper()
143 if got := decodeWords(b); !slices.Equal(got, want) { // nil and empty are equal
144 t.Fatalf("%s wire = %v, want %v", what, got, want)
145 }
146}
147
148// wantRequestWords reads one request and requires its exact object, opcode and
149// 32-bit word arguments.
150func wantRequestWords(t *testing.T, p *net.UnixConn, id uint32, op uint16, want ...uint32) {
151 t.Helper()
152 gid, gop, body := request(t, p)
153 if gid != id || gop != op || len(body) != 4*len(want) {
154 t.Fatalf("request object=%d op=%d body=%x, want object=%d op=%d words=%v", gid, gop, body, id, op, want)
155 }
156 wantWords(t, "request", body, want...)
157}
158
159// requireFDClosed fails if fd is still open in this process: the binding must
160// close its copy of a descriptor once the kernel took it with a request. Call
161// it before reading the request, because the received duplicate may reuse the
162// same descriptor number.
163func requireFDClosed(t *testing.T, fd int) {
164 t.Helper()
165 if _, e := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); e == nil {
166 t.Fatalf("descriptor %d sent by the client was not closed", fd)
167 }
168}
169
170// requireNoWrite fails unless the client wrote nothing within a short deadline.
171func requireNoWrite(t *testing.T, p *net.UnixConn) {
172 t.Helper()
173 p.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
174 var b [64]byte
175 n, e := p.Read(b[:])
176 if e == nil {
177 t.Fatalf("unexpected bytes on the wire: %x", b[:n])
178 }
179 if !errors.Is(e, os.ErrDeadlineExceeded) {
180 t.Fatal(e)
181 }
182}
183
184// recvRequestFDs reads one client message and the descriptors that came with
185// it. Every received descriptor is closed before a fatal failure, so a failed
186// check cannot leak them; on success the caller owns fds.
187func recvRequestFDs(t *testing.T, p *net.UnixConn) (id uint32, op uint16, body []byte, fds []int) {
188 t.Helper()
189 fail := func(format string, args ...any) {
190 t.Helper()
191 for _, fd := range fds {
192 unix.Close(fd)
193 }
194 t.Fatalf(format, args...)
195 }
196 p.SetReadDeadline(time.Now().Add(2 * time.Second))
197 b := make([]byte, 4096)
198 oob := make([]byte, 256)
199 n, oobn, _, _, e := p.ReadMsgUnix(b, oob)
200 if oobn > 0 {
201 msgs, pe := unix.ParseSocketControlMessage(oob[:oobn])
202 for i := range msgs {
203 r, re := unix.ParseUnixRights(&msgs[i])
204 fds = append(fds, r...)
205 if re != nil && pe == nil {
206 pe = re
207 }
208 }
209 if pe != nil {
210 fail("control message: %v", pe)
211 }
212 }
213 if e != nil {
214 fail("read request: %v", e)
215 }
216 if n < 8 {
217 fail("short request (%d bytes)", n)
218 }
219 if size := int(binary.NativeEndian.Uint32(b[4:]) >> 16); size != n {
220 fail("header size %d != %d bytes read", size, n)
221 }
222 return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n], fds
223}
224
225// requestWithFD reads the next request, requires its object and opcode, and
226// requires exactly one descriptor, which the caller then owns.
227func requestWithFD(t *testing.T, p *net.UnixConn, id uint32, op uint16) ([]byte, int) {
228 t.Helper()
229 gid, gop, body, fds := recvRequestFDs(t, p)
230 if gid != id || gop != op || len(fds) != 1 {
231 for _, fd := range fds {
232 unix.Close(fd)
233 }
234 t.Fatalf("request object=%d op=%d fds=%d, want object=%d op=%d with 1 descriptor (body %x)", gid, gop, len(fds), id, op, body)
235 }
236 return body, fds[0]
237}
238
239// register gives a proxy a client-allocated ID and registers it with d.
240func register(d *wlturbo.Display, o wlturbo.Proxy) {
241 o.SetID(d.AllocateID())
242 d.Context().Register(o)
243}
244
245// wireEnv is one client connection against a fake compositor on a socketpair.
246type wireEnv struct {
247 t *testing.T
248 d *wlturbo.Display
249 p *net.UnixConn
250}
251
252func newWireEnv(t *testing.T) *wireEnv {
253 t.Helper()
254 c, p := pair(t)
255 d, e := wlturbo.ConnectFromConn(c)
256 noerr(t, e)
257 t.Cleanup(func() { d.Close() })
258 request(t, p) // wl_display.get_registry
259 return &wireEnv{t: t, d: d, p: p}
260}
261
262func (s *wireEnv) dispatch() { s.t.Helper(); noerr(s.t, s.d.Dispatch()) }
263
264// bind advertises and binds a global; see the package-level bind.
265func (s *wireEnv) bind(name uint32, iface string, announced, supported uint32, proxy wlturbo.Proxy) {
266 s.t.Helper()
267 bind(s.t, s.d, s.p, name, iface, announced, supported, proxy)
268}
269
270// event sends one server event and dispatches it on the client.
271func (s *wireEnv) event(id uint32, op uint16, body ...byte) {
272 s.t.Helper()
273 s.eventFD(id, op, body)
274}
275
276// eventFD is event for an event that carries descriptors (SCM_RIGHTS).
277func (s *wireEnv) eventFD(id uint32, op uint16, body []byte, fd ...int) {
278 s.t.Helper()
279 send(s.t, s.p, payload(id, op, body...), fd...)
280 s.dispatch()
281}
282
283// req reads the next client request and checks its object and opcode.
284func (s *wireEnv) req(id uint32, op uint16) []byte {
285 s.t.Helper()
286 gid, gop, b := request(s.t, s.p)
287 if gid != id || gop != op {
288 s.t.Fatalf("request object=%d op=%d, want object=%d op=%d (body %x)", gid, gop, id, op, b)
289 }
290 return b
291}
292
293// reqFD is req for a request that carries exactly one descriptor.
294func (s *wireEnv) reqFD(id uint32, op uint16) ([]byte, int) {
295 s.t.Helper()
296 return requestWithFD(s.t, s.p, id, op)
297}
298
299// noRequest fails if the client wrote anything.
300func (s *wireEnv) noRequest() { s.t.Helper(); requireNoWrite(s.t, s.p) }
301
302func (s *wireEnv) surface() *wayland.Surface {
303 o := wayland.NewSurface(s.d.Context())
304 register(s.d, o)
305 return o
306}
307func (s *wireEnv) seat() *wayland.Seat {
308 o := wayland.NewSeat(s.d.Context())
309 register(s.d, o)
310 return o
311}
312func (s *wireEnv) output() *wayland.Output {
313 o := wayland.NewOutput(s.d.Context())
314 register(s.d, o)
315 return o
316}
317func (s *wireEnv) pointer() *wayland.Pointer {
318 o := wayland.NewPointer(s.d.Context())
319 register(s.d, o)
320 return o
321}
322func (s *wireEnv) region() *wayland.Region {
323 o := wayland.NewRegion(s.d.Context())
324 register(s.d, o)
325 return o
326}
327func (s *wireEnv) buffer() *wayland.Buffer {
328 o := wayland.NewBuffer(s.d.Context())
329 register(s.d, o)
330 return o
331}
332
333func TestExtensionsOverSocketpair(t *testing.T) {
334 c, p := pair(t)
335 d, e := wlturbo.ConnectFromConn(c)
336 noerr(t, e)
337 defer d.Close()
338 request(t, p)
339 // A registry announcement is the only input to optional-global discovery.
340 send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...))
341 noerr(t, d.Dispatch())
342 if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 {
343 t.Fatal("announced fractional scale missing", g, ok)
344 }
345 if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
346 t.Fatal("absent text input")
347 }
348 // Remove the discovery-only announcement before the bind matrix assigns
349 // its own global name to this interface.
350 send(t, p, msg(d.Registry().ID(), 1, 11))
351 noerr(t, d.Dispatch())
352 surface := wayland.NewSurface(d.Context())
353 surface.SetID(d.AllocateID())
354 d.Context().Register(surface)
355 seat := wayland.NewSeat(d.Context())
356 seat.SetID(d.AllocateID())
357 d.Context().Register(seat)
358 wm := xdgshell.NewXdgWmBase(d.Context())
359 bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 6, 7, wm)
360 xs, e := wm.GetXdgSurface(surface)
361 noerr(t, e)
362 request(t, p)
363 top, e := xs.GetToplevel()
364 noerr(t, e)
365 request(t, p)
366 var sequence []string
367 top.OnConfigure(func(w, h int32, states []byte) {
368 if w != 800 || h != 600 {
369 t.Fatalf("size %d,%d", w, h)
370 }
371 sequence = append(sequence, "top")
372 })
373 xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) })
374 send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...))
375 noerr(t, d.Dispatch())
376 send(t, p, msg(xs.ID(), 0, 9))
377 noerr(t, d.Dispatch())
378 _, op, b := request(t, p)
379 if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" {
380 t.Fatalf("xdg %d %x %v", op, b, sequence)
381 }
382 noerr(t, top.Destroy())
383 request(t, p)
384 noerr(t, xs.Destroy())
385 request(t, p)
386 noerr(t, wm.Destroy())
387 request(t, p)
388 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
389 bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, 4, dm)
390 fb, e := dm.GetDefaultFeedback()
391 noerr(t, e)
392 request(t, p)
393 table, e := os.CreateTemp(t.TempDir(), "table")
394 noerr(t, e)
395 var bytes [32]byte
396 binary.NativeEndian.PutUint32(bytes[:], 0x34325258)
397 binary.NativeEndian.PutUint64(bytes[8:], 19)
398 binary.NativeEndian.PutUint32(bytes[16:], 0x34325241)
399 binary.NativeEndian.PutUint64(bytes[24:], 42)
400 _, e = table.Write(bytes[:])
401 noerr(t, e)
402 defer table.Close()
403 var entries []linuxdmabuf.FormatEntry
404 var indices []uint16
405 var feedback []string
406 fb.OnMainDevice(func(dev []byte) {
407 if len(dev) != 8 {
408 t.Fatalf("device %x", dev)
409 }
410 feedback = append(feedback, "main")
411 })
412 fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
413 entries, e = linuxdmabuf.ReadFormatTable(fd, size)
414 noerr(t, e)
415 feedback = append(feedback, "table")
416 })
417 fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") })
418 fb.OnTrancheFormats(func(data []byte) {
419 indices, e = linuxdmabuf.TrancheIndices(data, len(entries))
420 noerr(t, e)
421 feedback = append(feedback, "formats")
422 })
423 fb.OnTrancheFlags(func(flags uint32) {
424 if flags != 1 {
425 t.Fatal(flags)
426 }
427 feedback = append(feedback, "flags")
428 })
429 fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") })
430 fb.OnDone(func() { feedback = append(feedback, "done") })
431 send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...))
432 noerr(t, d.Dispatch())
433 send(t, p, msg(fb.ID(), 1, 32), int(table.Fd()))
434 noerr(t, d.Dispatch())
435 send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...))
436 noerr(t, d.Dispatch())
437 send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...))
438 noerr(t, d.Dispatch())
439 send(t, p, msg(fb.ID(), 6, 1))
440 noerr(t, d.Dispatch())
441 send(t, p, msg(fb.ID(), 3))
442 noerr(t, d.Dispatch())
443 send(t, p, msg(fb.ID(), 0))
444 noerr(t, d.Dispatch())
445 if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" {
446 t.Fatalf("feedback %v %v %v", entries, indices, feedback)
447 }
448 noerr(t, fb.Destroy())
449 request(t, p)
450 params, e := dm.CreateParams()
451 noerr(t, e)
452 request(t, p)
453 f, e := os.Open("/dev/null")
454 noerr(t, e)
455 noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0))
456 request(t, p)
457 if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
458 t.Fatal("sent fd not closed")
459 }
460 f.Close()
461 var buffer *wayland.Buffer
462 params.OnCreated(func(b *wayland.Buffer) { buffer = b })
463 params.OnFailed(func() { sequence = append(sequence, "failed") })
464 noerr(t, params.Create(1, 1, 0x34325258, 0))
465 request(t, p)
466 send(t, p, msg(params.ID(), 0, 999))
467 noerr(t, d.Dispatch())
468 if buffer == nil || buffer.ID() != 999 {
469 t.Fatal("typed buffer missing")
470 }
471 noerr(t, buffer.Destroy())
472 request(t, p)
473 noerr(t, params.Destroy())
474 request(t, p)
475 failedParams, e := dm.CreateParams()
476 noerr(t, e)
477 request(t, p)
478 failedParams.OnFailed(func() { sequence = append(sequence, "failed") })
479 noerr(t, failedParams.Create(1, 1, 0x34325258, 0))
480 request(t, p)
481 send(t, p, msg(failedParams.ID(), 1))
482 noerr(t, d.Dispatch())
483 if sequence[len(sequence)-1] != "failed" {
484 t.Fatal(sequence)
485 }
486 noerr(t, failedParams.Destroy())
487 request(t, p)
488 immedParams, e := dm.CreateParams()
489 noerr(t, e)
490 request(t, p)
491 immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0)
492 noerr(t, e)
493 request(t, p)
494 noerr(t, immediate.Destroy())
495 request(t, p)
496 noerr(t, immedParams.Destroy())
497 request(t, p)
498 noerr(t, dm.Destroy())
499 request(t, p)
500 sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
501 bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, sync)
502 ss, e := sync.GetSurface(surface)
503 noerr(t, e)
504 request(t, p)
505 f, e = os.Open("/dev/null")
506 noerr(t, e)
507 tl, e := sync.ImportTimeline(int(f.Fd()))
508 noerr(t, e)
509 request(t, p)
510 if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
511 t.Fatal("timeline fd not closed")
512 }
513 f.Close()
514 noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456))
515 _, op, b = request(t, p)
516 if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 {
517 t.Fatalf("acquire %d %x", op, b)
518 }
519 noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc))
520 _, op, b = request(t, p)
521 if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 {
522 t.Fatalf("release %d %x", op, b)
523 }
524 noerr(t, ss.Destroy())
525 request(t, p)
526 noerr(t, tl.Destroy())
527 request(t, p)
528 noerr(t, sync.Destroy())
529 request(t, p)
530 vp := viewporter.NewWpViewporter(d.Context())
531 bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, 1, vp)
532 view, e := vp.GetViewport(surface)
533 noerr(t, e)
534 request(t, p)
535 noerr(t, view.Destroy())
536 request(t, p)
537 noerr(t, vp.Destroy())
538 request(t, p)
539 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
540 bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, 1, fs)
541 scale, e := fs.GetFractionalScale(surface)
542 noerr(t, e)
543 request(t, p)
544 var preferred uint32
545 scale.OnPreferredScale(func(v uint32) { preferred = v })
546 send(t, p, msg(scale.ID(), 0, 150))
547 noerr(t, d.Dispatch())
548 if preferred != 150 {
549 t.Fatal(preferred)
550 }
551 noerr(t, scale.Destroy())
552 request(t, p)
553 noerr(t, fs.Destroy())
554 request(t, p)
555 ti := textinput.NewTextInputManagerV3(d.Context())
556 bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 2, 1, ti)
557 input, e := ti.GetTextInput(seat)
558 noerr(t, e)
559 request(t, p)
560 noerr(t, input.Enable())
561 request(t, p)
562 noerr(t, input.SetSurroundingText("hello", 5, 5))
563 request(t, p)
564 noerr(t, input.Commit())
565 request(t, p)
566 var events []string
567 input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) })
568 input.OnCommitString(func(s string) { events = append(events, "commit:"+s) })
569 input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") })
570 input.OnDone(func(uint32) { events = append(events, "done") })
571 send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...))
572 noerr(t, d.Dispatch())
573 send(t, p, payload(input.ID(), 3, str("hi")...))
574 noerr(t, d.Dispatch())
575 send(t, p, msg(input.ID(), 4, 1, 2))
576 noerr(t, d.Dispatch())
577 send(t, p, msg(input.ID(), 5, 1))
578 noerr(t, d.Dispatch())
579 if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" {
580 t.Fatal(events)
581 }
582 noerr(t, input.Disable())
583 request(t, p)
584 noerr(t, input.Commit())
585 request(t, p)
586 noerr(t, input.Destroy())
587 request(t, p)
588 noerr(t, ti.Destroy())
589 request(t, p)
590 // Core data-device remains in client/wayland; verify its typed child and
591 // both directions of clipboard descriptor ownership.
592 mgr := wayland.NewDataDeviceManager(d.Context())
593 bind(t, d, p, 7, wayland.DataDeviceManagerInterface, 3, 4, mgr)
594 device, e := mgr.GetDataDevice(seat)
595 noerr(t, e)
596 request(t, p)
597 source, e := mgr.CreateDataSource()
598 noerr(t, e)
599 request(t, p)
600 var offer *wayland.DataOffer
601 device.OnDataOffer(func(o *wayland.DataOffer) { offer = o })
602 send(t, p, msg(device.ID(), 0, 998))
603 noerr(t, d.Dispatch())
604 if offer == nil || offer.ID() != 998 {
605 t.Fatal("offer not typed")
606 }
607 read, write, e := os.Pipe()
608 noerr(t, e)
609 defer read.Close()
610 noerr(t, offer.Receive("text/plain", int(write.Fd())))
611 request(t, p)
612 if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil {
613 t.Fatal("clipboard sent fd retained")
614 }
615 write.Close()
616 source.OnSend(func(mime string, fd *wlturbo.OwnedFD) {
617 if mime != "text/plain" {
618 t.Fatal(mime)
619 }
620 n, e := fd.Take()
621 noerr(t, e)
622 unix.Close(n)
623 })
624 received, e := os.Open("/dev/null")
625 noerr(t, e)
626 send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd()))
627 noerr(t, d.Dispatch())
628 received.Close()
629 noerr(t, offer.Destroy())
630 request(t, p)
631 noerr(t, source.Destroy())
632 request(t, p)
633 noerr(t, device.Release())
634 request(t, p)
635 // release is a version 4 request; the manager was negotiated at 3, so it
636 // must be refused locally instead of reaching the compositor.
637 if err := mgr.Release(); !errors.Is(err, wlturbo.ErrVersionTooLow) {
638 t.Fatalf("release at v3 = %v, want ErrVersionTooLow", err)
639 }
640 // Cursor shape: get a device for a wl_pointer and set a shape at an
641 // enter serial; check the exact wire arguments.
642 pointer := wayland.NewPointer(d.Context())
643 pointer.SetID(d.AllocateID())
644 d.Context().Register(pointer)
645 shapes := cursorshape.NewWpCursorShapeManager(d.Context())
646 bind(t, d, p, 8, cursorshape.WpCursorShapeManagerInterface, 2, 1, shapes)
647 shape, e := shapes.GetPointer(pointer)
648 noerr(t, e)
649 if id, op, b := request(t, p); id != shapes.ID() || op != 1 || binary.NativeEndian.Uint32(b) != shape.ID() || binary.NativeEndian.Uint32(b[4:]) != pointer.ID() {
650 t.Fatalf("get_pointer wire id=%d op=%d %x", id, op, b)
651 }
652 noerr(t, shape.SetShape(42, cursorshape.SHAPE_POINTER))
653 if id, op, b := request(t, p); id != shape.ID() || op != 1 || binary.NativeEndian.Uint32(b) != 42 || binary.NativeEndian.Uint32(b[4:]) != 4 {
654 t.Fatalf("set_shape wire id=%d op=%d %x", id, op, b)
655 }
656 noerr(t, shape.Destroy())
657 request(t, p)
658 noerr(t, shapes.Destroy())
659 request(t, p)
660
661}
662
663func TestLiveFeedback(t *testing.T) {
664 if os.Getenv("WLTURBO_LIVE") != "1" {
665 t.Skip("set WLTURBO_LIVE=1")
666 }
667 d, e := wlturbo.Connect("")
668 noerr(t, e)
669 defer d.Close()
670 noerr(t, d.Roundtrip())
671 for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface, cursorshape.WpCursorShapeManagerInterface} {
672 g, ok := d.Registry().FindGlobal(iface)
673 if !ok {
674 t.Logf("%s absent", iface)
675 continue
676 }
677 v := g.Version
678 cap := uint32(1)
679 if iface == xdgshell.XdgWmBaseInterface {
680 cap = 7
681 }
682 if iface == linuxdmabuf.LinuxDmabufInterface {
683 cap = 4
684 }
685 if v > cap {
686 v = cap
687 }
688 t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version)
689 if iface != linuxdmabuf.LinuxDmabufInterface {
690 continue
691 }
692 if v < 4 {
693 t.Fatal("dmabuf feedback requires v4")
694 }
695 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
696 negotiated, err := d.Registry().BindNegotiated(iface, 4, dm)
697 noerr(t, err)
698 if negotiated != v {
699 t.Fatalf("dmabuf negotiated=%d want=%d", negotiated, v)
700 }
701 fb, e := dm.GetDefaultFeedback()
702 noerr(t, e)
703 done := false
704 fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) })
705 fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
706 entries, err := linuxdmabuf.ReadFormatTable(fd, size)
707 if err != nil {
708 t.Errorf("table: %v", err)
709 }
710 t.Logf("format-table entries=%d", len(entries))
711 })
712 fb.OnDone(func() { done = true })
713 for i := 0; i < 100 && !done; i++ {
714 noerr(t, d.Dispatch())
715 }
716 if !done {
717 t.Fatal("no feedback done")
718 }
719 noerr(t, fb.Destroy())
720 noerr(t, dm.Destroy())
721 }
722}