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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 76 | 76 | ctx, cancel := context.WithCancel(context.Background()) | |
| 77 | 77 | done := make(chan error, 1) | |
| 78 | 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 | 87 | 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 | 137 | // none says that no event is pending once the display goroutine caught up. | |
| 111 | 138 | func (h *castHarness) none() { | |
| 112 | 139 | h.t.Helper() | |
| @@ -331,7 +358,13 @@ func TestCastOutputUnplugStops(t *testing.T) { | |||
| 331 | 358 | data := make([]byte, 64*32*4) | |
| 332 | 359 | h.casts <- castFrame(1, 9, data, 64, 32) | |
| 333 | 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 | 368 | h.none() | |
| 336 | 369 | h.complete(req, nil) | |
| 337 | 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 | 648 | h.flip("HEADLESS-1") | |
| 616 | 649 | h.casts <- castFrame(1, i%3, data, 64, 32) | |
| 617 | 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 | 657 | noInv() | |
| 620 | 658 | ||
| @@ -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 h | 87 | 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 | ||||