detached 7edbc98

test(wayland): synchronize screencast lifecycle assertions

Apply output removal on the display owner before completing its capture. Wait for each inventory-test frame completion before lending another buffer, accepting interleaved damage events. Bound cast harness shutdown so a stuck server fails promptly.
commit
7edbc98b0eae899556454f0908ecbef69117f5d7
author
brice · · 2026-10-19 18:41 UTC
parent
353b4e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +40 -2
M internal/adapters/wayland/screencast_test.go +40 -2
@@ -76,7 +76,14 @@ func newCastHarness(t *testing.T, withSink bool) *castHarness {
7676 ctx, cancel := context.WithCancel(context.Background())
7777 done := make(chan error, 1)
7878 go func() { done <- s.Run(ctx) }()
79- t.Cleanup(func() { cancel(); <-done })
79+ t.Cleanup(func() {
80+ cancel()
81+ select {
82+ case <-done:
83+ case <-time.After(3 * time.Second):
84+ t.Fatal("server did not stop")
85+ }
86+ })
8087 return h
8188 }
8289
@@ -107,6 +114,26 @@ func (h *castHarness) next(kind ports.CastEventKind) ports.CastEvent {
107114 }
108115 }
109116
117+func (h *castHarness) nextCastFrameDone() ports.CastEvent {
118+ h.t.Helper()
119+ timer := time.NewTimer(3 * time.Second)
120+ defer timer.Stop()
121+ for {
122+ select {
123+ case ev := <-h.got:
124+ if ev.Kind == ports.CastFrameDone {
125+ return ev
126+ }
127+ if ev.Kind != ports.CastDamage {
128+ h.t.Fatalf("unexpected event %+v", ev)
129+ }
130+ case <-timer.C:
131+ h.t.Fatal("no frame completion")
132+ return ports.CastEvent{}
133+ }
134+ }
135+}
136+
110137 // none says that no event is pending once the display goroutine caught up.
111138 func (h *castHarness) none() {
112139 h.t.Helper()
@@ -331,7 +358,13 @@ func TestCastOutputUnplugStops(t *testing.T) {
331358 data := make([]byte, 64*32*4)
332359 h.casts <- castFrame(1, 9, data, 64, 32)
333360 req := h.request()
334- h.commands <- ports.SetOutputs{Outputs: castLayout[1:], Focused: "HEADLESS-2"}
361+ // Apply on the owner before completing the capture. Posting a barrier
362+ // does not fence the independent Commands forwarding goroutine.
363+ if !h.s.display.Do(func() {
364+ h.s.apply(ports.SetOutputs{Outputs: castLayout[1:], Focused: "HEADLESS-2"})
365+ }) {
366+ t.Fatal("display stopped")
367+ }
335368 h.none()
336369 h.complete(req, nil)
337370 if done := h.next(ports.CastFrameDone); done.ID != 1 || done.Buf != 9 || done.Err == nil {
@@ -615,6 +648,11 @@ func TestCastInventoryPublishedOncePerChange(t *testing.T) {
615648 h.flip("HEADLESS-1")
616649 h.casts <- castFrame(1, i%3, data, 64, 32)
617650 h.complete(h.request(), nil)
651+ // Flips may also publish damage; settle this buffer before lending
652+ // the next one, independently of the reply-forwarding goroutine.
653+ if ev := h.nextCastFrameDone(); ev.Err != nil {
654+ t.Fatalf("frame %d: %+v", i, ev)
655+ }
618656 }
619657 noInv()
620658
@@ -76,7 +76,14 @@ func newCastHarness(t *testing.T, withSink bool) *castHarness {
76 ctx, cancel := context.WithCancel(context.Background())76 ctx, cancel := context.WithCancel(context.Background())
77 done := make(chan error, 1)77 done := make(chan error, 1)
78 go func() { done <- s.Run(ctx) }()78 go func() { done <- s.Run(ctx) }()
79- t.Cleanup(func() { cancel(); <-done })79+ t.Cleanup(func() {
80+ cancel()
81+ select {
82+ case <-done:
83+ case <-time.After(3 * time.Second):
84+ t.Fatal("server did not stop")
85+ }
86+ })
80 return h87 return h
81 }88 }
82 89
@@ -107,6 +114,26 @@ func (h *castHarness) next(kind ports.CastEventKind) ports.CastEvent {
107 }114 }
108 }115 }
109 116
117+func (h *castHarness) nextCastFrameDone() ports.CastEvent {
118+ h.t.Helper()
119+ timer := time.NewTimer(3 * time.Second)
120+ defer timer.Stop()
121+ for {
122+ select {
123+ case ev := <-h.got:
124+ if ev.Kind == ports.CastFrameDone {
125+ return ev
126+ }
127+ if ev.Kind != ports.CastDamage {
128+ h.t.Fatalf("unexpected event %+v", ev)
129+ }
130+ case <-timer.C:
131+ h.t.Fatal("no frame completion")
132+ return ports.CastEvent{}
133+ }
134+ }
135+}
136+
110 // none says that no event is pending once the display goroutine caught up.137 // none says that no event is pending once the display goroutine caught up.
111 func (h *castHarness) none() {138 func (h *castHarness) none() {
112 h.t.Helper()139 h.t.Helper()
@@ -331,7 +358,13 @@ func TestCastOutputUnplugStops(t *testing.T) {
331 data := make([]byte, 64*32*4)358 data := make([]byte, 64*32*4)
332 h.casts <- castFrame(1, 9, data, 64, 32)359 h.casts <- castFrame(1, 9, data, 64, 32)
333 req := h.request()360 req := h.request()
334- h.commands <- ports.SetOutputs{Outputs: castLayout[1:], Focused: "HEADLESS-2"}361+ // Apply on the owner before completing the capture. Posting a barrier
362+ // does not fence the independent Commands forwarding goroutine.
363+ if !h.s.display.Do(func() {
364+ h.s.apply(ports.SetOutputs{Outputs: castLayout[1:], Focused: "HEADLESS-2"})
365+ }) {
366+ t.Fatal("display stopped")
367+ }
335 h.none()368 h.none()
336 h.complete(req, nil)369 h.complete(req, nil)
337 if done := h.next(ports.CastFrameDone); done.ID != 1 || done.Buf != 9 || done.Err == nil {370 if done := h.next(ports.CastFrameDone); done.ID != 1 || done.Buf != 9 || done.Err == nil {
@@ -615,6 +648,11 @@ func TestCastInventoryPublishedOncePerChange(t *testing.T) {
615 h.flip("HEADLESS-1")648 h.flip("HEADLESS-1")
616 h.casts <- castFrame(1, i%3, data, 64, 32)649 h.casts <- castFrame(1, i%3, data, 64, 32)
617 h.complete(h.request(), nil)650 h.complete(h.request(), nil)
651+ // Flips may also publish damage; settle this buffer before lending
652+ // the next one, independently of the reply-forwarding goroutine.
653+ if ev := h.nextCastFrameDone(); ev.Err != nil {
654+ t.Fatalf("frame %d: %+v", i, ev)
655+ }
618 }656 }
619 noInv()657 noInv()
620 658

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close