1package wayland
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "image"
8 "strings"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
14 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
15 extws "github.com/bnema/go-wayland-bindings/server/extworkspace"
16 "github.com/bnema/go-wayland-bindings/server/wayland"
17 "github.com/bnema/go-wayland-bindings/server/wlrlayershell"
18 wlr "github.com/bnema/go-wayland-bindings/server/wlrscreencopy"
19 "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
20 "github.com/bnema/neferwl/internal/logging"
21 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
22 "github.com/bnema/neferwl/internal/ports"
23 "github.com/bnema/purego-libwayland/server"
24 "github.com/bnema/wlturbo"
25 "github.com/stretchr/testify/mock"
26 "golang.org/x/sys/unix"
27)
28
29// sessionMsg is one decoded event of the neferwl extension.
30type sessionMsg struct {
31 op uint16
32 token string
33 numbers []int64
34}
35
36// attachEvents decodes the events of a neferwl_capture_layer_v1.
37type attachEvents struct {
38 wlturbo.BaseProxy
39 msgs chan sessionMsg
40}
41
42func (p *attachEvents) Dispatch(e *wlturbo.Event) {
43 m := sessionMsg{op: e.Opcode}
44 if uint32(e.Opcode) != imagecapture.NeferwlCaptureLayerV1EventAttached {
45 m.numbers = append(m.numbers, int64(e.Uint32()))
46 }
47 p.msgs <- m
48}
49
50// exclusionEvents decodes the events of a neferwl_capture_exclusion_v1.
51type exclusionEvents struct {
52 wlturbo.BaseProxy
53 msgs chan sessionMsg
54}
55
56func (p *exclusionEvents) Dispatch(e *wlturbo.Event) {
57 m := sessionMsg{op: e.Opcode}
58 switch uint32(e.Opcode) {
59 case imagecapture.NeferwlCaptureExclusionV1EventToken:
60 m.token = e.String()
61 case imagecapture.NeferwlCaptureExclusionV1EventFailed:
62 m.numbers = append(m.numbers, int64(e.Uint32()))
63 }
64 p.msgs <- m
65}
66
67// captureHarness is a server with one 4x4 output and every capture channel.
68type captureHarness struct {
69 s *Server
70 dir string
71 events chan ports.ClientEvent
72 commands chan ports.ClientCommand
73 workspaces chan ports.Workspaces
74 captures chan ports.CaptureRequest
75 captured chan ports.CaptureDone
76}
77
78var captureOutput = ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}
79
80func newCaptureHarness(t *testing.T) *captureHarness {
81 t.Helper()
82 return newCaptureHarnessWith(t, nil)
83}
84
85// newCaptureHarnessWith lets a test adjust the options and channels (for
86// instance to wire a session-lock gate) before the server starts.
87func newCaptureHarnessWith(t *testing.T, tune func(*Options, *Channels)) *captureHarness {
88 t.Helper()
89 h := &captureHarness{
90 dir: t.TempDir(),
91 events: make(chan ports.ClientEvent, 64),
92 commands: make(chan ports.ClientCommand, 16),
93 workspaces: make(chan ports.Workspaces, 4),
94 captures: make(chan ports.CaptureRequest, 8),
95 captured: make(chan ports.CaptureDone, 8),
96 }
97 opts := Options{RuntimeDir: h.dir, Outputs: ports.Layout{captureOutput}, CaptureAllow: allowStore(t, "*\n")}
98 ch := Channels{Events: h.events, Commands: h.commands, Workspaces: h.workspaces, Captures: h.captures, Captured: h.captured}
99 if tune != nil {
100 tune(&opts, &ch)
101 }
102 s, err := New(opts, ch, logging.For(context.Background(), "wayland"))
103 if err != nil {
104 t.Fatal(err)
105 }
106 h.s = s
107 ctx, cancel := context.WithCancel(context.Background())
108 done := make(chan error, 1)
109 go func() { done <- s.Run(ctx) }()
110 t.Cleanup(func() {
111 cancel()
112 if err := <-done; err != nil {
113 t.Errorf("Run: %v", err)
114 }
115 for len(h.captures) > 0 {
116 _ = (<-h.captures).Dst.File.Close()
117 }
118 })
119 return h
120}
121
122// cclient is a client with every capture global bound.
123type cclient struct {
124 c *wlturbo.Display
125 output uint32
126 outSrc uint32 // ext_output_image_capture_source_manager_v1
127 extra uint32 // neferwl_image_capture_source_manager_v1
128 copyMgr uint32 // ext_image_copy_capture_manager_v1
129 excl uint32 // neferwl_capture_exclusion_manager_v1
130}
131
132func (h *captureHarness) client(t *testing.T) *cclient {
133 t.Helper()
134 c := protocolClient(t, h.s, h.dir)
135 out := bindVersion(t, c, "wl_output", 4)
136 registerProtocol(t, c, out)
137 return &cclient{c: c, output: out,
138 outSrc: bindProtocol(t, c, "ext_output_image_capture_source_manager_v1"),
139 extra: bindProtocol(t, c, "neferwl_image_capture_source_manager_v1"),
140 copyMgr: bindProtocol(t, c, "ext_image_copy_capture_manager_v1"),
141 excl: bindProtocol(t, c, "neferwl_capture_exclusion_manager_v1"),
142 }
143}
144
145func (cc *cclient) roundtrip(t *testing.T) {
146 t.Helper()
147 if err := cc.c.Roundtrip(); err != nil {
148 t.Fatal(err)
149 }
150}
151
152// outputSource, regionSource and workspaceSource make a capture source.
153func (cc *cclient) outputSource(t *testing.T) uint32 {
154 t.Helper()
155 src := cc.c.AllocateID()
156 registerProtocol(t, cc.c, src)
157 requestProtocol(t, cc.c, cc.outSrc, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, cc.output)
158 return src
159}
160
161func (cc *cclient) regionSource(t *testing.T, x, y, w, h int32) uint32 {
162 t.Helper()
163 src := cc.c.AllocateID()
164 registerProtocol(t, cc.c, src)
165 requestProtocol(t, cc.c, cc.extra, imagecapture.NeferwlImageCaptureSourceManagerV1RequestCreateOutputRegionSource, src, cc.output, x, y, w, h)
166 return src
167}
168
169func (cc *cclient) workspaceSource(t *testing.T, handle uint32) uint32 {
170 t.Helper()
171 src := cc.c.AllocateID()
172 registerProtocol(t, cc.c, src)
173 requestProtocol(t, cc.c, cc.extra, imagecapture.NeferwlImageCaptureSourceManagerV1RequestCreateWorkspaceSource, src, handle)
174 return src
175}
176
177// session opens an ext-image-copy-capture session on a source and returns its
178// object and the decoded events (see captureDetails).
179func (cc *cclient) session(t *testing.T, src uint32) (uint32, chan []uint32) {
180 t.Helper()
181 id := cc.c.AllocateID()
182 p := &captureDetails{events: make(chan []uint32, 32)}
183 p.SetID(id)
184 registerWireProxy(cc.c, p)
185 requestProtocol(t, cc.c, cc.copyMgr, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, id, src, uint32(0))
186 cc.roundtrip(t)
187 return id, p.events
188}
189
190// Session event opcodes as captureDetails reports them.
191const (
192 evBufferSize = uint32(ext.ExtImageCopyCaptureSessionV1EventBufferSize)
193 evDone = uint32(ext.ExtImageCopyCaptureSessionV1EventDone)
194 evStopped = uint32(ext.ExtImageCopyCaptureSessionV1EventStopped)
195)
196
197// constraints reads one complete batch (buffer_size, two formats, done) and
198// returns the size.
199func constraints(t *testing.T, cc *cclient, ev chan []uint32) (w, h uint32) {
200 t.Helper()
201 got := nextSession(t, cc, ev)
202 if got[0] != evBufferSize || len(got) != 3 {
203 t.Fatalf("expected buffer_size, got %v", got)
204 }
205 w, h = got[1], got[2]
206 for _, want := range []uint32{uint32(ext.ExtImageCopyCaptureSessionV1EventShmFormat), uint32(ext.ExtImageCopyCaptureSessionV1EventShmFormat), evDone} {
207 if g := nextSession(t, cc, ev); g[0] != want {
208 t.Fatalf("batch event %v, want opcode %d", g, want)
209 }
210 }
211 return w, h
212}
213
214// nextSession waits for the next decoded session event, dispatching the client.
215func nextSession(t *testing.T, cc *cclient, ev chan []uint32) []uint32 {
216 t.Helper()
217 deadline := time.Now().Add(2 * time.Second)
218 for time.Now().Before(deadline) {
219 select {
220 case v := <-ev:
221 return v
222 default:
223 }
224 cc.roundtrip(t)
225 time.Sleep(time.Millisecond)
226 }
227 t.Fatal("timed out waiting for a session event")
228 return nil
229}
230
231func noSession(t *testing.T, cc *cclient, ev chan []uint32) {
232 t.Helper()
233 cc.roundtrip(t)
234 select {
235 case v := <-ev:
236 t.Fatalf("unexpected session event %v", v)
237 default:
238 }
239}
240
241// settle waits until the server applied the commands sent so far and the
242// client received what followed.
243func (h *captureHarness) settle(t *testing.T, cc *cclient) {
244 t.Helper()
245 // The command loop takes commands off the channel before it applies
246 // them on the display: an empty channel does not mean applied. Commands
247 // apply in order, so a marker (SlotsPending, a plain flag) seen on the
248 // display means every command sent before it was applied.
249 var was bool
250 if !h.s.display.Do(func() { was = h.s.slotsPending }) {
251 t.Fatal("display stopped")
252 }
253 h.applied(t, !was)
254 h.applied(t, was)
255 cc.roundtrip(t)
256}
257
258// applied sends SlotsPending{pending} and waits until the display applied it.
259func (h *captureHarness) applied(t *testing.T, pending bool) {
260 t.Helper()
261 h.commands <- ports.SlotsPending{Pending: pending}
262 deadline := time.Now().Add(2 * time.Second)
263 for {
264 var now bool
265 if !h.s.display.Do(func() { now = h.s.slotsPending }) {
266 t.Fatal("display stopped")
267 }
268 if now == pending {
269 return
270 }
271 if time.Now().After(deadline) {
272 t.Fatal("commands not applied")
273 }
274 time.Sleep(time.Millisecond)
275 }
276}
277
278func (h *captureHarness) send(cmd ports.ClientCommand) { h.commands <- cmd }
279
280// captureShm makes a w x h XRGB8888 wl_buffer.
281func captureShm(t *testing.T, c *wlturbo.Display, w, h int32) (buf uint32, fd int) {
282 t.Helper()
283 shm := bindProtocol(t, c, "wl_shm")
284 registerProtocol(t, c, shm)
285 fd, err := unix.MemfdCreate("ext-capture", 0)
286 if err != nil {
287 t.Fatal(err)
288 }
289 size := w * h * 4
290 if err := unix.Ftruncate(fd, int64(size)); err != nil {
291 t.Fatal(err)
292 }
293 pool := c.AllocateID()
294 buf = c.AllocateID()
295 registerProtocol(t, c, pool)
296 registerProtocol(t, c, buf)
297 if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
298 t.Fatal(err)
299 }
300 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), w, h, w*4, uint32(wayland.ShmFormatXrgb8888))
301 t.Cleanup(func() { _ = unix.Close(fd) })
302 return buf, fd
303}
304
305// extFrame asks one frame of a session into a fresh w x h buffer and returns
306// its events; the frame object is destroyed so the next one may follow.
307func (cc *cclient) extFrame(t *testing.T, session uint32, w, h int32) *captureEvents {
308 t.Helper()
309 buf, _ := captureShm(t, cc.c, w, h)
310 frame := cc.c.AllocateID()
311 ev := &captureEvents{events: make(chan uint16, 16)}
312 ev.SetID(frame)
313 registerWireProxy(cc.c, ev)
314 requestProtocol(t, cc.c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
315 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
316 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
317 cc.roundtrip(t)
318 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestDestroy)
319 cc.roundtrip(t)
320 return ev
321}
322
323// liveExtFrame is extFrame whose frame object stays alive, so a held frame
324// can still be served.
325func (cc *cclient) liveExtFrame(t *testing.T, session uint32, w, h int32) *captureEvents {
326 t.Helper()
327 buf, _ := captureShm(t, cc.c, w, h)
328 frame := cc.c.AllocateID()
329 ev := &captureEvents{events: make(chan uint16, 16)}
330 ev.SetID(frame)
331 registerWireProxy(cc.c, ev)
332 requestProtocol(t, cc.c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
333 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
334 requestProtocol(t, cc.c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
335 cc.roundtrip(t)
336 return ev
337}
338
339// wlrFrame asks a wlr-screencopy capture of the whole output into a buffer.
340func (cc *cclient) wlrFrame(t *testing.T) *captureEvents {
341 t.Helper()
342 buf, _ := captureShm(t, cc.c, 4, 4)
343 mgr := bindVersion(t, cc.c, "zwlr_screencopy_manager_v1", 3)
344 frame := cc.c.AllocateID()
345 ev := &captureEvents{events: make(chan uint16, 16)}
346 ev.SetID(frame)
347 registerWireProxy(cc.c, ev)
348 requestProtocol(t, cc.c, mgr, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), cc.output)
349 requestProtocol(t, cc.c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
350 cc.roundtrip(t)
351 return ev
352}
353
354func receiveCapture(t *testing.T, ch chan ports.CaptureRequest) ports.CaptureRequest {
355 t.Helper()
356 select {
357 case r := <-ch:
358 _ = r.Dst.File.Close()
359 return r
360 case <-time.After(2 * time.Second):
361 t.Fatal("no capture request")
362 return ports.CaptureRequest{}
363 }
364}
365
366func noCapture(t *testing.T, ch chan ports.CaptureRequest) {
367 t.Helper()
368 select {
369 case r := <-ch:
370 _ = r.Dst.File.Close()
371 t.Fatalf("a capture reached the renderer: %+v", r)
372 default:
373 }
374}
375
376// eventsInclude drains the frame events received so far and reports op among them.
377func eventsInclude(ev *captureEvents, op uint16) bool {
378 found := false
379 for len(ev.events) > 0 {
380 if <-ev.events == op {
381 found = true
382 }
383 }
384 return found
385}
386
387const (
388 frameReady = uint16(ext.ExtImageCopyCaptureFrameV1EventReady)
389 frameFailed = uint16(ext.ExtImageCopyCaptureFrameV1EventFailed)
390)
391
392func (h *captureHarness) open(t *testing.T) ports.CaptureSessionOpen {
393 t.Helper()
394 return nextEvent[ports.CaptureSessionOpen](t, h.events)
395}
396
397// active is the state core sends for a live session on the whole output.
398func active(id uint64) ports.CaptureSessionState {
399 return ports.CaptureSessionState{ID: id, Output: "HEADLESS-1", Rect: ports.Rect{W: 4, H: 4}, Active: true}
400}
401
402func TestTrustedPeer(t *testing.T) {
403 for _, tc := range []struct {
404 name string
405 peer server.Credentials
406 err error
407 want bool
408 }{
409 {"same user", server.Credentials{UID: 1000}, nil, true},
410 {"other user", server.Credentials{UID: 1001}, nil, false},
411 {"root is not the user", server.Credentials{UID: 0}, nil, false},
412 {"overflow id", server.Credentials{UID: 65534}, nil, false},
413 {"unreadable", server.Credentials{UID: 1000}, context.Canceled, false},
414 } {
415 if got := trustedPeer(1000, tc.peer, tc.err); got != tc.want {
416 t.Errorf("%s: %v", tc.name, got)
417 }
418 }
419}
420
421// A session on an output source registers with core and gets the output size.
422func TestOutputSessionRegistersAndClose(t *testing.T) {
423 h := newCaptureHarness(t)
424 cc := h.client(t)
425 session, ev := cc.session(t, cc.outputSource(t))
426 if w, hh := constraints(t, cc, ev); w != 4 || hh != 4 {
427 t.Fatalf("size %dx%d", w, hh)
428 }
429 open := h.open(t)
430 if open.ID == 0 || open.Output != "HEADLESS-1" || open.Workspace != 0 || open.Region != (ports.Rect{}) {
431 t.Fatalf("open %+v", open)
432 }
433 requestProtocol(t, cc.c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
434 cc.roundtrip(t)
435 if c := nextEvent[ports.CaptureSessionClose](t, h.events); c.ID != open.ID {
436 t.Fatalf("close %+v", c)
437 }
438}
439
440// A region source is clipped to the output, sized in physical pixels, and its
441// buffer size is sent again when the scale changes.
442func TestRegionSourceConstraintsFollowScale(t *testing.T) {
443 h := newCaptureHarness(t)
444 cc := h.client(t)
445 session, ev := cc.session(t, cc.regionSource(t, 1, 1, 2, 2))
446 if w, hh := constraints(t, cc, ev); w != 2 || hh != 2 {
447 t.Fatalf("size %dx%d", w, hh)
448 }
449 open := h.open(t)
450 if open.Output != "HEADLESS-1" || open.Region != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) {
451 t.Fatalf("open %+v", open)
452 }
453 // Clipped: a rectangle past the output edge.
454 _, ev2 := cc.session(t, cc.regionSource(t, 3, 3, 10, 10))
455 if w, hh := constraints(t, cc, ev2); w != 1 || hh != 1 {
456 t.Fatalf("clipped size %dx%d", w, hh)
457 }
458 h.open(t)
459 // Doubling the scale of a bigger mode doubles the buffer.
460 bigger := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 8, Height: 8}, Width: 4, Height: 4, Scale: 2}
461 applyOutputs(t, h.s, h.commands, ports.SetOutputs{Outputs: ports.Layout{bigger}})
462 if w, hh := constraints(t, cc, ev); w != 4 || hh != 4 {
463 t.Fatalf("size after the scale change %dx%d", w, hh)
464 }
465 noSession(t, cc, ev) // one batch only
466 _ = session
467 // The frame asked with the new size is served for the new region.
468 r := func() ports.CaptureRequest {
469 cc.extFrame(t, session, 4, 4)
470 return receiveCapture(t, h.captures)
471 }()
472 if r.Region.Dx() != 4 || r.Region.Dy() != 4 || r.Workspace != 0 || r.Exclude {
473 t.Fatalf("request %+v", r)
474 }
475}
476
477// A frame whose buffer does not fit the geometry computed now, while the last
478// constraints the client got are stale (the refresh has not run), fails with
479// buffer_constraints after a new constraints batch: the client can retry.
480func TestStaleConstraintsAreResentBeforeBufferConstraintsFailure(t *testing.T) {
481 h := newCaptureHarness(t)
482 cc := h.client(t)
483 session, ev := cc.session(t, cc.outputSource(t))
484 constraints(t, cc, ev)
485 h.open(t)
486 // The client's last batch said 1x1 while the output is 4x4 now.
487 if !h.s.display.Do(func() {
488 for c := range h.s.captureSessions {
489 c.sent = image.Pt(1, 1)
490 }
491 }) {
492 t.Fatal("display stopped")
493 }
494 frame := cc.extFrame(t, session, 1, 1)
495 if !eventsInclude(frame, frameFailed) {
496 t.Fatal("a buffer of the stale size was served")
497 }
498 noCapture(t, h.captures)
499 if w, hh := constraints(t, cc, ev); w != 4 || hh != 4 {
500 t.Fatalf("resent constraints %dx%d", w, hh)
501 }
502 // Up to date: a wrong buffer fails without another batch.
503 frame = cc.extFrame(t, session, 1, 1)
504 if !eventsInclude(frame, frameFailed) {
505 t.Fatal("a buffer of the wrong size was served")
506 }
507 noSession(t, cc, ev)
508}
509
510func TestRegionSourceInvalidSize(t *testing.T) {
511 for _, size := range [][2]int32{{0, 4}, {4, 0}, {-1, 4}} {
512 h := newCaptureHarness(t)
513 cc := h.client(t)
514 cc.regionSource(t, 0, 0, size[0], size[1])
515 expectProtocolError(t, cc.c, cc.extra, uint32(imagecapture.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion))
516 }
517}
518
519// A region with nothing left on the output stops at once and is not
520// registered with core; so does a session on an output that is gone.
521func TestRegionAndOutputSourcesThatAreEmptyStop(t *testing.T) {
522 h := newCaptureHarness(t)
523 cc := h.client(t)
524 _, ev := cc.session(t, cc.regionSource(t, 100, 100, 5, 5))
525 if g := nextSession(t, cc, ev); g[0] != evStopped {
526 t.Fatalf("event %v", g)
527 }
528 select {
529 case e := <-h.events:
530 t.Fatalf("event for a stopped session: %+v", e)
531 default:
532 }
533}
534
535// Removing the output stops its sessions at once and tells core.
536func TestOutputRemovalStopsRegionAndOutputSessions(t *testing.T) {
537 h := newCaptureHarness(t)
538 cc := h.client(t)
539 _, ev1 := cc.session(t, cc.outputSource(t))
540 _, ev2 := cc.session(t, cc.regionSource(t, 0, 0, 2, 2))
541 constraints(t, cc, ev1)
542 constraints(t, cc, ev2)
543 a, b := h.open(t), h.open(t)
544 applyOutputs(t, h.s, h.commands, ports.SetOutputs{})
545 for _, ev := range []chan []uint32{ev1, ev2} {
546 if g := nextSession(t, cc, ev); g[0] != evStopped {
547 t.Fatalf("event %v", g)
548 }
549 }
550 closed := map[uint64]bool{}
551 for len(closed) < 2 {
552 closed[nextEvent[ports.CaptureSessionClose](t, h.events).ID] = true
553 }
554 if !closed[a.ID] || !closed[b.ID] {
555 t.Fatalf("closed %v", closed)
556 }
557}
558
559// workspaceClient binds ext_workspace_manager_v1 on cc and returns the handle
560// of each workspace announced, by order.
561func (h *captureHarness) handles(t *testing.T, cc *cclient, want int) []uint32 {
562 t.Helper()
563 mgr := bindProtocol(t, cc.c, "ext_workspace_manager_v1")
564 p := &workspaceEvents{client: cc.c, events: make(chan [2]uint32, 128), idEvents: make(chan string, 16), order: &workspaceEventOrder{}}
565 p.SetID(mgr)
566 registerWireProxy(cc.c, p)
567 var out []uint32
568 for i := 0; i < 16 && len(out) < want; i++ {
569 cc.roundtrip(t)
570 for len(p.events) > 0 {
571 if v := <-p.events; v[0] == uint32(extws.ExtWorkspaceManagerV1EventWorkspace) {
572 out = append(out, v[1])
573 }
574 }
575 }
576 if len(out) != want {
577 t.Fatalf("%d workspace handles, want %d", len(out), want)
578 }
579 return out
580}
581
582func (h *captureHarness) snapshot(t *testing.T, ws ...ports.WorkspaceInfo) {
583 t.Helper()
584 h.workspaces <- ports.Workspaces{Outputs: []ports.WorkspaceOutput{{Name: "HEADLESS-1", Workspaces: ws}}}
585 deadline := time.Now().Add(2 * time.Second)
586 for time.Now().Before(deadline) {
587 got := 0
588 h.s.display.Do(func() {
589 for _, o := range h.s.workspaceSnapshot.Outputs {
590 got += len(o.Workspaces)
591 }
592 })
593 if got == len(ws) {
594 return
595 }
596 time.Sleep(time.Millisecond)
597 }
598 t.Fatal("snapshot not applied")
599}
600
601// A workspace source registers the workspace ID, sizes buffers from the
602// frame (off screen: the whole frame at the output scale), sends new
603// constraints when core moves or resizes it, and stops at once when the
604// handle is removed.
605func TestWorkspaceSourceSessionLifecycle(t *testing.T) {
606 h := newCaptureHarness(t)
607 h.snapshot(t,
608 ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}},
609 ports.WorkspaceInfo{ID: 43, Name: "2", Hidden: true, Index: 1, Frame: ports.Rect{W: 3, H: 2}},
610 )
611 cc := h.client(t)
612 handles := h.handles(t, cc, 2)
613 // On screen: its frame on the output.
614 _, evOn := cc.session(t, cc.workspaceSource(t, handles[0]))
615 if w, hh := constraints(t, cc, evOn); w != 4 || hh != 4 {
616 t.Fatalf("on-screen size %dx%d", w, hh)
617 }
618 if o := h.open(t); o.Workspace != 42 || o.Output != "" {
619 t.Fatalf("open %+v", o)
620 }
621 // Off screen: the whole frame.
622 sess, ev := cc.session(t, cc.workspaceSource(t, handles[1]))
623 if w, hh := constraints(t, cc, ev); w != 3 || hh != 2 {
624 t.Fatalf("off-screen size %dx%d", w, hh)
625 }
626 open := h.open(t)
627 if open.Workspace != 43 {
628 t.Fatalf("open %+v", open)
629 }
630 // Core resizes the frame: new constraints, once.
631 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 2, H: 2}, Workspace: 43, Hidden: true, Active: true})
632 if w, hh := constraints(t, cc, ev); w != 2 || hh != 2 {
633 t.Fatalf("resized %dx%d", w, hh)
634 }
635 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 2, H: 2}, Workspace: 43, Hidden: true, Active: true, Revision: 0})
636 h.settle(t, cc)
637 noSession(t, cc, ev)
638 // A frame asks for the off-screen workspace, whole.
639 cc.extFrame(t, sess, 2, 2)
640 r := receiveCapture(t, h.captures)
641 if r.Workspace != 43 || !r.OffScreen || r.Session != open.ID || r.Region.Dx() != 2 || r.Region.Dy() != 2 {
642 t.Fatalf("request %+v", r)
643 }
644 // Removing workspace 43 stops its session at once; the other lives on.
645 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
646 if g := nextSession(t, cc, ev); g[0] != evStopped {
647 t.Fatalf("event %v", g)
648 }
649 if c := nextEvent[ports.CaptureSessionClose](t, h.events); c.ID != open.ID {
650 t.Fatalf("close %+v", c)
651 }
652 noSession(t, cc, evOn)
653}
654
655// A handle that is already removed gives a source whose sessions stop at once.
656func TestWorkspaceSourceOfRemovedHandleStops(t *testing.T) {
657 h := newCaptureHarness(t)
658 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
659 cc := h.client(t)
660 handles := h.handles(t, cc, 1)
661 h.snapshot(t)
662 cc.roundtrip(t)
663 _, ev := cc.session(t, cc.workspaceSource(t, handles[0]))
664 if g := nextSession(t, cc, ev); g[0] != evStopped {
665 t.Fatalf("event %v", g)
666 }
667}
668
669// The workspace ID survives a configured-name change: same handle, same
670// decimal id, so a source made before stays valid.
671func TestWorkspaceSourceSurvivesConfiguredRename(t *testing.T) {
672 h := newCaptureHarness(t)
673 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
674 cc := h.client(t)
675 handles := h.handles(t, cc, 1)
676 _, ev := cc.session(t, cc.workspaceSource(t, handles[0]))
677 constraints(t, cc, ev)
678 h.open(t)
679 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "dev", Configured: "dev", Active: true, Frame: ports.Rect{W: 4, H: 4}})
680 h.settle(t, cc)
681 noSession(t, cc, ev)
682 select {
683 case e := <-h.events:
684 t.Fatalf("event %+v after a rename", e)
685 default:
686 }
687}
688
689// frameEvents decodes the events of a neferwl_workspace_frame_v1.
690type frameEvents struct {
691 wlturbo.BaseProxy
692 rects chan [4]int32
693}
694
695func (p *frameEvents) Dispatch(e *wlturbo.Event) {
696 p.rects <- [4]int32{e.Int32(), e.Int32(), e.Int32(), e.Int32()}
697}
698
699// workspaceFrame asks get_workspace_frame for a workspace handle.
700func (cc *cclient) workspaceFrame(t *testing.T, handle uint32) (uint32, chan [4]int32) {
701 t.Helper()
702 id := cc.c.AllocateID()
703 ev := &frameEvents{rects: make(chan [4]int32, 8)}
704 ev.SetID(id)
705 registerWireProxy(cc.c, ev)
706 requestProtocol(t, cc.c, cc.extra, imagecapture.NeferwlImageCaptureSourceManagerV1RequestGetWorkspaceFrame, id, handle)
707 cc.roundtrip(t)
708 return id, ev.rects
709}
710
711func wantFrame(t *testing.T, ch chan [4]int32, want [4]int32) {
712 t.Helper()
713 select {
714 case got := <-ch:
715 if got != want {
716 t.Fatalf("frame %v, want %v", got, want)
717 }
718 default:
719 t.Fatalf("no frame, want %v", want)
720 }
721}
722
723func noFrame(t *testing.T, ch chan [4]int32) {
724 t.Helper()
725 select {
726 case got := <-ch:
727 t.Fatalf("unexpected frame %v", got)
728 default:
729 }
730}
731
732// A workspace frame object reports the frame when it is created and each time
733// it changes, for every workspace, whether or not it is on screen; it reports
734// nothing for a removed handle and stops after a rename replaced the handle.
735func TestWorkspaceFrameEvents(t *testing.T) {
736 h := newCaptureHarness(t)
737 h.snapshot(t,
738 ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{X: 1, Y: 0, W: 2, H: 4}},
739 ports.WorkspaceInfo{ID: 43, Name: "2", Hidden: true, Index: 1, Frame: ports.Rect{W: 3, H: 2}},
740 )
741 cc := h.client(t)
742 handles := h.handles(t, cc, 2)
743 _, on := cc.workspaceFrame(t, handles[0])
744 wantFrame(t, on, [4]int32{1, 0, 2, 4})
745 _, off := cc.workspaceFrame(t, handles[1])
746 wantFrame(t, off, [4]int32{0, 0, 3, 2})
747 // Unchanged snapshot: nothing. A change: once, to the right object.
748 h.snapshot(t,
749 ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{X: 1, Y: 0, W: 2, H: 4}},
750 ports.WorkspaceInfo{ID: 43, Name: "2", Hidden: true, Index: 1, Frame: ports.Rect{X: 1, Y: 1, W: 2, H: 2}},
751 )
752 h.settle(t, cc)
753 noFrame(t, on)
754 wantFrame(t, off, [4]int32{1, 1, 2, 2})
755 noFrame(t, off)
756 // Removal sends nothing; a handle removed before the request sends nothing.
757 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{X: 1, Y: 0, W: 2, H: 4}})
758 h.settle(t, cc)
759 noFrame(t, off)
760 _, gone := cc.workspaceFrame(t, handles[1])
761 noFrame(t, gone)
762 // A configured rename replaces the handle: the old object goes quiet.
763 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "dev", Configured: "dev", Active: true, Frame: ports.Rect{W: 4, H: 4}})
764 h.settle(t, cc)
765 noFrame(t, on)
766}
767
768// While the session is locked a frame object sends nothing, whether it is
769// created before the lock or during it; the unlock sends the current frame
770// once, and only if it changed.
771func TestWorkspaceFrameSessionLock(t *testing.T) {
772 // unlock leaves protection the way the session-lock unlock does.
773 unlock := func(t *testing.T, h *captureHarness, state *atomic.Pointer[ports.SecurityState]) {
774 t.Helper()
775 protect(t, h.s, state, 2, false)
776 if !h.s.display.Do(func() { h.s.updateWorkspaceManagers(h.s.workspaceSnapshot) }) {
777 t.Fatal("display stopped")
778 }
779 }
780 info := func(r ports.Rect) ports.WorkspaceInfo {
781 return ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: r}
782 }
783 t.Run("change while locked", func(t *testing.T) {
784 h := newCaptureHarness(t)
785 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
786 cc := h.client(t)
787 handles := h.handles(t, cc, 1)
788 _, ev := cc.workspaceFrame(t, handles[0])
789 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
790 state := installSecurity(t, h.s)
791 protect(t, h.s, state, 1, true)
792 h.snapshot(t, info(ports.Rect{X: 1, Y: 1, W: 3, H: 3}))
793 h.settle(t, cc)
794 noFrame(t, ev)
795 unlock(t, h, state)
796 cc.roundtrip(t)
797 wantFrame(t, ev, [4]int32{1, 1, 3, 3})
798 noFrame(t, ev)
799 })
800 t.Run("no change while locked", func(t *testing.T) {
801 h := newCaptureHarness(t)
802 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
803 cc := h.client(t)
804 handles := h.handles(t, cc, 1)
805 _, ev := cc.workspaceFrame(t, handles[0])
806 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
807 state := installSecurity(t, h.s)
808 protect(t, h.s, state, 1, true)
809 h.snapshot(t, info(ports.Rect{X: 1, Y: 1, W: 3, H: 3}))
810 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
811 h.settle(t, cc)
812 unlock(t, h, state)
813 cc.roundtrip(t)
814 noFrame(t, ev)
815 })
816 t.Run("get while locked", func(t *testing.T) {
817 h := newCaptureHarness(t)
818 h.snapshot(t, info(ports.Rect{W: 2, H: 2}))
819 cc := h.client(t)
820 handles := h.handles(t, cc, 1)
821 state := installSecurity(t, h.s)
822 protect(t, h.s, state, 1, true)
823 _, ev := cc.workspaceFrame(t, handles[0])
824 h.settle(t, cc)
825 noFrame(t, ev)
826 unlock(t, h, state)
827 cc.roundtrip(t)
828 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
829 noFrame(t, ev)
830 })
831}
832
833// Destroying a frame object drops it from the server's list, and nothing is
834// sent to it afterwards.
835func TestWorkspaceFrameDestroyRemovesObject(t *testing.T) {
836 h := newCaptureHarness(t)
837 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 2, H: 2}})
838 cc := h.client(t)
839 handles := h.handles(t, cc, 1)
840 count := func() (n int) {
841 h.s.display.Do(func() { n = len(h.s.workspaceFrames) })
842 return n
843 }
844 id, ev := cc.workspaceFrame(t, handles[0])
845 wantFrame(t, ev, [4]int32{0, 0, 2, 2})
846 if n := count(); n != 1 {
847 t.Fatalf("%d frame objects, want 1", n)
848 }
849 requestProtocol(t, cc.c, id, imagecapture.NeferwlWorkspaceFrameV1RequestDestroy)
850 cc.roundtrip(t)
851 if n := count(); n != 0 {
852 t.Fatalf("%d frame objects after destroy, want 0", n)
853 }
854 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 3, H: 3}})
855 h.settle(t, cc)
856 noFrame(t, ev)
857}
858
859// exclusion asks get_exclusion on a session and returns the event stream.
860func (cc *cclient) exclusion(t *testing.T, session uint32) (uint32, chan sessionMsg) {
861 t.Helper()
862 id := cc.c.AllocateID()
863 ev := &exclusionEvents{msgs: make(chan sessionMsg, 8)}
864 ev.SetID(id)
865 registerWireProxy(cc.c, ev)
866 requestProtocol(t, cc.c, cc.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestGetExclusion, id, session)
867 cc.roundtrip(t)
868 return id, ev.msgs
869}
870
871// exclusionOwner opens a session and its exclusion, confirmed by core.
872func (h *captureHarness) exclusionOwner(t *testing.T) (*cclient, uint32, ports.CaptureSessionOpen, chan sessionMsg, string) {
873 t.Helper()
874 cc := h.client(t)
875 session, sev := cc.session(t, cc.outputSource(t))
876 constraints(t, cc, sev)
877 open := h.open(t)
878 _, msgs := cc.exclusion(t, session)
879 m := waitMsg(t, cc.c, msgs)
880 if uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventToken || len(m.token) != 64 || strings.Trim(m.token, "0123456789abcdef") != "" {
881 t.Fatalf("token %+v", m)
882 }
883 if b := nextEvent[ports.CaptureExclusionBegin](t, h.events); b.Session != open.ID {
884 t.Fatalf("begin %+v", b)
885 }
886 return cc, session, open, msgs, m.token
887}
888
889// The exclusion answers with a token; a second one is busy, even from
890// another connection, and is not reported to core.
891func TestExclusionTokenAndBusy(t *testing.T) {
892 h := newCaptureHarness(t)
893 _, _, open, _, _ := h.exclusionOwner(t)
894 other := h.client(t)
895 session2, sev2 := other.session(t, other.outputSource(t))
896 constraints(t, other, sev2)
897 h.open(t)
898 _, msgs := other.exclusion(t, session2)
899 if m := waitMsg(t, other.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventFailed || m.numbers[0] != int64(imagecapture.NeferwlCaptureExclusionV1FailureBusy) {
900 t.Fatalf("second exclusion %+v", m)
901 }
902 for len(h.events) > 0 {
903 if b, ok := (<-h.events).(ports.CaptureExclusionBegin); ok {
904 t.Fatalf("a refused exclusion reached core: %+v (owner %d)", b, open.ID)
905 }
906 }
907}
908
909// get_exclusion on a session that already stopped answers
910// failed(session_stopped), whatever the reason it stopped.
911func TestExclusionOnStoppedSession(t *testing.T) {
912 h := newCaptureHarness(t)
913 cc := h.client(t)
914 // Stopped at creation (a region with nothing on the output).
915 session, ev := cc.session(t, cc.regionSource(t, 100, 100, 5, 5))
916 if g := nextSession(t, cc, ev); g[0] != evStopped {
917 t.Fatalf("event %v", g)
918 }
919 _, msgs := cc.exclusion(t, session)
920 if m := waitMsg(t, cc.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventFailed || m.numbers[0] != int64(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped) {
921 t.Fatalf("exclusion %+v", m)
922 }
923 // Stopped by core after it was registered.
924 session2, sev := cc.session(t, cc.outputSource(t))
925 constraints(t, cc, sev)
926 open := h.open(t)
927 h.send(ports.CaptureSessionState{ID: open.ID, Reason: ports.CaptureReasonOutputGone})
928 if g := nextSession(t, cc, sev); g[0] != evStopped {
929 t.Fatalf("event %v", g)
930 }
931 _, msgs = cc.exclusion(t, session2)
932 if m := waitMsg(t, cc.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventFailed || m.numbers[0] != int64(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped) {
933 t.Fatalf("exclusion %+v", m)
934 }
935 for len(h.events) > 0 {
936 if b, ok := (<-h.events).(ports.CaptureExclusionBegin); ok {
937 t.Fatalf("a refused exclusion reached core: %+v", b)
938 }
939 }
940}
941
942// One session gets one live exclusion object.
943func TestExclusionAlreadyExcluded(t *testing.T) {
944 h := newCaptureHarness(t)
945 cc, session, _, _, _ := h.exclusionOwner(t)
946 requestProtocol(t, cc.c, cc.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestGetExclusion, cc.c.AllocateID(), session)
947 expectProtocolError(t, cc.c, cc.excl, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorAlreadyExcluded))
948}
949
950// Destroying the exclusion ends it in core, detaches its layers and frees
951// the slot; the session lives on.
952func TestExclusionDestroyFreesTheSlot(t *testing.T) {
953 h := newCaptureHarness(t)
954 cc, session, open, _, _ := h.exclusionOwner(t)
955 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 4, H: 4}, Active: true, Exclusion: true, Revision: 1})
956 h.settle(t, cc)
957 var excl uint32
958 h.s.display.Do(func() { excl = h.s.excl.res.ID() })
959 requestProtocol(t, cc.c, excl, imagecapture.NeferwlCaptureExclusionV1RequestDestroy)
960 cc.roundtrip(t)
961 if e := nextEvent[ports.CaptureExclusionEnd](t, h.events); e.Session != open.ID {
962 t.Fatalf("end %+v", e)
963 }
964 _, msgs := cc.exclusion(t, session)
965 if m := waitMsg(t, cc.c, msgs); uint32(m.op) != imagecapture.NeferwlCaptureExclusionV1EventToken {
966 t.Fatalf("new exclusion %+v", m)
967 }
968}
969
970// Frames of the session that owns the exclusion are fenced: held until core
971// knows the exclusion, then tagged with its revision. Every other capture,
972// wlr-screencopy included, carries no tag.
973func TestExclusionFrameTagging(t *testing.T) {
974 h := newCaptureHarness(t)
975 owner, session, open, _, _ := h.exclusionOwner(t)
976 // Core has not confirmed the exclusion: the frame waits, unserved.
977 ev := owner.liveExtFrame(t, session, 4, 4)
978 if eventsInclude(ev, frameFailed) || eventsInclude(ev, frameReady) {
979 t.Fatal("a frame before core confirmed the exclusion was answered")
980 }
981 noCapture(t, h.captures)
982 // Once core confirms it, the held frame is served, tagged.
983 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 4, H: 4}, Active: true, Exclusion: true, Revision: 9})
984 h.settle(t, owner)
985 r := receiveCapture(t, h.captures)
986 if !r.Exclude || r.Session != open.ID || r.CaptureRevision != 9 {
987 t.Fatalf("owner frame %+v", r)
988 }
989 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
990 // A wlr-screencopy capture, even of the owner's connection, is untagged.
991 owner.wlrFrame(t)
992 r = receiveCapture(t, h.captures)
993 if r.Exclude || r.Session != 0 || r.CaptureRevision != 0 || r.Workspace != 0 {
994 t.Fatalf("wlr capture tagged: %+v", r)
995 }
996 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
997 // So is a plain ext session of another client.
998 other := h.client(t)
999 session2, sev := other.session(t, other.outputSource(t))
1000 constraints(t, other, sev)
1001 h.open(t)
1002 other.extFrame(t, session2, 4, 4)
1003 r = receiveCapture(t, h.captures)
1004 if r.Exclude || r.CaptureRevision != 0 {
1005 t.Fatalf("another session's frame tagged: %+v", r)
1006 }
1007}
1008
1009// A transient renderer failure is asked again and served: failing the frame
1010// would end a portal screencast. A permanent one fails at once, unretried.
1011func TestExtFrameRendererFailure(t *testing.T) {
1012 for _, tc := range []struct {
1013 name string
1014 err error
1015 retry bool
1016 }{
1017 {"transient", fmt.Errorf("%w: indicator", ports.ErrCaptureTransient), true},
1018 {"permanent", errors.New("capture buffer truncated"), false},
1019 } {
1020 t.Run(tc.name, func(t *testing.T) {
1021 h := newCaptureHarness(t)
1022 cc := h.client(t)
1023 session, ev := cc.session(t, cc.outputSource(t))
1024 constraints(t, cc, ev)
1025 open := h.open(t)
1026 h.send(active(open.ID))
1027 h.settle(t, cc)
1028 frame := cc.liveExtFrame(t, session, 4, 4)
1029 r := receiveCapture(t, h.captures)
1030 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Err: tc.err}
1031 if tc.retry {
1032 r = receiveCapture(t, h.captures)
1033 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
1034 }
1035 for deadline := time.Now().Add(2 * time.Second); len(frame.events) == 0; time.Sleep(time.Millisecond) {
1036 cc.roundtrip(t)
1037 if time.Now().After(deadline) {
1038 t.Fatal("frame not answered")
1039 }
1040 }
1041 if failed := eventsInclude(frame, frameFailed); failed == tc.retry {
1042 t.Fatalf("failed = %v", failed)
1043 }
1044 if !tc.retry {
1045 noCapture(t, h.captures)
1046 }
1047 })
1048 }
1049}
1050
1051// A frame whose target stays unavailable fails once its retry deadline has
1052// passed, so a client is never left waiting.
1053func TestExtFrameRetryDeadline(t *testing.T) {
1054 var now atomic.Int64
1055 timers := make(chan func(), 4)
1056 clock := portsmocks.NewMockClock(t)
1057 clock.EXPECT().Now().RunAndReturn(func() time.Time { return time.Unix(0, now.Load()) }).Maybe()
1058 clock.EXPECT().AfterFunc(mock.Anything, mock.Anything).RunAndReturn(func(_ time.Duration, f func()) ports.Timer {
1059 timers <- f
1060 return portsmocks.NewMockTimer(t)
1061 }).Maybe()
1062 h := newCaptureHarnessWith(t, func(o *Options, _ *Channels) { o.Clock = clock })
1063 cc := h.client(t)
1064 session, ev := cc.session(t, cc.outputSource(t))
1065 constraints(t, cc, ev)
1066 open := h.open(t)
1067 h.send(active(open.ID))
1068 h.settle(t, cc)
1069 frame := cc.liveExtFrame(t, session, 4, 4)
1070 busy := fmt.Errorf("%w: busy", ports.ErrCaptureTransient)
1071 r := receiveCapture(t, h.captures)
1072 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Err: busy}
1073 var fire func()
1074 select {
1075 case fire = <-timers:
1076 case <-time.After(2 * time.Second):
1077 t.Fatal("no retry armed")
1078 }
1079 now.Store(int64(captureRetryFor))
1080 fire()
1081 r = receiveCapture(t, h.captures)
1082 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Err: busy}
1083 for deadline := time.Now().Add(2 * time.Second); len(frame.events) == 0; time.Sleep(time.Millisecond) {
1084 cc.roundtrip(t)
1085 if time.Now().After(deadline) {
1086 t.Fatal("frame not failed past its deadline")
1087 }
1088 }
1089 if !eventsInclude(frame, frameFailed) {
1090 t.Fatal("frame not failed past its deadline")
1091 }
1092 if len(timers) > 0 {
1093 t.Fatal("retried past its deadline")
1094 }
1095}
1096
1097// hud is a second connection: a layer surface it attaches with the token.
1098type hud struct {
1099 *cclient
1100 surface, layer uint32
1101}
1102
1103func (h *captureHarness) hud(t *testing.T, layer ports.Layer, keyboard uint32) *hud {
1104 t.Helper()
1105 return h.hudVersion(t, layer, keyboard, 1)
1106}
1107
1108func (h *captureHarness) hudVersion(t *testing.T, layer ports.Layer, keyboard, shellVersion uint32) *hud {
1109 t.Helper()
1110 pc := h.client(t)
1111 comp := bindProtocol(t, pc.c, "wl_compositor")
1112 shell := bindVersion(t, pc.c, "zwlr_layer_shell_v1", shellVersion)
1113 surf, l := pc.c.AllocateID(), pc.c.AllocateID()
1114 requestProtocol(t, pc.c, comp, wayland.CompositorRequestCreateSurface, surf)
1115 registerProtocol(t, pc.c, surf)
1116 requestProtocol(t, pc.c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, l, surf, pc.output, uint32(layer), "hud")
1117 registerProtocol(t, pc.c, l)
1118 if keyboard != 0 {
1119 requestProtocol(t, pc.c, l, wlrlayershell.ZwlrLayerSurfaceV1RequestSetKeyboardInteractivity, keyboard)
1120 }
1121 if err := pc.c.Roundtrip(); err != nil {
1122 t.Fatal(err)
1123 }
1124 return &hud{cclient: pc, surface: surf, layer: l}
1125}
1126
1127// attach sends attach_surface and returns the attachment's event stream.
1128func (hd *hud) attach(t *testing.T, token string) chan sessionMsg {
1129 t.Helper()
1130 return hd.attachSurface(t, token, hd.surface)
1131}
1132
1133func (hd *hud) attachSurface(t *testing.T, token string, surface uint32) chan sessionMsg {
1134 t.Helper()
1135 id := hd.c.AllocateID()
1136 ev := &attachEvents{msgs: make(chan sessionMsg, 8)}
1137 ev.SetID(id)
1138 registerWireProxy(hd.c, ev)
1139 requestProtocol(t, hd.c, hd.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestAttachSurface, id, token, surface)
1140 if err := hd.c.Roundtrip(); err != nil {
1141 t.Fatal(err)
1142 }
1143 return ev.msgs
1144}
1145
1146// expectAttachment waits for one attachment event of the given opcode and first argument.
1147func expectAttachment(t *testing.T, c *wlturbo.Display, ch chan sessionMsg, op uint32, arg ...int64) {
1148 t.Helper()
1149 m := waitMsg(t, c, ch)
1150 if uint32(m.op) != op || (len(arg) > 0 && m.numbers[0] != arg[0]) {
1151 t.Fatalf("attachment event %+v, want op %d %v", m, op, arg)
1152 }
1153}
1154
1155// waitMsg waits for a message on ch, dispatching the client meanwhile.
1156func waitMsg(t *testing.T, c *wlturbo.Display, ch chan sessionMsg) sessionMsg {
1157 t.Helper()
1158 deadline := time.Now().Add(2 * time.Second)
1159 for time.Now().Before(deadline) {
1160 select {
1161 case m := <-ch:
1162 return m
1163 default:
1164 }
1165 if err := c.Roundtrip(); err != nil {
1166 t.Fatal(err)
1167 }
1168 time.Sleep(time.Millisecond)
1169 }
1170 t.Fatal("timed out waiting for an event")
1171 return sessionMsg{}
1172}
1173
1174func noMessage(t *testing.T, c *wlturbo.Display, ch chan sessionMsg) {
1175 t.Helper()
1176 if err := c.Roundtrip(); err != nil {
1177 t.Fatal(err)
1178 }
1179 select {
1180 case m := <-ch:
1181 t.Fatalf("unexpected event %+v", m)
1182 default:
1183 }
1184}
1185
1186func nextEvent[T ports.ClientEvent](t *testing.T, ch chan ports.ClientEvent) T {
1187 t.Helper()
1188 for {
1189 select {
1190 case ev := <-ch:
1191 if v, ok := ev.(T); ok {
1192 return v
1193 }
1194 case <-time.After(2 * time.Second):
1195 t.Fatal("timed out waiting for a client event")
1196 }
1197 }
1198}
1199
1200// Attach works from another connection with the token, reaches core before
1201// any content, and is confirmed to the client only once core lists the layer.
1202func TestExclusionAttach(t *testing.T) {
1203 h := newCaptureHarness(t)
1204 _, _, open, _, token := h.exclusionOwner(t)
1205 hd := h.hud(t, ports.LayerOverlay, 2)
1206 // A wrong token fails that attachment without touching the exclusion.
1207 bad := hd.attach(t, strings.Repeat("0", 64))
1208 expectAttachment(t, hd.c, bad, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureUnknownToken))
1209 good := hd.attach(t, token)
1210 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1211 if layer.Session != open.ID || !layer.Attached || layer.Layer == 0 {
1212 t.Fatalf("layer %+v", layer)
1213 }
1214 noMessage(t, hd.c, good) // not confirmed yet
1215 st := active(open.ID)
1216 st.Exclusion, st.Revision = true, 2
1217 h.send(st)
1218 noMessage(t, hd.c, good) // a state that does not list the layer confirms nothing
1219 st.Revision, st.Layers = 3, []ports.WindowID{layer.Layer}
1220 h.send(st)
1221 expectAttachment(t, hd.c, good, imagecapture.NeferwlCaptureLayerV1EventAttached)
1222 // Attaching the same surface again fails on its own object.
1223 again := hd.attach(t, token)
1224 expectAttachment(t, hd.c, again, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureAlreadyAttached))
1225 // Destroying the layer detaches it: LayerChanged (the unmap) first, then
1226 // the detach, on the same ordered channel.
1227 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
1228 if err := hd.c.Roundtrip(); err != nil {
1229 t.Fatal(err)
1230 }
1231 expectAttachment(t, hd.c, good, imagecapture.NeferwlCaptureLayerV1EventDetached, int64(imagecapture.NeferwlCaptureLayerV1DetachReasonLayerDestroyed))
1232 if l := nextEvent[ports.CaptureExclusionLayer](t, h.events); l.Attached || l.Layer != layer.Layer {
1233 t.Fatalf("detach %+v", l)
1234 }
1235}
1236
1237// Once the exclusion ended the token is dead.
1238func TestExclusionTokenDeadAfterEnd(t *testing.T) {
1239 h := newCaptureHarness(t)
1240 owner, session, _, _, token := h.exclusionOwner(t)
1241 requestProtocol(t, owner.c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
1242 owner.roundtrip(t)
1243 hd := h.hud(t, ports.LayerOverlay, 0)
1244 att := hd.attach(t, token)
1245 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureUnknownToken))
1246}
1247
1248// Order on the event channel: a mapped attached layer that is destroyed
1249// queues its LayerChanged removal before the detach reaches core.
1250func TestExclusionDetachOrdering(t *testing.T) {
1251 h := newCaptureHarness(t)
1252 _, _, open, _, token := h.exclusionOwner(t)
1253 hd := h.hud(t, ports.LayerTop, 0)
1254 att := hd.attach(t, token)
1255 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1256 st := active(open.ID)
1257 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1258 h.send(st)
1259 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventAttached)
1260 mapLayer(t, hd)
1261 if l := nextEvent[ports.LayerChanged](t, h.events); len(l.Layers) != 1 {
1262 t.Fatalf("mapped %+v", l)
1263 }
1264 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
1265 if err := hd.c.Roundtrip(); err != nil {
1266 t.Fatal(err)
1267 }
1268 var order []string
1269 for len(order) < 2 {
1270 switch v := (<-h.events).(type) {
1271 case ports.LayerChanged:
1272 if len(v.Layers) == 0 {
1273 order = append(order, "unmap")
1274 }
1275 case ports.CaptureExclusionLayer:
1276 if !v.Attached {
1277 order = append(order, "detach")
1278 }
1279 }
1280 }
1281 if order[0] != "unmap" || order[1] != "detach" {
1282 t.Fatalf("order %v: the exclusion must outlive the layer's last frame", order)
1283 }
1284}
1285
1286// mapLayer configures, acks and commits a buffer on the hud's layer.
1287func mapLayer(t *testing.T, hd *hud) {
1288 t.Helper()
1289 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
1290 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestCommit)
1291 cfg := &layerProxy{configured: make(chan [3]uint32, 4)}
1292 cfg.SetID(hd.layer)
1293 registerWireProxy(hd.c, cfg)
1294 if err := hd.c.Roundtrip(); err != nil {
1295 t.Fatal(err)
1296 }
1297 c := <-cfg.configured
1298 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, c[0])
1299 _, buf, fd := captureSmallBuffer(t, hd.c)
1300 t.Cleanup(func() { _ = unix.Close(fd) })
1301 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1302 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestCommit)
1303 if err := hd.c.Roundtrip(); err != nil {
1304 t.Fatal(err)
1305 }
1306}
1307
1308// Ending the exclusion (here with its session) detaches every attachment,
1309// closes the layer surfaces (so none lingers into the next exclusion) and
1310// removes them before core is told the session closed.
1311func TestExclusionEndClosesLayers(t *testing.T) {
1312 h := newCaptureHarness(t)
1313 owner, session, open, _, token := h.exclusionOwner(t)
1314 hd := h.hud(t, ports.LayerOverlay, 0)
1315 closed := make(chan sessionMsg, 4)
1316 lp := &layerClosedProxy{msgs: closed}
1317 lp.SetID(hd.layer)
1318 registerWireProxy(hd.c, lp)
1319 confirmed := hd.attach(t, token)
1320 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1321 st := active(open.ID)
1322 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1323 h.send(st)
1324 expectAttachment(t, hd.c, confirmed, imagecapture.NeferwlCaptureLayerV1EventAttached)
1325 pending := hd.attachSurfaceNew(t, token)
1326 requestProtocol(t, owner.c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
1327 owner.roundtrip(t)
1328 expectAttachment(t, hd.c, confirmed, imagecapture.NeferwlCaptureLayerV1EventDetached, int64(imagecapture.NeferwlCaptureLayerV1DetachReasonExclusionEnded))
1329 expectAttachment(t, hd.c, pending, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureExclusionEnded))
1330 if m := waitMsg(t, hd.c, closed); uint32(m.op) != uint32(wlrlayershell.ZwlrLayerSurfaceV1EventClosed) {
1331 t.Fatalf("layer event %+v", m)
1332 }
1333 if e := nextEvent[ports.CaptureSessionClose](t, h.events); e.ID != open.ID {
1334 t.Fatalf("close %+v", e)
1335 }
1336}
1337
1338type layerClosedProxy struct {
1339 wlturbo.BaseProxy
1340 msgs chan sessionMsg
1341}
1342
1343func (p *layerClosedProxy) Dispatch(e *wlturbo.Event) {
1344 if e.Opcode == uint16(wlrlayershell.ZwlrLayerSurfaceV1EventClosed) {
1345 p.msgs <- sessionMsg{op: e.Opcode}
1346 }
1347}
1348
1349// attachSurfaceNew attaches a second, fresh layer surface of the hud and returns its stream.
1350func (hd *hud) attachSurfaceNew(t *testing.T, token string) chan sessionMsg {
1351 t.Helper()
1352 comp := bindProtocol(t, hd.c, "wl_compositor")
1353 shell := bindProtocol(t, hd.c, "zwlr_layer_shell_v1")
1354 surf, l := hd.c.AllocateID(), hd.c.AllocateID()
1355 requestProtocol(t, hd.c, comp, wayland.CompositorRequestCreateSurface, surf)
1356 registerProtocol(t, hd.c, surf)
1357 requestProtocol(t, hd.c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, l, surf, hd.output, uint32(ports.LayerTop), "hud2")
1358 registerProtocol(t, hd.c, l)
1359 return hd.attachSurface(t, token, surf)
1360}
1361
1362func TestExclusionAttachRefusals(t *testing.T) {
1363 for _, tc := range []struct {
1364 name string
1365 layer ports.Layer
1366 keyboard uint32
1367 code uint32
1368 }{
1369 {"background layer", ports.LayerBackground, 0, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidLayer)},
1370 {"bottom layer", ports.LayerBottom, 0, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidLayer)},
1371 {"exclusive keyboard", ports.LayerTop, 1, uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorExclusiveKeyboard)},
1372 } {
1373 t.Run(tc.name, func(t *testing.T) {
1374 h := newCaptureHarness(t)
1375 _, _, _, _, token := h.exclusionOwner(t)
1376 hd := h.hud(t, tc.layer, tc.keyboard)
1377 requestProtocol(t, hd.c, hd.excl, imagecapture.NeferwlCaptureExclusionManagerV1RequestAttachSurface, hd.c.AllocateID(), token, hd.surface)
1378 expectProtocolError(t, hd.c, hd.excl, tc.code)
1379 for len(h.events) > 0 {
1380 if l, ok := (<-h.events).(ports.CaptureExclusionLayer); ok {
1381 t.Fatalf("a refused attach reached core: %+v", l)
1382 }
1383 }
1384 })
1385 }
1386}
1387
1388func TestExclusionAttachLimit(t *testing.T) {
1389 h := newCaptureHarness(t)
1390 _, _, _, _, token := h.exclusionOwner(t)
1391 hd := h.hud(t, ports.LayerTop, 0)
1392 hd.attach(t, token)
1393 for i := 1; i < ports.MaxExclusionLayers; i++ {
1394 hd.attachSurfaceNew(t, token)
1395 }
1396 over := hd.attachSurfaceNew(t, token)
1397 expectAttachment(t, hd.c, over, imagecapture.NeferwlCaptureLayerV1EventFailed, int64(imagecapture.NeferwlCaptureLayerV1FailureTooManyLayers))
1398 // Events reach core through a forwarding goroutine: wait for each.
1399 for range ports.MaxExclusionLayers {
1400 if l := nextEvent[ports.CaptureExclusionLayer](t, h.events); !l.Attached {
1401 t.Fatalf("detach %+v", l)
1402 }
1403 }
1404 // The refused one never follows: it was refused before the failed event
1405 // the client already read, so a short wait covers the forwarder.
1406 deadline := time.After(50 * time.Millisecond)
1407 for {
1408 select {
1409 case ev := <-h.events:
1410 if l, ok := ev.(ports.CaptureExclusionLayer); ok && l.Attached {
1411 t.Fatalf("layer over the limit reached core: %+v", l)
1412 }
1413 case <-deadline:
1414 return
1415 }
1416 }
1417}
1418
1419// An attached layer that commits a layer below top is closed and detached
1420// once, and its unmap reaches core before the detach.
1421func TestExclusionLayerMovedBelowTop(t *testing.T) {
1422 h := newCaptureHarness(t)
1423 _, _, open, _, token := h.exclusionOwner(t)
1424 hd := h.hudVersion(t, ports.LayerTop, 0, 2)
1425 att := hd.attach(t, token)
1426 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1427 st := active(open.ID)
1428 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1429 h.send(st)
1430 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventAttached)
1431 mapLayer(t, hd)
1432 if l := nextEvent[ports.LayerChanged](t, h.events); len(l.Layers) != 1 {
1433 t.Fatalf("mapped %+v", l)
1434 }
1435 closed := make(chan sessionMsg, 4)
1436 lp := &layerClosedProxy{msgs: closed}
1437 lp.SetID(hd.layer)
1438 registerWireProxy(hd.c, lp)
1439 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetLayer, uint32(ports.LayerBottom))
1440 requestProtocol(t, hd.c, hd.surface, wayland.SurfaceRequestCommit)
1441 if err := hd.c.Roundtrip(); err != nil {
1442 t.Fatal(err)
1443 }
1444 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventDetached, int64(imagecapture.NeferwlCaptureLayerV1DetachReasonLayerDestroyed))
1445 if m := waitMsg(t, hd.c, closed); uint32(m.op) != uint32(wlrlayershell.ZwlrLayerSurfaceV1EventClosed) {
1446 t.Fatalf("layer event %+v", m)
1447 }
1448 noMessage(t, hd.c, att) // detached exactly once
1449 var order []string
1450 for len(order) < 2 {
1451 switch v := (<-h.events).(type) {
1452 case ports.LayerChanged:
1453 if len(v.Layers) == 0 {
1454 order = append(order, "unmap")
1455 }
1456 case ports.CaptureExclusionLayer:
1457 if !v.Attached {
1458 order = append(order, "detach")
1459 }
1460 }
1461 }
1462 if order[0] != "unmap" || order[1] != "detach" {
1463 t.Fatalf("order %v", order)
1464 }
1465}
1466
1467// An attached layer asking for keyboard interactivity, before or after the
1468// attach, is never reported with it to core.
1469func TestExclusionLayerKeyboardNone(t *testing.T) {
1470 h := newCaptureHarness(t)
1471 _, _, open, _, token := h.exclusionOwner(t)
1472 hd := h.hud(t, ports.LayerOverlay, 2) // on demand before the attach
1473 att := hd.attach(t, token)
1474 layer := nextEvent[ports.CaptureExclusionLayer](t, h.events)
1475 st := active(open.ID)
1476 st.Exclusion, st.Revision, st.Layers = true, 2, []ports.WindowID{layer.Layer}
1477 h.send(st)
1478 expectAttachment(t, hd.c, att, imagecapture.NeferwlCaptureLayerV1EventAttached)
1479 requestProtocol(t, hd.c, hd.layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetKeyboardInteractivity, uint32(2)) // and after
1480 mapLayer(t, hd)
1481 l := nextEvent[ports.LayerChanged](t, h.events)
1482 if len(l.Layers) != 1 || l.Layers[0].Keyboard != 0 {
1483 t.Fatalf("layer reported with keyboard: %+v", l.Layers)
1484 }
1485}
1486
1487// frameTaken waits for the next CaptureFrameTaken core is told of.
1488func (h *captureHarness) frameTaken(t *testing.T) ports.CaptureFrameTaken {
1489 t.Helper()
1490 return nextEvent[ports.CaptureFrameTaken](t, h.events)
1491}
1492
1493// Every capture is reported to core, which shows the indicator for it: a
1494// wlr-screencopy frame (no session) and a frame of each kind of ext source,
1495// with the target the client asked for.
1496func TestEveryCaptureFrameIsReportedToCore(t *testing.T) {
1497 h := newCaptureHarness(t)
1498 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
1499 cc := h.client(t)
1500 // wlr-screencopy, whole output.
1501 cc.wlrFrame(t)
1502 r := receiveCapture(t, h.captures)
1503 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Output: "HEADLESS-1"}) {
1504 t.Fatalf("wlr frame reported %+v", got)
1505 }
1506 h.captured <- ports.CaptureDone{ID: r.ID, Output: r.Output, Time: time.Now()}
1507 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Output: "HEADLESS-1"}) {
1508 t.Fatalf("wlr completion reported %+v", got)
1509 }
1510 // ext output source.
1511 session, ev := cc.session(t, cc.outputSource(t))
1512 constraints(t, cc, ev)
1513 open := h.open(t)
1514 cc.extFrame(t, session, 4, 4)
1515 receiveCapture(t, h.captures)
1516 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Session: open.ID, Output: "HEADLESS-1"}) {
1517 t.Fatalf("ext output frame reported %+v", got)
1518 }
1519 // ext region source: the logical rectangle as asked.
1520 session, ev = cc.session(t, cc.regionSource(t, 1, 1, 2, 2))
1521 constraints(t, cc, ev)
1522 open = h.open(t)
1523 cc.extFrame(t, session, 2, 2)
1524 receiveCapture(t, h.captures)
1525 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Session: open.ID, Output: "HEADLESS-1", Region: ports.Rect{X: 1, Y: 1, W: 2, H: 2}}) {
1526 t.Fatalf("ext region frame reported %+v", got)
1527 }
1528 // ext workspace source.
1529 handles := h.handles(t, cc, 1)
1530 session, ev = cc.session(t, cc.workspaceSource(t, handles[0]))
1531 constraints(t, cc, ev)
1532 open = h.open(t)
1533 cc.extFrame(t, session, 4, 4)
1534 receiveCapture(t, h.captures)
1535 if got := h.frameTaken(t); got != (ports.CaptureFrameTaken{Session: open.ID, Workspace: 42}) {
1536 t.Fatalf("ext workspace frame reported %+v", got)
1537 }
1538}
1539
1540// Every capture request asks the output owner to hold it until the indicator
1541// is on screen, whatever protocol or source made it.
1542func TestEveryCaptureRequestAsksForItsIndicator(t *testing.T) {
1543 h := newCaptureHarness(t)
1544 h.snapshot(t, ports.WorkspaceInfo{ID: 42, Name: "1", Active: true, Frame: ports.Rect{W: 4, H: 4}})
1545 cc := h.client(t)
1546 cc.wlrFrame(t)
1547 if r := receiveCapture(t, h.captures); !r.Indicate {
1548 t.Fatalf("wlr request %+v", r)
1549 }
1550 session, ev := cc.session(t, cc.outputSource(t))
1551 constraints(t, cc, ev)
1552 h.open(t)
1553 cc.extFrame(t, session, 4, 4)
1554 if r := receiveCapture(t, h.captures); !r.Indicate {
1555 t.Fatalf("ext output request %+v", r)
1556 }
1557 handles := h.handles(t, cc, 1)
1558 session, ev = cc.session(t, cc.workspaceSource(t, handles[0]))
1559 constraints(t, cc, ev)
1560 h.open(t)
1561 cc.extFrame(t, session, 4, 4)
1562 if r := receiveCapture(t, h.captures); !r.Indicate || r.Workspace != 42 {
1563 t.Fatalf("ext workspace request %+v", r)
1564 }
1565}
1566
1567// A capture the compositor refuses is not reported: nothing was captured.
1568func TestRefusedCaptureIsNotReported(t *testing.T) {
1569 h := newCaptureHarness(t)
1570 cc := h.client(t)
1571 session, ev := cc.session(t, cc.outputSource(t))
1572 constraints(t, cc, ev)
1573 h.open(t)
1574 // A wrong-sized buffer is refused before it reaches the renderer.
1575 cc.extFrame(t, session, 2, 2)
1576 noCapture(t, h.captures)
1577 h.settle(t, cc)
1578 for len(h.events) > 0 {
1579 if v, ok := (<-h.events).(ports.CaptureFrameTaken); ok {
1580 t.Fatalf("refused capture reported: %+v", v)
1581 }
1582 }
1583}
1584
1585// logicalRegion gives core the logical rectangle of a physical region.
1586func TestLogicalRegion(t *testing.T) {
1587 o := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Width: 8, Height: 8}, Scale: 2}}
1588 for _, tc := range []struct {
1589 phys image.Rectangle
1590 want ports.Rect
1591 }{
1592 {image.Rect(0, 0, 8, 8), ports.Rect{}},
1593 {image.Rect(2, 2, 6, 4), ports.Rect{X: 1, Y: 1, W: 2, H: 1}},
1594 {image.Rect(1, 1, 5, 3), ports.Rect{X: 0, Y: 0, W: 3, H: 2}}, // rounded outwards
1595 } {
1596 if got := logicalRegion(o, tc.phys); got != tc.want {
1597 t.Errorf("%v: %+v, want %+v", tc.phys, got, tc.want)
1598 }
1599 }
1600}