1package capture
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/ports"
8 "github.com/stretchr/testify/require"
9)
10
11func excludedTestScene() ports.Scene {
12 return ports.Scene{
13 Seq: 5,
14 Windows: []ports.SceneWindow{
15 {ID: 1}, {ID: 20, Popup: true, OverLayers: true}, {ID: 21, Popup: true, OverLayers: true}, {ID: 3},
16 },
17 Layers: []ports.SceneLayer{{ID: 10, Layer: ports.LayerTop}, {ID: 11, Layer: ports.LayerTop}, {ID: 12, Layer: ports.LayerOverlay}},
18 Capture: &ports.SceneCapture{Session: 7, Excluded: []ports.WindowID{10, 20}},
19 }
20}
21
22func ids(q []ports.CaptureRequest) (out []uint64) {
23 for _, r := range q {
24 out = append(out, r.ID)
25 }
26 return out
27}
28
29func TestExcludedSceneDropsOnlyExcluded(t *testing.T) {
30 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
31 defer p.Close(nil)
32 s := excludedTestScene()
33 c := p.ExcludedScene(s)
34 require.Nil(t, c.Capture)
35 require.Zero(t, c.Seq)
36 require.Equal(t, []ports.SceneWindow{{ID: 1}, {ID: 21, Popup: true, OverLayers: true}, {ID: 3}}, c.Windows)
37 require.Equal(t, []ports.SceneLayer{{ID: 11, Layer: ports.LayerTop}, {ID: 12, Layer: ports.LayerOverlay}}, c.Layers)
38 // The displayed scene is not mutated.
39 require.Len(t, s.Windows, 4)
40 require.Len(t, s.Layers, 3)
41 require.NotNil(t, s.Capture)
42 require.Equal(t, uint64(5), s.Seq)
43}
44
45func TestSplitNoAlloc(t *testing.T) {
46 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
47 defer p.Close(nil)
48 for name, s := range map[string]ports.Scene{"no capture": {Windows: excludedTestScene().Windows}, "exclusion": excludedTestScene()} {
49 reqs := make([]ports.CaptureRequest, 3)
50 for i := range reqs {
51 reqs[i].ID = uint64(i + 1)
52 }
53 if allocs := testing.AllocsPerRun(100, func() {
54 _ = p.ExcludedScene(s)
55 p.Split(s, reqs)
56 }); allocs != 0 {
57 t.Fatalf("%s: %.1f allocs, want 0", name, allocs)
58 }
59 }
60}
61
62func TestSplitOrdersAndFailsClosed(t *testing.T) {
63 replies := make(chan ports.CaptureDone, 8)
64 p := NewPipeline(context.Background(), replies)
65 defer p.Close(nil)
66 s := excludedTestScene()
67 s.Capture.Revision = 1
68 reqs := []ports.CaptureRequest{
69 {ID: 1, Exclude: true, Session: 7, CaptureRevision: 1},
70 {ID: 2},
71 {ID: 3, Exclude: true, Session: 8, CaptureRevision: 1}, // another session: never served
72 {ID: 4, Exclude: true, Session: 7, CaptureRevision: 1},
73 {ID: 5},
74 }
75 normal, excluded, hidden := p.Split(s, reqs)
76 require.ElementsMatch(t, []uint64{2, 5}, ids(normal))
77 require.ElementsMatch(t, []uint64{1, 4}, ids(excluded))
78 require.Empty(t, hidden)
79 for _, q := range excluded {
80 require.True(t, q.Exclude)
81 }
82 got := awaitCapture(t, replies)
83 require.Equal(t, uint64(3), got.ID)
84 require.ErrorIs(t, got.Err, ErrSessionInactive)
85 for _, q := range reqs[len(normal)+len(excluded):] {
86 require.True(t, Handed(q), "tail must be zeroed")
87 }
88}
89
90func TestSplitExcludeWithoutSceneExclusionFails(t *testing.T) {
91 replies := make(chan ports.CaptureDone, 4)
92 p := NewPipeline(context.Background(), replies)
93 defer p.Close(nil)
94 s := excludedTestScene()
95 s.Capture = nil
96 normal, excluded, _ := p.Split(s, []ports.CaptureRequest{{ID: 1, Exclude: true, Session: 7}, {ID: 2}})
97 require.Equal(t, []uint64{2}, ids(normal))
98 require.Empty(t, excluded)
99 got := awaitCapture(t, replies)
100 require.Equal(t, uint64(1), got.ID)
101 require.ErrorIs(t, got.Err, ErrSessionInactive)
102}
103
104// An exclusion with nothing to hide in the scene is served from the displayed
105// frame: no second composition.
106func TestSplitNoSecondCompositionWhenNothingExcludedIsShown(t *testing.T) {
107 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
108 defer p.Close(nil)
109 s := excludedTestScene()
110 s.Capture.Excluded = []ports.WindowID{99}
111 normal, excluded, _ := p.Split(s, []ports.CaptureRequest{{ID: 1, Exclude: true, Session: 7}, {ID: 2}})
112 require.Len(t, normal, 2)
113 require.Empty(t, excluded)
114}
115
116// Every other capture keeps the HUD, even while an exclusion is live.
117func TestSplitStandardCapturesKeepExclusionScene(t *testing.T) {
118 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
119 defer p.Close(nil)
120 normal, excluded, _ := p.Split(excludedTestScene(), []ports.CaptureRequest{{ID: 1}, {ID: 2}})
121 require.Len(t, normal, 2)
122 require.Empty(t, excluded)
123}
124
125// A request stamped with a revision the scene has not reached is refused:
126// the scene's exclusion list may predate a newly mapped HUD layer.
127func TestSplitStaleRevisionFailsClosed(t *testing.T) {
128 replies := make(chan ports.CaptureDone, 4)
129 p := NewPipeline(context.Background(), replies)
130 defer p.Close(nil)
131 s := excludedTestScene()
132 s.Capture.Revision = 3
133 normal, excluded, _ := p.Split(s, []ports.CaptureRequest{
134 {ID: 1, Exclude: true, Session: 7, CaptureRevision: 4},
135 {ID: 2, Exclude: true, Session: 7, CaptureRevision: 3},
136 {ID: 3, Exclude: true, Session: 7, CaptureRevision: 2},
137 })
138 require.Empty(t, normal)
139 require.Len(t, excluded, 2)
140 got := awaitCapture(t, replies)
141 require.Equal(t, uint64(1), got.ID)
142 require.ErrorIs(t, got.Err, ErrSessionStale)
143}
144
145// A workspace request is served only if its workspace is still where the
146// request expected: on screen (Shown), or the one rendered off screen.
147func TestSplitWorkspaceMovedFailsClosed(t *testing.T) {
148 replies := make(chan ports.CaptureDone, 8)
149 p := NewPipeline(context.Background(), replies)
150 defer p.Close(nil)
151 s := ports.Scene{Capture: &ports.SceneCapture{Shown: 3, Workspace: 4}}
152 reqs := []ports.CaptureRequest{
153 {ID: 1, Workspace: 3}, // on screen, still shown
154 {ID: 2, Workspace: 4, OffScreen: true}, // off screen, still rendered
155 {ID: 3, Workspace: 4}, // was on screen, now hidden
156 {ID: 4, Workspace: 3, OffScreen: true}, // was hidden, now shown
157 {ID: 5, Workspace: 9, OffScreen: true}, // not rendered at all
158 {ID: 6},
159 }
160 normal, excluded, hidden := p.Split(s, reqs)
161 require.ElementsMatch(t, []uint64{1, 6}, ids(normal))
162 require.Empty(t, excluded)
163 require.Equal(t, []uint64{2}, ids(hidden))
164 failed := map[uint64]bool{}
165 for range 3 {
166 d := awaitCapture(t, replies)
167 require.ErrorIs(t, d.Err, ErrWorkspaceMoved)
168 failed[d.ID] = true
169 }
170 require.Equal(t, map[uint64]bool{3: true, 4: true, 5: true}, failed)
171 // Without any capture state no workspace request is served.
172 normal, _, hidden = p.Split(ports.Scene{}, []ports.CaptureRequest{{ID: 7, Workspace: 3}})
173 require.Empty(t, normal)
174 require.Empty(t, hidden)
175 require.ErrorIs(t, awaitCapture(t, replies).Err, ErrWorkspaceMoved)
176}