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