1package daemon
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/require"
8)
9
10func TestAfterFrameQueueReleasesOnlyAfterALaterCapture(t *testing.T) {
11 tests := []struct {
12 name string
13 steps []string // "add:<id>", "capture", "emit:<n>"
14 wantRan []string
15 }{
16 {name: "nothing emitted", steps: []string{"add:a", "capture"}},
17 {name: "capture started before registration", steps: []string{"capture", "add:a", "capture", "emit:1"}},
18 {name: "capture started after registration", steps: []string{"capture", "add:a", "capture", "emit:2"}, wantRan: []string{"a"}},
19 {name: "unnumbered emission", steps: []string{"add:a", "capture", "emit:0"}},
20 {name: "partial release keeps later work", steps: []string{"add:a", "capture", "add:b", "capture", "emit:1"}, wantRan: []string{"a"}},
21 {name: "later emission releases the rest", steps: []string{"add:a", "capture", "add:b", "capture", "emit:1", "emit:2"}, wantRan: []string{"a", "b"}},
22 }
23 for _, tt := range tests {
24 t.Run(tt.name, func(t *testing.T) {
25 var q afterFrameQueue
26 ran := make(chan string, len(tt.steps))
27 for _, step := range tt.steps {
28 switch {
29 case step == "capture":
30 q.beginCapture()
31 case step[:4] == "add:":
32 id := step[4:]
33 q.add(func() { ran <- id })
34 case step[:5] == "emit:":
35 q.emitted(uint64(step[5] - '0'))
36 }
37 }
38 q.runReady()
39 q.runReady()
40 var got []string
41 for range tt.wantRan {
42 got = append(got, awaitTestValue(t, ran, "released work did not run"))
43 }
44 require.ElementsMatch(t, tt.wantRan, got)
45 select {
46 case extra := <-ran:
47 t.Fatalf("unexpected work %q ran", extra)
48 case <-time.After(50 * time.Millisecond):
49 }
50 })
51 }
52}
53
54// Work registered through afterNextFrame runs once a later capture is emitted
55// on the same attachment, and is dropped once the attachment reconnected.
56func TestAfterNextFrameRunsOrDropsByCapability(t *testing.T) {
57 tests := []struct {
58 name string
59 reconnect bool
60 freeze bool
61 wantRun bool
62 }{
63 {name: "same attachment runs", wantRun: true},
64 {name: "reconnected attachment drops", reconnect: true},
65 {name: "transient freeze delays until thaw", freeze: true, wantRun: true},
66 }
67 for _, tt := range tests {
68 t.Run(tt.name, func(t *testing.T) {
69 d, source, ac, _, _, releases, _ := setupMovePickerSessionsCore(t, stubClock{}, 0)
70 defer releaseAll(releases)
71 effect, admitted := ac.beginAttachmentEffect(captureAttachmentCapability(source, ac, ac.transport()))
72 require.True(t, admitted)
73 ran := make(chan struct{}, 1)
74 d.queueAfterNextFrame(ac, effect, func(*attachmentEffect) { ran <- struct{}{} })
75 effect.End()
76 if tt.reconnect {
77 ac.replaceTransport(ac.transport())
78 ac.installTestAttachmentCapability(captureAttachmentCapability(source, ac, ac.transport()))
79 }
80 var frozen attachmentTransitionGuard
81 if tt.freeze {
82 frozen = freezeAttachmentEffectGates(ac)
83 require.True(t, frozen.acquired)
84 }
85 ac.afterFrame.emitted(ac.afterFrame.beginCapture())
86 ac.afterFrame.runReady()
87 if tt.freeze {
88 // Released while frozen: it waits, with no further paint needed.
89 select {
90 case <-ran:
91 t.Fatal("work ran while the attachment was frozen")
92 case <-time.After(50 * time.Millisecond):
93 }
94 frozen.unfreeze()
95 }
96 if tt.wantRun {
97 awaitTestValue(t, ran, "queued work did not run after the frame")
98 return
99 }
100 select {
101 case <-ran:
102 t.Fatal("work ran for a reconnected attachment")
103 case <-time.After(100 * time.Millisecond):
104 }
105 })
106 }
107}