1//go:build linux
2
3package protocol_test
4
5import (
6 "encoding/binary"
7 "errors"
8 "net"
9 "os"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/bnema/wlturbo"
15 "github.com/bnema/wlturbo/protocol/core"
16 "github.com/bnema/wlturbo/protocol/cursorshape"
17 "github.com/bnema/wlturbo/protocol/drmsyncobj"
18 "github.com/bnema/wlturbo/protocol/fractionalscale"
19 "github.com/bnema/wlturbo/protocol/linuxdmabuf"
20 "github.com/bnema/wlturbo/protocol/textinput"
21 "github.com/bnema/wlturbo/protocol/viewporter"
22 "github.com/bnema/wlturbo/protocol/xdgshell"
23 "golang.org/x/sys/unix"
24)
25
26func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
27 t.Helper()
28 fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
29 if e != nil {
30 t.Fatal(e)
31 }
32 a := os.NewFile(uintptr(fds[0]), "a")
33 b := os.NewFile(uintptr(fds[1]), "b")
34 ca, e := net.FileConn(a)
35 a.Close()
36 if e != nil {
37 t.Fatal(e)
38 }
39 cb, e := net.FileConn(b)
40 b.Close()
41 if e != nil {
42 t.Fatal(e)
43 }
44 t.Cleanup(func() { ca.Close(); cb.Close() })
45 return ca.(*net.UnixConn), cb.(*net.UnixConn)
46}
47func msg(id uint32, op uint16, body ...uint32) []byte {
48 b := make([]byte, 8+4*len(body))
49 binary.NativeEndian.PutUint32(b, id)
50 binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
51 for i, v := range body {
52 binary.NativeEndian.PutUint32(b[8+i*4:], v)
53 }
54 return b
55}
56func payload(id uint32, op uint16, b ...byte) []byte {
57 p := make([]byte, 8+len(b))
58 copy(p, msg(id, op))
59 copy(p[8:], b)
60 binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
61 return p
62}
63func arr(b []byte) []byte {
64 out := make([]byte, 4+(len(b)+3)&^3)
65 binary.NativeEndian.PutUint32(out, uint32(len(b)))
66 copy(out[4:], b)
67 return out
68}
69func str(s string) []byte { return arr(append([]byte(s), 0)) }
70func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) {
71 t.Helper()
72 var rights []byte
73 if len(fd) > 0 {
74 rights = unix.UnixRights(fd...)
75 }
76 if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil {
77 t.Fatal(e)
78 }
79}
80func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
81 t.Helper()
82 p.SetReadDeadline(time.Now().Add(2 * time.Second))
83 b := make([]byte, 4096)
84 n, e := p.Read(b)
85 if e != nil {
86 t.Fatal(e)
87 }
88 if n < 8 {
89 t.Fatal("short request")
90 }
91 return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n]
92}
93func noerr(t *testing.T, e error) {
94 t.Helper()
95 if e != nil {
96 t.Fatal(e)
97 }
98}
99func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, announced uint32, supported uint32, proxy wlturbo.Proxy) {
100 t.Helper()
101 // A fake server must advertise a global before clients can negotiate it.
102 announcement := append(msg(0, 0, name)[8:], str(iface)...)
103 announcement = append(announcement, msg(0, 0, announced)[8:]...)
104 send(t, p, payload(d.Registry().ID(), 0, announcement...))
105 noerr(t, d.Dispatch())
106 v, err := d.Registry().BindNegotiated(iface, supported, proxy)
107 noerr(t, err)
108 want := min(announced, supported)
109 _, op, b := request(t, p)
110 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() {
111 t.Fatalf("bad bind %s announced=%d supported=%d negotiated=%d wire=%x", iface, announced, supported, v, b)
112 }
113}
114func TestExtensionsOverSocketpair(t *testing.T) {
115 c, p := pair(t)
116 d, e := wlturbo.ConnectFromConn(c)
117 noerr(t, e)
118 defer d.Close()
119 request(t, p)
120 // A registry announcement is the only input to optional-global discovery.
121 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]...)...)...))
122 noerr(t, d.Dispatch())
123 if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 {
124 t.Fatal("announced fractional scale missing", g, ok)
125 }
126 if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
127 t.Fatal("absent text input")
128 }
129 // Remove the discovery-only announcement before the bind matrix assigns
130 // its own global name to this interface.
131 send(t, p, msg(d.Registry().ID(), 1, 11))
132 noerr(t, d.Dispatch())
133 surface := core.NewSurface(d.Context())
134 surface.SetID(d.AllocateID())
135 d.Context().Register(surface)
136 seat := core.NewSeat(d.Context())
137 seat.SetID(d.AllocateID())
138 d.Context().Register(seat)
139 wm := xdgshell.NewXdgWmBase(d.Context())
140 bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 6, 7, wm)
141 xs, e := wm.GetXdgSurface(surface)
142 noerr(t, e)
143 request(t, p)
144 top, e := xs.GetToplevel()
145 noerr(t, e)
146 request(t, p)
147 var sequence []string
148 top.OnConfigure(func(w, h int32, states []byte) {
149 if w != 800 || h != 600 {
150 t.Fatalf("size %d,%d", w, h)
151 }
152 sequence = append(sequence, "top")
153 })
154 xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) })
155 send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...))
156 noerr(t, d.Dispatch())
157 send(t, p, msg(xs.ID(), 0, 9))
158 noerr(t, d.Dispatch())
159 _, op, b := request(t, p)
160 if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" {
161 t.Fatalf("xdg %d %x %v", op, b, sequence)
162 }
163 noerr(t, top.Destroy())
164 request(t, p)
165 noerr(t, xs.Destroy())
166 request(t, p)
167 noerr(t, wm.Destroy())
168 request(t, p)
169 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
170 bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, 4, dm)
171 fb, e := dm.GetDefaultFeedback()
172 noerr(t, e)
173 request(t, p)
174 table, e := os.CreateTemp(t.TempDir(), "table")
175 noerr(t, e)
176 var bytes [32]byte
177 binary.NativeEndian.PutUint32(bytes[:], 0x34325258)
178 binary.NativeEndian.PutUint64(bytes[8:], 19)
179 binary.NativeEndian.PutUint32(bytes[16:], 0x34325241)
180 binary.NativeEndian.PutUint64(bytes[24:], 42)
181 _, e = table.Write(bytes[:])
182 noerr(t, e)
183 defer table.Close()
184 var entries []linuxdmabuf.FormatEntry
185 var indices []uint16
186 var feedback []string
187 fb.OnMainDevice(func(dev []byte) {
188 if len(dev) != 8 {
189 t.Fatalf("device %x", dev)
190 }
191 feedback = append(feedback, "main")
192 })
193 fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
194 entries, e = linuxdmabuf.ReadFormatTable(fd, size)
195 noerr(t, e)
196 feedback = append(feedback, "table")
197 })
198 fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") })
199 fb.OnTrancheFormats(func(data []byte) {
200 indices, e = linuxdmabuf.TrancheIndices(data, len(entries))
201 noerr(t, e)
202 feedback = append(feedback, "formats")
203 })
204 fb.OnTrancheFlags(func(flags uint32) {
205 if flags != 1 {
206 t.Fatal(flags)
207 }
208 feedback = append(feedback, "flags")
209 })
210 fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") })
211 fb.OnDone(func() { feedback = append(feedback, "done") })
212 send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...))
213 noerr(t, d.Dispatch())
214 send(t, p, msg(fb.ID(), 1, 32), int(table.Fd()))
215 noerr(t, d.Dispatch())
216 send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...))
217 noerr(t, d.Dispatch())
218 send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...))
219 noerr(t, d.Dispatch())
220 send(t, p, msg(fb.ID(), 6, 1))
221 noerr(t, d.Dispatch())
222 send(t, p, msg(fb.ID(), 3))
223 noerr(t, d.Dispatch())
224 send(t, p, msg(fb.ID(), 0))
225 noerr(t, d.Dispatch())
226 if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" {
227 t.Fatalf("feedback %v %v %v", entries, indices, feedback)
228 }
229 noerr(t, fb.Destroy())
230 request(t, p)
231 params, e := dm.CreateParams()
232 noerr(t, e)
233 request(t, p)
234 f, e := os.Open("/dev/null")
235 noerr(t, e)
236 noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0))
237 request(t, p)
238 if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
239 t.Fatal("sent fd not closed")
240 }
241 f.Close()
242 var buffer *core.Buffer
243 params.OnCreated(func(b *core.Buffer) { buffer = b })
244 params.OnFailed(func() { sequence = append(sequence, "failed") })
245 noerr(t, params.Create(1, 1, 0x34325258, 0))
246 request(t, p)
247 send(t, p, msg(params.ID(), 0, 999))
248 noerr(t, d.Dispatch())
249 if buffer == nil || buffer.ID() != 999 {
250 t.Fatal("typed buffer missing")
251 }
252 noerr(t, buffer.Destroy())
253 request(t, p)
254 noerr(t, params.Destroy())
255 request(t, p)
256 failedParams, e := dm.CreateParams()
257 noerr(t, e)
258 request(t, p)
259 failedParams.OnFailed(func() { sequence = append(sequence, "failed") })
260 noerr(t, failedParams.Create(1, 1, 0x34325258, 0))
261 request(t, p)
262 send(t, p, msg(failedParams.ID(), 1))
263 noerr(t, d.Dispatch())
264 if sequence[len(sequence)-1] != "failed" {
265 t.Fatal(sequence)
266 }
267 noerr(t, failedParams.Destroy())
268 request(t, p)
269 immedParams, e := dm.CreateParams()
270 noerr(t, e)
271 request(t, p)
272 immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0)
273 noerr(t, e)
274 request(t, p)
275 noerr(t, immediate.Destroy())
276 request(t, p)
277 noerr(t, immedParams.Destroy())
278 request(t, p)
279 noerr(t, dm.Destroy())
280 request(t, p)
281 sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
282 bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, sync)
283 ss, e := sync.GetSurface(surface)
284 noerr(t, e)
285 request(t, p)
286 f, e = os.Open("/dev/null")
287 noerr(t, e)
288 tl, e := sync.ImportTimeline(int(f.Fd()))
289 noerr(t, e)
290 request(t, p)
291 if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
292 t.Fatal("timeline fd not closed")
293 }
294 f.Close()
295 noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456))
296 _, op, b = request(t, p)
297 if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 {
298 t.Fatalf("acquire %d %x", op, b)
299 }
300 noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc))
301 _, op, b = request(t, p)
302 if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 {
303 t.Fatalf("release %d %x", op, b)
304 }
305 noerr(t, ss.Destroy())
306 request(t, p)
307 noerr(t, tl.Destroy())
308 request(t, p)
309 noerr(t, sync.Destroy())
310 request(t, p)
311 vp := viewporter.NewWpViewporter(d.Context())
312 bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, 1, vp)
313 view, e := vp.GetViewport(surface)
314 noerr(t, e)
315 request(t, p)
316 noerr(t, view.Destroy())
317 request(t, p)
318 noerr(t, vp.Destroy())
319 request(t, p)
320 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
321 bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, 1, fs)
322 scale, e := fs.GetFractionalScale(surface)
323 noerr(t, e)
324 request(t, p)
325 var preferred uint32
326 scale.OnPreferredScale(func(v uint32) { preferred = v })
327 send(t, p, msg(scale.ID(), 0, 150))
328 noerr(t, d.Dispatch())
329 if preferred != 150 {
330 t.Fatal(preferred)
331 }
332 noerr(t, scale.Destroy())
333 request(t, p)
334 noerr(t, fs.Destroy())
335 request(t, p)
336 ti := textinput.NewTextInputManagerV3(d.Context())
337 bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 2, 1, ti)
338 input, e := ti.GetTextInput(seat)
339 noerr(t, e)
340 request(t, p)
341 noerr(t, input.Enable())
342 request(t, p)
343 noerr(t, input.SetSurroundingText("hello", 5, 5))
344 request(t, p)
345 noerr(t, input.Commit())
346 request(t, p)
347 var events []string
348 input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) })
349 input.OnCommitString(func(s string) { events = append(events, "commit:"+s) })
350 input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") })
351 input.OnDone(func(uint32) { events = append(events, "done") })
352 send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...))
353 noerr(t, d.Dispatch())
354 send(t, p, payload(input.ID(), 3, str("hi")...))
355 noerr(t, d.Dispatch())
356 send(t, p, msg(input.ID(), 4, 1, 2))
357 noerr(t, d.Dispatch())
358 send(t, p, msg(input.ID(), 5, 1))
359 noerr(t, d.Dispatch())
360 if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" {
361 t.Fatal(events)
362 }
363 noerr(t, input.Disable())
364 request(t, p)
365 noerr(t, input.Commit())
366 request(t, p)
367 noerr(t, input.Destroy())
368 request(t, p)
369 noerr(t, ti.Destroy())
370 request(t, p)
371 // Core data-device remains in protocol/core; verify its typed child and
372 // both directions of clipboard descriptor ownership.
373 mgr := core.NewDataDeviceManager(d.Context())
374 bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, 4, mgr)
375 device, e := mgr.GetDataDevice(seat)
376 noerr(t, e)
377 request(t, p)
378 source, e := mgr.CreateDataSource()
379 noerr(t, e)
380 request(t, p)
381 var offer *core.DataOffer
382 device.OnDataOffer(func(o *core.DataOffer) { offer = o })
383 send(t, p, msg(device.ID(), 0, 998))
384 noerr(t, d.Dispatch())
385 if offer == nil || offer.ID() != 998 {
386 t.Fatal("offer not typed")
387 }
388 read, write, e := os.Pipe()
389 noerr(t, e)
390 defer read.Close()
391 noerr(t, offer.Receive("text/plain", int(write.Fd())))
392 request(t, p)
393 if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil {
394 t.Fatal("clipboard sent fd retained")
395 }
396 write.Close()
397 source.OnSend(func(mime string, fd *wlturbo.OwnedFD) {
398 if mime != "text/plain" {
399 t.Fatal(mime)
400 }
401 n, e := fd.Take()
402 noerr(t, e)
403 unix.Close(n)
404 })
405 received, e := os.Open("/dev/null")
406 noerr(t, e)
407 send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd()))
408 noerr(t, d.Dispatch())
409 received.Close()
410 noerr(t, offer.Destroy())
411 request(t, p)
412 noerr(t, source.Destroy())
413 request(t, p)
414 noerr(t, device.Release())
415 request(t, p)
416 // release is a version 4 request; the manager was negotiated at 3, so it
417 // must be refused locally instead of reaching the compositor.
418 if err := mgr.Release(); !errors.Is(err, wlturbo.ErrVersionTooLow) {
419 t.Fatalf("release at v3 = %v, want ErrVersionTooLow", err)
420 }
421 // Cursor shape: get a device for a wl_pointer and set a shape at an
422 // enter serial; check the exact wire arguments.
423 pointer := core.NewPointer(d.Context())
424 pointer.SetID(d.AllocateID())
425 d.Context().Register(pointer)
426 shapes := cursorshape.NewWpCursorShapeManager(d.Context())
427 bind(t, d, p, 8, cursorshape.WpCursorShapeManagerInterface, 2, 1, shapes)
428 shape, e := shapes.GetPointer(pointer)
429 noerr(t, e)
430 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() {
431 t.Fatalf("get_pointer wire id=%d op=%d %x", id, op, b)
432 }
433 noerr(t, shape.SetShape(42, cursorshape.SHAPE_POINTER))
434 if id, op, b := request(t, p); id != shape.ID() || op != 1 || binary.NativeEndian.Uint32(b) != 42 || binary.NativeEndian.Uint32(b[4:]) != 4 {
435 t.Fatalf("set_shape wire id=%d op=%d %x", id, op, b)
436 }
437 noerr(t, shape.Destroy())
438 request(t, p)
439 noerr(t, shapes.Destroy())
440 request(t, p)
441
442}
443
444func TestLiveFeedback(t *testing.T) {
445 if os.Getenv("WLTURBO_LIVE") != "1" {
446 t.Skip("set WLTURBO_LIVE=1")
447 }
448 d, e := wlturbo.Connect("")
449 noerr(t, e)
450 defer d.Close()
451 noerr(t, d.Roundtrip())
452 for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface, cursorshape.WpCursorShapeManagerInterface} {
453 g, ok := d.Registry().FindGlobal(iface)
454 if !ok {
455 t.Logf("%s absent", iface)
456 continue
457 }
458 v := g.Version
459 cap := uint32(1)
460 if iface == xdgshell.XdgWmBaseInterface {
461 cap = 7
462 }
463 if iface == linuxdmabuf.LinuxDmabufInterface {
464 cap = 4
465 }
466 if v > cap {
467 v = cap
468 }
469 t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version)
470 if iface != linuxdmabuf.LinuxDmabufInterface {
471 continue
472 }
473 if v < 4 {
474 t.Fatal("dmabuf feedback requires v4")
475 }
476 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
477 negotiated, err := d.Registry().BindNegotiated(iface, 4, dm)
478 noerr(t, err)
479 if negotiated != v {
480 t.Fatalf("dmabuf negotiated=%d want=%d", negotiated, v)
481 }
482 fb, e := dm.GetDefaultFeedback()
483 noerr(t, e)
484 done := false
485 fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) })
486 fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
487 entries, err := linuxdmabuf.ReadFormatTable(fd, size)
488 if err != nil {
489 t.Errorf("table: %v", err)
490 }
491 t.Logf("format-table entries=%d", len(entries))
492 })
493 fb.OnDone(func() { done = true })
494 for i := 0; i < 100 && !done; i++ {
495 noerr(t, d.Dispatch())
496 }
497 if !done {
498 t.Fatal("no feedback done")
499 }
500 noerr(t, fb.Destroy())
501 noerr(t, dm.Destroy())
502 }
503}