input_desktop_extensions_test.go RawHistoryFolder
1//go:build linux
2
3package protocol_test
4
5import (
6 "encoding/binary"
7 "errors"
8 "os"
9 "reflect"
10 "testing"
11 "time"
12
13 "github.com/bnema/wlturbo"
14 "github.com/bnema/wlturbo/protocol/datacontrol"
15 "github.com/bnema/wlturbo/protocol/extforeigntoplevel"
16 "github.com/bnema/wlturbo/protocol/idleinhibit"
17 "github.com/bnema/wlturbo/protocol/idlenotify"
18 "github.com/bnema/wlturbo/protocol/imagecapturesource"
19 "github.com/bnema/wlturbo/protocol/imagecopycapture"
20 "github.com/bnema/wlturbo/protocol/pointerconstraints"
21 "github.com/bnema/wlturbo/protocol/primaryselection"
22 "github.com/bnema/wlturbo/protocol/relativepointer"
23 "github.com/bnema/wlturbo/protocol/shortcutsinhibit"
24 "github.com/bnema/wlturbo/protocol/workspace"
25 "github.com/bnema/wlturbo/protocol/xdgdecoration"
26 "github.com/bnema/wlturbo/protocol/xdgoutput"
27 "github.com/bnema/wlturbo/protocol/xdgshell"
28 "golang.org/x/sys/unix"
29)
30
31func TestRelativePointerMotionDecode(t *testing.T) {
32 s := newWireEnv(t)
33 ptr := s.pointer()
34 mgr := relativepointer.NewRelativePointerManager(s.d.Context())
35 s.bind(1, relativepointer.RelativePointerManagerInterface, 1, 1, mgr)
36 rp, e := mgr.GetRelativePointer(ptr)
37 noerr(t, e)
38 wantWords(t, "get_relative_pointer", s.req(mgr.ID(), 1), rp.ID(), ptr.ID())
39
40 type motion struct {
41 hi, lo uint32
42 dx, dy, ux, uy wlturbo.Fixed
43 }
44 var got []motion
45 rp.OnRelativeMotion(func(hi, lo uint32, dx, dy, ux, uy wlturbo.Fixed) {
46 got = append(got, motion{hi, lo, dx, dy, ux, uy})
47 })
48 // 64-bit microsecond timestamp split in two words; negative deltas must
49 // keep their sign through the 24.8 fixed-point decode.
50 want := motion{1, 0x89abcdef, wlturbo.NewFixed(-1.5), wlturbo.NewFixed(2.25), wlturbo.NewFixed(-3), wlturbo.NewFixed(0.5)}
51 s.event(rp.ID(), 0, wordBytes(want.hi, want.lo, uint32(want.dx), uint32(want.dy), uint32(want.ux), uint32(want.uy))...)
52 if len(got) != 1 || got[0] != want {
53 t.Fatalf("motion %+v, want %+v", got, want)
54 }
55 if got[0].dx.Float64() != -1.5 || got[0].dy.Float64() != 2.25 {
56 t.Fatalf("fixed decode %v %v", got[0].dx.Float64(), got[0].dy.Float64())
57 }
58 // Two handlers are both called, in registration order.
59 var order []int
60 rp.OnRelativeMotion(func(_, _ uint32, _, _, _, _ wlturbo.Fixed) { order = append(order, 1) })
61 rp.OnRelativeMotion(func(_, _ uint32, _, _, _, _ wlturbo.Fixed) { order = append(order, 2) })
62 s.event(rp.ID(), 0, wordBytes(0, 1, 0, 0, 0, 0)...)
63 if !reflect.DeepEqual(order, []int{1, 2}) || len(got) != 2 {
64 t.Fatalf("handlers order=%v motions=%d", order, len(got))
65 }
66 noerr(t, rp.Destroy())
67 s.req(rp.ID(), 0)
68 noerr(t, mgr.Destroy())
69 s.req(mgr.ID(), 0)
70}
71
72func TestPointerConstraintsNullableRegion(t *testing.T) {
73 s := newWireEnv(t)
74 surf, ptr := s.surface(), s.pointer()
75 pc := pointerconstraints.NewPointerConstraints(s.d.Context())
76 s.bind(1, pointerconstraints.PointerConstraintsInterface, 1, 1, pc)
77
78 // A null region means "use the surface input region" and is wire id 0.
79 lock, e := pc.LockPointer(surf, ptr, nil, pointerconstraints.LIFETIME_PERSISTENT)
80 noerr(t, e)
81 wantWords(t, "lock_pointer(nil)", s.req(pc.ID(), 1), lock.ID(), surf.ID(), ptr.ID(), 0, pointerconstraints.LIFETIME_PERSISTENT)
82
83 reg := s.region()
84 conf, e := pc.ConfinePointer(surf, ptr, reg, pointerconstraints.LIFETIME_ONESHOT)
85 noerr(t, e)
86 wantWords(t, "confine_pointer(region)", s.req(pc.ID(), 2), conf.ID(), surf.ID(), ptr.ID(), reg.ID(), pointerconstraints.LIFETIME_ONESHOT)
87
88 noerr(t, lock.SetCursorPositionHint(wlturbo.NewFixed(10.5), wlturbo.NewFixed(-2)))
89 wantWords(t, "set_cursor_position_hint", s.req(lock.ID(), 1), uint32(wlturbo.NewFixed(10.5)), uint32(wlturbo.NewFixed(-2)))
90 noerr(t, lock.SetRegion(nil))
91 wantWords(t, "locked.set_region(nil)", s.req(lock.ID(), 2), 0)
92 noerr(t, lock.SetRegion(reg))
93 wantWords(t, "locked.set_region", s.req(lock.ID(), 2), reg.ID())
94 noerr(t, conf.SetRegion(nil))
95 wantWords(t, "confined.set_region(nil)", s.req(conf.ID(), 1), 0)
96
97 var seq []string
98 lock.OnLocked(func() { seq = append(seq, "locked") })
99 lock.OnUnlocked(func() { seq = append(seq, "unlocked") })
100 conf.OnConfined(func() { seq = append(seq, "confined") })
101 conf.OnUnconfined(func() { seq = append(seq, "unconfined") })
102 s.event(lock.ID(), 0)
103 s.event(lock.ID(), 1)
104 s.event(conf.ID(), 0)
105 s.event(conf.ID(), 1)
106 if !reflect.DeepEqual(seq, []string{"locked", "unlocked", "confined", "unconfined"}) {
107 t.Fatal(seq)
108 }
109 noerr(t, lock.Destroy())
110 s.req(lock.ID(), 0)
111 noerr(t, conf.Destroy())
112 s.req(conf.ID(), 0)
113 noerr(t, pc.Destroy())
114 s.req(pc.ID(), 0)
115}
116
117// rawPipe returns a pipe as plain descriptors. An *os.File would close a reused
118// descriptor number on Close after the binding already closed the original.
119func rawPipe(t *testing.T) (*os.File, int) {
120 t.Helper()
121 var fds [2]int
122 noerr(t, unix.Pipe2(fds[:], unix.O_CLOEXEC))
123 r := os.NewFile(uintptr(fds[0]), "pipe-r")
124 t.Cleanup(func() { r.Close() })
125 return r, fds[1]
126}
127
128func TestPrimarySelectionNewIDAndFDOwnership(t *testing.T) {
129 s := newWireEnv(t)
130 seat := s.seat()
131 mgr := primaryselection.NewPrimarySelectionDeviceManager(s.d.Context())
132 s.bind(1, primaryselection.PrimarySelectionDeviceManagerInterface, 1, 1, mgr)
133 dev, e := mgr.GetDevice(seat)
134 noerr(t, e)
135 wantWords(t, "get_device", s.req(mgr.ID(), 1), dev.ID(), seat.ID())
136 src, e := mgr.CreateSource()
137 noerr(t, e)
138 wantWords(t, "create_source", s.req(mgr.ID(), 0), src.ID())
139 noerr(t, src.Offer("text/plain"))
140 s.req(src.ID(), 0)
141 noerr(t, dev.SetSelection(src, 77))
142 wantWords(t, "set_selection", s.req(dev.ID(), 0), src.ID(), 77)
143 noerr(t, dev.SetSelection(nil, 78))
144 wantWords(t, "set_selection(nil)", s.req(dev.ID(), 0), 0, 78)
145
146 // data_offer carries a server-allocated new_id which must come back as a
147 // typed, registered child inheriting the device version.
148 var offer *primaryselection.PrimarySelectionOffer
149 var mimes []string
150 dev.OnDataOffer(func(o *primaryselection.PrimarySelectionOffer) {
151 offer = o
152 o.OnOffer(func(m string) { mimes = append(mimes, m) })
153 })
154 var selected []uint32
155 dev.OnSelection(func(id uint32) { selected = append(selected, id) })
156 s.event(dev.ID(), 0, wordBytes(serverID+1)...)
157 if offer == nil || offer.ID() != serverID+1 || offer.Version() != dev.Version() {
158 t.Fatalf("offer %+v", offer)
159 }
160 s.event(offer.ID(), 0, str("text/plain")...)
161 s.event(dev.ID(), 1, wordBytes(offer.ID())...)
162 s.event(dev.ID(), 1, wordBytes(0)...) // null selection
163 if !reflect.DeepEqual(mimes, []string{"text/plain"}) || !reflect.DeepEqual(selected, []uint32{offer.ID(), 0}) {
164 t.Fatalf("mimes=%v selected=%v", mimes, selected)
165 }
166
167 // Client -> compositor: the sent descriptor is duplicated by the kernel and
168 // the client's copy is closed by the binding.
169 r, w := rawPipe(t)
170 noerr(t, offer.Receive("text/plain", w))
171 // Check before reading the request: the received duplicate may reuse the
172 // number of the descriptor the client just closed.
173 requireFDClosed(t, w)
174 body, fd := s.reqFD(offer.ID(), 0)
175 // Closed explicitly below; the guard covers early t.Fatal paths only.
176 defer func() {
177 if fd >= 0 {
178 unix.Close(fd)
179 }
180 }()
181 if len(body) < 4 {
182 t.Fatalf("receive body %x", body)
183 }
184 _, e = unix.Write(fd, []byte("payload"))
185 noerr(t, e)
186 noerr(t, unix.Close(fd))
187 fd = -1
188 buf := make([]byte, 16)
189 n, e := r.Read(buf)
190 noerr(t, e)
191 if string(buf[:n]) != "payload" {
192 t.Fatalf("payload %q", buf[:n])
193 }
194
195 // Compositor -> client: source.send hands over an owned descriptor.
196 var taken, ignored *wlturbo.OwnedFD
197 var sendMimes []string
198 src.OnSend(func(m string, f *wlturbo.OwnedFD) {
199 sendMimes = append(sendMimes, m)
200 if taken == nil {
201 taken = f
202 n, e := f.Take()
203 noerr(t, e)
204 _, e = unix.Write(n, []byte("from-client"))
205 noerr(t, e)
206 unix.Close(n)
207 return
208 }
209 ignored = f // deliberately neither taken nor closed
210 })
211 sr, sw, e := os.Pipe()
212 noerr(t, e)
213 send(t, s.p, payload(src.ID(), 0, str("text/plain")...), int(sw.Fd()))
214 s.dispatch()
215 sw.Close()
216 n, e = sr.Read(buf)
217 noerr(t, e)
218 if string(buf[:n]) != "from-client" {
219 t.Fatalf("source payload %q", buf[:n])
220 }
221 if _, e := taken.Take(); e == nil {
222 t.Fatal("second Take on a taken descriptor must fail")
223 }
224 sr.Close()
225 // An unclaimed descriptor is closed by the transport when dispatch ends,
226 // so the pipe reader sees EOF although the handler never closed it.
227 pr, pw, e := os.Pipe()
228 noerr(t, e)
229 defer pr.Close()
230 send(t, s.p, payload(src.ID(), 0, str("text/html")...), int(pw.Fd()))
231 pw.Close() // only the in-flight duplicate keeps the write end alive
232 s.dispatch()
233 if ignored == nil || len(sendMimes) != 2 {
234 t.Fatalf("send events %v", sendMimes)
235 }
236 if _, e := ignored.Take(); e == nil {
237 t.Fatal("unclaimed descriptor must be closed after dispatch, but Take succeeded")
238 }
239 pr.SetReadDeadline(time.Now().Add(2 * time.Second))
240 if n, e := pr.Read(buf); n != 0 || e == nil {
241 t.Fatalf("unclaimed descriptor still open: n=%d err=%v", n, e)
242 }
243 var cancelled bool
244 src.OnCancelled(func() { cancelled = true })
245 s.event(src.ID(), 1)
246 if !cancelled {
247 t.Fatal("cancelled not delivered")
248 }
249 noerr(t, offer.Destroy())
250 s.req(offer.ID(), 1)
251 noerr(t, src.Destroy())
252 s.req(src.ID(), 1)
253 noerr(t, dev.Destroy())
254 s.req(dev.ID(), 1)
255 noerr(t, mgr.Destroy())
256 s.req(mgr.ID(), 2)
257}
258
259func TestDataControlNewIDAndFDOwnership(t *testing.T) {
260 s := newWireEnv(t)
261 seat := s.seat()
262 mgr := datacontrol.NewExtDataControlManager(s.d.Context())
263 s.bind(1, datacontrol.ExtDataControlManagerInterface, 1, 1, mgr)
264 dev, e := mgr.GetDataDevice(seat)
265 noerr(t, e)
266 wantWords(t, "get_data_device", s.req(mgr.ID(), 1), dev.ID(), seat.ID())
267 src, e := mgr.CreateDataSource()
268 noerr(t, e)
269 wantWords(t, "create_data_source", s.req(mgr.ID(), 0), src.ID())
270 noerr(t, dev.SetSelection(src))
271 wantWords(t, "set_selection", s.req(dev.ID(), 0), src.ID())
272 noerr(t, dev.SetPrimarySelection(nil))
273 wantWords(t, "set_primary_selection(nil)", s.req(dev.ID(), 2), 0)
274
275 var offers []*datacontrol.ExtDataControlOffer
276 dev.OnDataOffer(func(o *datacontrol.ExtDataControlOffer) { offers = append(offers, o) })
277 var sel, primary []uint32
278 dev.OnSelection(func(id uint32) { sel = append(sel, id) })
279 dev.OnPrimarySelection(func(id uint32) { primary = append(primary, id) })
280 var finished bool
281 dev.OnFinished(func() { finished = true })
282 s.event(dev.ID(), 0, wordBytes(serverID+1)...)
283 s.event(dev.ID(), 0, wordBytes(serverID+2)...)
284 if len(offers) != 2 || offers[0].ID() != serverID+1 || offers[1].ID() != serverID+2 {
285 t.Fatalf("offers %+v", offers)
286 }
287 s.event(dev.ID(), 1, wordBytes(serverID+1)...)
288 s.event(dev.ID(), 3, wordBytes(serverID+2)...)
289 s.event(dev.ID(), 3, wordBytes(0)...)
290 if !reflect.DeepEqual(sel, []uint32{serverID + 1}) || !reflect.DeepEqual(primary, []uint32{serverID + 2, 0}) {
291 t.Fatalf("sel=%v primary=%v", sel, primary)
292 }
293 var mime string
294 offers[0].OnOffer(func(m string) { mime = m })
295 s.event(offers[0].ID(), 0, str("text/uri-list")...)
296 if mime != "text/uri-list" {
297 t.Fatal(mime)
298 }
299
300 r, w := rawPipe(t)
301 noerr(t, offers[0].Receive("text/uri-list", w))
302 requireFDClosed(t, w)
303 _, fd := s.reqFD(offers[0].ID(), 0)
304 unix.Close(fd)
305 buf := make([]byte, 8)
306 if n, e := r.Read(buf); n != 0 || e == nil {
307 t.Fatalf("expected EOF once both write ends closed, n=%d err=%v", n, e)
308 }
309
310 // source.send: the handler owns the descriptor and can Close it early.
311 var closedByHandler bool
312 src.OnSend(func(m string, f *wlturbo.OwnedFD) {
313 noerr(t, f.Close())
314 noerr(t, f.Close()) // idempotent
315 if _, e := f.Take(); e == nil {
316 t.Error("Take after Close must fail")
317 }
318 closedByHandler = true
319 })
320 pr, pw, e := os.Pipe()
321 noerr(t, e)
322 defer pr.Close()
323 send(t, s.p, payload(src.ID(), 0, str("text/plain")...), int(pw.Fd()))
324 pw.Close()
325 s.dispatch()
326 pr.SetReadDeadline(time.Now().Add(2 * time.Second))
327 if n, e := pr.Read(buf); !closedByHandler || n != 0 || e == nil {
328 t.Fatalf("closed=%v n=%d err=%v", closedByHandler, n, e)
329 }
330 s.event(dev.ID(), 2)
331 if !finished {
332 t.Fatal("finished not delivered")
333 }
334 noerr(t, src.Offer("text/plain"))
335 s.req(src.ID(), 0)
336 noerr(t, offers[0].Destroy())
337 s.req(offers[0].ID(), 1)
338 noerr(t, src.Destroy())
339 s.req(src.ID(), 1)
340 noerr(t, dev.Destroy())
341 s.req(dev.ID(), 1)
342 noerr(t, mgr.Destroy())
343 s.req(mgr.ID(), 2)
344}
345
346func TestWorkspaceLifecycle(t *testing.T) {
347 s := newWireEnv(t)
348 out := s.output()
349 mgr := workspace.NewExtWorkspaceManager(s.d.Context())
350 s.bind(1, workspace.ExtWorkspaceManagerInterface, 1, 1, mgr)
351
352 var groups []*workspace.ExtWorkspaceGroupHandle
353 var spaces []*workspace.ExtWorkspaceHandle
354 var log []string
355 mgr.OnWorkspaceGroup(func(g *workspace.ExtWorkspaceGroupHandle) { groups = append(groups, g) })
356 mgr.OnWorkspace(func(w *workspace.ExtWorkspaceHandle) { spaces = append(spaces, w) })
357 mgr.OnDone(func() { log = append(log, "done") })
358 mgr.OnFinished(func() { log = append(log, "finished") })
359
360 s.event(mgr.ID(), 0, wordBytes(serverID+1)...)
361 s.event(mgr.ID(), 1, wordBytes(serverID+2)...)
362 if len(groups) != 1 || len(spaces) != 1 || groups[0].ID() != serverID+1 || spaces[0].ID() != serverID+2 {
363 t.Fatal("typed children missing")
364 }
365 g, w := groups[0], spaces[0]
366 var gcaps, wcaps, state uint32
367 var outs, entered []uint32
368 var id, name string
369 var coords []byte
370 g.OnCapabilities(func(c uint32) { gcaps = c })
371 g.OnOutputEnter(func(o uint32) { outs = append(outs, o) })
372 g.OnWorkspaceEnter(func(x uint32) { entered = append(entered, x) })
373 g.OnRemoved(func() { log = append(log, "group-removed") })
374 w.OnId(func(v string) { id = v })
375 w.OnName(func(v string) { name = v })
376 w.OnCoordinates(func(c []byte) { coords = c })
377 w.OnState(func(v uint32) { state = v })
378 w.OnCapabilities(func(v uint32) { wcaps = v })
379 w.OnRemoved(func() { log = append(log, "workspace-removed") })
380
381 s.event(g.ID(), 0, wordBytes(workspace.GROUP_CAPABILITIES_CREATE_WORKSPACE)...)
382 s.event(g.ID(), 1, wordBytes(out.ID())...)
383 s.event(g.ID(), 3, wordBytes(w.ID())...)
384 s.event(w.ID(), 0, str("ws-1")...)
385 s.event(w.ID(), 1, str("Workspace é")...)
386 s.event(w.ID(), 2, arr(wordBytes(1, 2))...)
387 s.event(w.ID(), 3, wordBytes(workspace.STATE_ACTIVE|workspace.STATE_URGENT)...)
388 s.event(w.ID(), 4, wordBytes(workspace.WORKSPACE_CAPABILITIES_ACTIVATE|workspace.WORKSPACE_CAPABILITIES_ASSIGN)...)
389 s.event(mgr.ID(), 2)
390 if gcaps != workspace.GROUP_CAPABILITIES_CREATE_WORKSPACE || !reflect.DeepEqual(outs, []uint32{out.ID()}) || !reflect.DeepEqual(entered, []uint32{w.ID()}) ||
391 id != "ws-1" || name != "Workspace é" || !reflect.DeepEqual(decodeWords(coords), []uint32{1, 2}) ||
392 state != 3 || wcaps != 9 || !reflect.DeepEqual(log, []string{"done"}) {
393 t.Fatalf("state gcaps=%d outs=%v entered=%v id=%q name=%q coords=%v state=%d wcaps=%d log=%v", gcaps, outs, entered, id, name, coords, state, wcaps, log)
394 }
395
396 // Requests are only transactional after commit, so the wire order is
397 // request, request, commit.
398 noerr(t, w.Activate())
399 s.req(w.ID(), 1)
400 noerr(t, w.Assign(g))
401 wantWords(t, "assign", s.req(w.ID(), 3), g.ID())
402 noerr(t, g.CreateWorkspace("new"))
403 if b := s.req(g.ID(), 0); binary.NativeEndian.Uint32(b) != 4 || string(b[4:7]) != "new" {
404 t.Fatalf("create_workspace %x", b)
405 }
406 noerr(t, w.Deactivate())
407 s.req(w.ID(), 2)
408 noerr(t, w.Remove())
409 s.req(w.ID(), 4)
410 noerr(t, mgr.Commit())
411 s.req(mgr.ID(), 0)
412
413 // Removal: the compositor sends removed, the client answers with destroy.
414 s.event(w.ID(), 5)
415 s.event(g.ID(), 5)
416 noerr(t, w.Destroy())
417 s.req(w.ID(), 0)
418 noerr(t, g.Destroy())
419 s.req(g.ID(), 1)
420 // stop is answered by finished, which is the final event.
421 noerr(t, mgr.Stop())
422 s.req(mgr.ID(), 1)
423 s.event(mgr.ID(), 3)
424 if !reflect.DeepEqual(log, []string{"done", "workspace-removed", "group-removed", "finished"}) {
425 t.Fatal(log)
426 }
427 // finished is a destructor event: the manager is dead and must not
428 // reach the compositor any more.
429 if e := mgr.Commit(); e == nil {
430 t.Fatal("Commit after finished must fail")
431 }
432 s.noRequest()
433}
434
435func TestIdleNotifyVersionGate(t *testing.T) {
436 s := newWireEnv(t)
437 seat := s.seat()
438 old := idlenotify.NewExtIdleNotifier(s.d.Context())
439 s.bind(1, idlenotify.ExtIdleNotifierInterface, 2, 1, old) // negotiated v1
440 if _, e := old.GetInputIdleNotification(1000, seat); !errors.Is(e, wlturbo.ErrVersionTooLow) {
441 t.Fatalf("v1 get_input_idle_notification = %v, want ErrVersionTooLow", e)
442 }
443 s.noRequest() // refused locally, nothing reaches the compositor
444 n, e := old.GetIdleNotification(1000, seat)
445 noerr(t, e)
446 wantWords(t, "get_idle_notification", s.req(old.ID(), 1), n.ID(), 1000, seat.ID())
447
448 send(t, s.p, msg(s.d.Registry().ID(), 1, 1)) // global_remove for the v1 bind
449 s.dispatch()
450 cur := idlenotify.NewExtIdleNotifier(s.d.Context())
451 s.bind(2, idlenotify.ExtIdleNotifierInterface, 2, 2, cur)
452 in, e := cur.GetInputIdleNotification(0, seat) // zero timeout is valid
453 noerr(t, e)
454 wantWords(t, "get_input_idle_notification", s.req(cur.ID(), 2), in.ID(), 0, seat.ID())
455 var seq []string
456 in.OnIdled(func() { seq = append(seq, "idled") })
457 in.OnResumed(func() { seq = append(seq, "resumed") })
458 s.event(in.ID(), 0)
459 s.event(in.ID(), 1)
460 if !reflect.DeepEqual(seq, []string{"idled", "resumed"}) {
461 t.Fatal(seq)
462 }
463 noerr(t, in.Destroy())
464 s.req(in.ID(), 0)
465 noerr(t, n.Destroy())
466 s.req(n.ID(), 0)
467}
468
469func TestInhibitAndDesktopHintsWire(t *testing.T) {
470 s := newWireEnv(t)
471 surf, seat, out := s.surface(), s.seat(), s.output()
472
473 ii := idleinhibit.NewIdleInhibitManager(s.d.Context())
474 s.bind(1, idleinhibit.IdleInhibitManagerInterface, 1, 1, ii)
475 inh, e := ii.CreateInhibitor(surf)
476 noerr(t, e)
477 wantWords(t, "create_inhibitor", s.req(ii.ID(), 1), inh.ID(), surf.ID())
478 noerr(t, inh.Destroy())
479 s.req(inh.ID(), 0)
480 noerr(t, ii.Destroy())
481 s.req(ii.ID(), 0)
482
483 ks := shortcutsinhibit.NewKeyboardShortcutsInhibitManager(s.d.Context())
484 s.bind(2, shortcutsinhibit.KeyboardShortcutsInhibitManagerInterface, 1, 1, ks)
485 k, e := ks.InhibitShortcuts(surf, seat)
486 noerr(t, e)
487 wantWords(t, "inhibit_shortcuts", s.req(ks.ID(), 1), k.ID(), surf.ID(), seat.ID())
488 var seq []string
489 k.OnActive(func() { seq = append(seq, "active") })
490 k.OnInactive(func() { seq = append(seq, "inactive") })
491 s.event(k.ID(), 0)
492 s.event(k.ID(), 1)
493 if !reflect.DeepEqual(seq, []string{"active", "inactive"}) {
494 t.Fatal(seq)
495 }
496 noerr(t, k.Destroy())
497 s.req(k.ID(), 0)
498 noerr(t, ks.Destroy())
499 s.req(ks.ID(), 0)
500
501 xo := xdgoutput.NewZxdgOutputManager(s.d.Context())
502 s.bind(3, xdgoutput.ZxdgOutputManagerInterface, 3, 3, xo)
503 o, e := xo.GetXdgOutput(out)
504 noerr(t, e)
505 wantWords(t, "get_xdg_output", s.req(xo.ID(), 1), o.ID(), out.ID())
506 var pos, size [2]int32
507 var name string
508 o.OnLogicalPosition(func(x, y int32) { pos = [2]int32{x, y} })
509 o.OnLogicalSize(func(w, h int32) { size = [2]int32{w, h} })
510 o.OnName(func(n string) { name = n })
511 s.event(o.ID(), 0, wordBytes(uint32(0xfffffff6), 20)...) // -10, 20
512 s.event(o.ID(), 1, wordBytes(1920, 1080)...)
513 s.event(o.ID(), 3, str("DP-1")...)
514 if pos != [2]int32{-10, 20} || size != [2]int32{1920, 1080} || name != "DP-1" {
515 t.Fatal(pos, size, name)
516 }
517 noerr(t, o.Destroy())
518 s.req(o.ID(), 0)
519 noerr(t, xo.Destroy())
520 s.req(xo.ID(), 0)
521}
522
523func TestXdgDecorationCrossPackageToplevel(t *testing.T) {
524 s := newWireEnv(t)
525 surf := s.surface()
526 wm := xdgshell.NewXdgWmBase(s.d.Context())
527 s.bind(1, xdgshell.XdgWmBaseInterface, 6, 6, wm)
528 xs, e := wm.GetXdgSurface(surf)
529 noerr(t, e)
530 s.req(wm.ID(), 2)
531 top, e := xs.GetToplevel()
532 noerr(t, e)
533 s.req(xs.ID(), 1)
534
535 dm := xdgdecoration.NewZxdgDecorationManager(s.d.Context())
536 s.bind(2, xdgdecoration.ZxdgDecorationManagerInterface, 2, 2, dm)
537 dec, e := dm.GetToplevelDecoration(top)
538 noerr(t, e)
539 wantWords(t, "get_toplevel_decoration", s.req(dm.ID(), 1), dec.ID(), top.ID())
540 noerr(t, dec.SetMode(xdgdecoration.MODE_SERVER_SIDE))
541 wantWords(t, "set_mode", s.req(dec.ID(), 1), xdgdecoration.MODE_SERVER_SIDE)
542 var modes []uint32
543 dec.OnConfigure(func(m uint32) { modes = append(modes, m) })
544 s.event(dec.ID(), 0, wordBytes(xdgdecoration.MODE_CLIENT_SIDE)...)
545 if !reflect.DeepEqual(modes, []uint32{xdgdecoration.MODE_CLIENT_SIDE}) {
546 t.Fatal(modes)
547 }
548 noerr(t, dec.UnsetMode())
549 s.req(dec.ID(), 2)
550 noerr(t, dec.Destroy())
551 s.req(dec.ID(), 0)
552 noerr(t, dm.Destroy())
553 s.req(dm.ID(), 0)
554}
555
556func TestImageCaptureCrossPackageChildren(t *testing.T) {
557 s := newWireEnv(t)
558 out, ptr := s.output(), s.pointer()
559
560 // Foreign toplevel handles come from ext-foreign-toplevel-list and are
561 // consumed by a different package (imagecapturesource).
562 list := extforeigntoplevel.NewExtForeignToplevelList(s.d.Context())
563 s.bind(1, extforeigntoplevel.ExtForeignToplevelListInterface, 1, 1, list)
564 var handle *extforeigntoplevel.ExtForeignToplevelHandle
565 list.OnToplevel(func(h *extforeigntoplevel.ExtForeignToplevelHandle) { handle = h })
566 s.event(list.ID(), 0, wordBytes(serverID+1)...)
567 if handle == nil || handle.ID() != serverID+1 {
568 t.Fatal("toplevel handle missing")
569 }
570 var title, appID, ident string
571 handle.OnTitle(func(v string) { title = v })
572 handle.OnAppId(func(v string) { appID = v })
573 handle.OnIdentifier(func(v string) { ident = v })
574 s.event(handle.ID(), 2, str("term")...)
575 s.event(handle.ID(), 3, str("org.example.term")...)
576 s.event(handle.ID(), 4, str("id-1")...)
577 if title != "term" || appID != "org.example.term" || ident != "id-1" {
578 t.Fatal(title, appID, ident)
579 }
580
581 om := imagecapturesource.NewExtOutputImageCaptureSourceManager(s.d.Context())
582 s.bind(2, imagecapturesource.ExtOutputImageCaptureSourceManagerInterface, 1, 1, om)
583 fm := imagecapturesource.NewExtForeignToplevelImageCaptureSourceManager(s.d.Context())
584 s.bind(3, imagecapturesource.ExtForeignToplevelImageCaptureSourceManagerInterface, 1, 1, fm)
585 outSrc, e := om.CreateSource(out)
586 noerr(t, e)
587 wantWords(t, "output create_source", s.req(om.ID(), 0), outSrc.ID(), out.ID())
588 topSrc, e := fm.CreateSource(handle)
589 noerr(t, e)
590 wantWords(t, "toplevel create_source", s.req(fm.ID(), 0), topSrc.ID(), handle.ID())
591
592 cm := imagecopycapture.NewExtImageCopyCaptureManager(s.d.Context())
593 s.bind(4, imagecopycapture.ExtImageCopyCaptureManagerInterface, 1, 1, cm)
594 sess, e := cm.CreateSession(topSrc, imagecopycapture.OPTIONS_PAINT_CURSORS)
595 noerr(t, e)
596 wantWords(t, "create_session", s.req(cm.ID(), 0), sess.ID(), topSrc.ID(), imagecopycapture.OPTIONS_PAINT_CURSORS)
597 cur, e := cm.CreatePointerCursorSession(outSrc, ptr)
598 noerr(t, e)
599 wantWords(t, "create_pointer_cursor_session", s.req(cm.ID(), 1), cur.ID(), outSrc.ID(), ptr.ID())
600
601 // Session constraints: advertised before done, arrays decoded.
602 var log []string
603 var size [2]uint32
604 var dev []byte
605 var dmaFormat uint32
606 var mods []byte
607 sess.OnBufferSize(func(w, h uint32) { size = [2]uint32{w, h}; log = append(log, "size") })
608 var shm []uint32
609 sess.OnShmFormat(func(f uint32) { shm = append(shm, f); log = append(log, "shm") })
610 sess.OnDmabufDevice(func(d []byte) { dev = d; log = append(log, "dev") })
611 sess.OnDmabufFormat(func(f uint32, m []byte) { dmaFormat, mods = f, m; log = append(log, "dmabuf") })
612 sess.OnDone(func() { log = append(log, "done") })
613 sess.OnStopped(func() { log = append(log, "stopped") })
614 s.event(sess.ID(), 0, wordBytes(1280, 720)...)
615 s.event(sess.ID(), 1, wordBytes(1)...) // wl_shm.format xrgb8888
616 s.event(sess.ID(), 2, arr(make([]byte, 8))...)
617 s.event(sess.ID(), 3, cat(wordBytes(0x34325258), arr(wordBytes(0, 0, 1, 0)))...)
618 s.event(sess.ID(), 4)
619 if !reflect.DeepEqual(shm, []uint32{1}) {
620 t.Fatalf("shm formats %v", shm)
621 }
622 if size != [2]uint32{1280, 720} || len(dev) != 8 || dmaFormat != 0x34325258 || len(mods) != 16 ||
623 !reflect.DeepEqual(log, []string{"size", "shm", "dev", "dmabuf", "done"}) {
624 t.Fatal(size, dev, dmaFormat, mods, log)
625 }
626
627 frame, e := sess.CreateFrame()
628 noerr(t, e)
629 wantWords(t, "create_frame", s.req(sess.ID(), 0), frame.ID())
630 buf := s.buffer()
631 noerr(t, frame.AttachBuffer(buf))
632 wantWords(t, "attach_buffer", s.req(frame.ID(), 1), buf.ID())
633 noerr(t, frame.DamageBuffer(0, 0, 1280, 720))
634 wantWords(t, "damage_buffer", s.req(frame.ID(), 2), 0, 0, 1280, 720)
635 noerr(t, frame.Capture())
636 s.req(frame.ID(), 3)
637
638 var fl []string
639 var damage [4]int32
640 var tv [3]uint32
641 frame.OnTransform(func(v uint32) { fl = append(fl, "transform") })
642 frame.OnDamage(func(x, y, w, h int32) { damage = [4]int32{x, y, w, h}; fl = append(fl, "damage") })
643 frame.OnPresentationTime(func(hi, lo, ns uint32) { tv = [3]uint32{hi, lo, ns}; fl = append(fl, "time") })
644 frame.OnReady(func() { fl = append(fl, "ready") })
645 frame.OnFailed(func(uint32) { fl = append(fl, "failed") })
646 s.event(frame.ID(), 0, wordBytes(0)...)
647 s.event(frame.ID(), 1, wordBytes(1, 2, 3, 4)...)
648 s.event(frame.ID(), 2, wordBytes(0, 1700000000, 999)...)
649 s.event(frame.ID(), 3)
650 if damage != [4]int32{1, 2, 3, 4} || tv != [3]uint32{0, 1700000000, 999} ||
651 !reflect.DeepEqual(fl, []string{"transform", "damage", "time", "ready"}) {
652 t.Fatal(damage, tv, fl)
653 }
654 // ready is terminal for a frame, and a session allows one frame at a time
655 // even after ready, so destroy it before creating the frame that fails.
656 noerr(t, frame.Destroy())
657 s.req(frame.ID(), 0)
658 failing, e := sess.CreateFrame()
659 noerr(t, e)
660 wantWords(t, "create_frame(2)", s.req(sess.ID(), 0), failing.ID())
661 noerr(t, failing.AttachBuffer(buf))
662 s.req(failing.ID(), 1)
663 noerr(t, failing.Capture())
664 s.req(failing.ID(), 3)
665 var reasons []uint32
666 failing.OnReady(func() { t.Error("failed frame must not become ready") })
667 failing.OnFailed(func(r uint32) { reasons = append(reasons, r) })
668 s.event(failing.ID(), 4, wordBytes(imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS)...)
669 if !reflect.DeepEqual(reasons, []uint32{imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS}) {
670 t.Fatal(reasons)
671 }
672
673 // Cursor session: get_capture_session is a typed child too.
674 var cl []string
675 var pos, hot [2]int32
676 cur.OnEnter(func() { cl = append(cl, "enter") })
677 cur.OnLeave(func() { cl = append(cl, "leave") })
678 cur.OnPosition(func(x, y int32) { pos = [2]int32{x, y} })
679 cur.OnHotspot(func(x, y int32) { hot = [2]int32{x, y} })
680 s.event(cur.ID(), 0)
681 s.event(cur.ID(), 2, wordBytes(uint32(0xfffffffb), 9)...) // -5, 9
682 s.event(cur.ID(), 3, wordBytes(4, 6)...)
683 s.event(cur.ID(), 1)
684 if pos != [2]int32{-5, 9} || hot != [2]int32{4, 6} || !reflect.DeepEqual(cl, []string{"enter", "leave"}) {
685 t.Fatal(pos, hot, cl)
686 }
687 csess, e := cur.GetCaptureSession()
688 noerr(t, e)
689 wantWords(t, "get_capture_session", s.req(cur.ID(), 1), csess.ID())
690 if csess.Version() != cur.Version() {
691 t.Fatalf("child version %d, parent %d", csess.Version(), cur.Version())
692 }
693
694 // destroy order: children, sources, managers, toplevel handle.
695 for _, c := range []struct {
696 name string
697 id uint32
698 op uint16
699 fn func() error
700 }{
701 {"failed frame", failing.ID(), 0, failing.Destroy},
702 {"capture session", csess.ID(), 1, csess.Destroy},
703 {"session", sess.ID(), 1, sess.Destroy},
704 {"cursor", cur.ID(), 0, cur.Destroy},
705 {"output source", outSrc.ID(), 0, outSrc.Destroy},
706 {"toplevel source", topSrc.ID(), 0, topSrc.Destroy},
707 {"copy manager", cm.ID(), 2, cm.Destroy},
708 {"output manager", om.ID(), 1, om.Destroy},
709 {"toplevel manager", fm.ID(), 1, fm.Destroy},
710 {"handle", handle.ID(), 0, handle.Destroy},
711 } {
712 // Subtests run serially (no t.Parallel): the wire order of destroy
713 // requests on the shared socketpair is part of what is asserted.
714 ok := t.Run(c.name, func(t *testing.T) {
715 ss := *s
716 ss.t = t
717 noerr(t, c.fn())
718 ss.req(c.id, c.op)
719 })
720 if !ok {
721 // A failed step leaves unread bytes on the socket; carrying on
722 // would misattribute them to later destroys.
723 t.FailNow()
724 }
725 }
726}