1package capture
2
3import (
4 "context"
5 "image"
6 "os"
7 "strings"
8 "sync"
9 "testing"
10
11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/stretchr/testify/mock"
14 "github.com/stretchr/testify/require"
15)
16
17const internalID = ports.CaptureIDInternal | 1
18
19// hintFrame describes a scene for hint.capture tests: what it draws and
20// whether its exclusion is active.
21type hintFrame struct {
22 capture bool // hint.capture
23 labels bool // hint mode is shown on this output
24 indicator bool
25 posHint bool
26 exclusion bool // the scene carries an exclusion session (7) hiding drawn layers
27 unshown bool // ... hiding a surface the scene does not draw
28}
29
30func (f hintFrame) scene() ports.Scene {
31 s := excludedTestScene()
32 s.Capture = nil
33 if f.exclusion {
34 s.Capture = &ports.SceneCapture{Session: 7, Excluded: []ports.WindowID{10, 20}}
35 }
36 if f.unshown {
37 s.Capture = &ports.SceneCapture{Session: 7, Excluded: []ports.WindowID{99}}
38 }
39 s.HintStyle.Capture = f.capture
40 if f.labels {
41 s.HintLabels = []ports.HintLabel{{Text: "f"}}
42 }
43 if f.indicator {
44 s.CaptureIndicators = []ports.CaptureIndicator{{Rect: ports.Rect{W: 10, H: 10}}}
45 }
46 if f.posHint {
47 s.PositionHint = ports.PositionHint{Opacity: 1, Columns: 1}
48 }
49 return s
50}
51
52// drawn names what a rendered scene shows: its labels, indicator and
53// position hint, and "excluded" when the excluded layers are gone.
54func drawn(s ports.Scene) string {
55 var parts []string
56 if len(s.HintLabels) > 0 {
57 parts = append(parts, "labels")
58 }
59 if len(s.CaptureIndicators) > 0 {
60 parts = append(parts, "indicator")
61 }
62 if s.PositionHint.Shown() {
63 parts = append(parts, "poshint")
64 }
65 if len(s.Layers) == 2 {
66 parts = append(parts, "excluded")
67 }
68 if len(parts) == 0 {
69 return "bare"
70 }
71 return strings.Join(parts, "+")
72}
73
74// frameRun is what one output frame did, as the renderer saw it.
75type frameRun struct {
76 renders []string // drawn() of every Render, in call order
77 batches []int // requests copied by each BeginCapture, in call order
78 results map[uint64]error // reply of every request
79}
80
81// runFrame serves the requests the way the outputs do: Split, SubmitPlain,
82// the excluded frame, then the displayed frame and what still reads it.
83func runFrame(t *testing.T, s ports.Scene, reqs ...ports.CaptureRequest) frameRun {
84 t.Helper()
85 replies := make(chan ports.CaptureDone, len(reqs))
86 p := NewPipeline(context.Background(), replies)
87 r := portsmocks.NewMockRenderer(t)
88 var mu sync.Mutex
89 var run frameRun
90 var frames []*portsmocks.MockCaptureFrame
91 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
92 mu.Lock()
93 defer mu.Unlock()
94 run.renders = append(run.renders, drawn(s))
95 return nil, nil
96 }).Maybe()
97 r.EXPECT().BeginCapture().RunAndReturn(func() (ports.CaptureFrame, error) {
98 mu.Lock()
99 defer mu.Unlock()
100 n := len(run.batches)
101 run.batches = append(run.batches, 0)
102 f := portsmocks.NewMockCaptureFrame(t)
103 f.EXPECT().Done().Return((*os.File)(nil)).Maybe()
104 f.EXPECT().Read(image.Rect(0, 0, 2, 2), mock.Anything, 8).RunAndReturn(func(image.Rectangle, []byte, int) error {
105 mu.Lock()
106 defer mu.Unlock()
107 run.batches[n]++
108 return nil
109 }).Maybe()
110 frames = append(frames, f)
111 return f, nil
112 }).Maybe()
113 r.EXPECT().EndCapture(mock.Anything).Return().Maybe()
114
115 normal, excluded, hidden := p.Split(s, reqs)
116 require.Empty(t, hidden)
117 took, done, err := p.SubmitPlain(r, s, nil, normal)
118 require.NoError(t, err)
119 require.Nil(t, done)
120 if took {
121 normal = nil
122 }
123 if len(excluded) > 0 {
124 _, _ = r.Render(p.ExcludedScene(s), nil)
125 p.SubmitScoped(s.Security, r, excluded)
126 }
127 _, _ = r.Render(s, nil)
128 p.SubmitScoped(s.Security, r, normal)
129
130 run.results = map[uint64]error{}
131 for range reqs {
132 d := awaitCapture(t, replies)
133 run.results[d.ID] = d.Err
134 }
135 p.Close(r)
136 for _, f := range frames {
137 f.Calls = nil // Read's arguments point into unmapped destinations
138 }
139 return run
140}
141
142func client(t *testing.T, id uint64) ports.CaptureRequest { return pipelineRequest(t, id) }
143
144func clientExclude(t *testing.T, id uint64) ports.CaptureRequest {
145 q := pipelineRequest(t, id)
146 q.Exclude, q.Session = true, 7
147 return q
148}
149
150func internalReq(t *testing.T, exclude bool) ports.CaptureRequest {
151 q := pipelineRequest(t, internalID)
152 if exclude {
153 q.Exclude, q.Session = true, 7
154 }
155 return q
156}
157
158// Every frame shape hint.capture can produce: what is rendered, how many
159// captures each batch copies, which requests fail. The displayed frame is
160// always the last render. No frame needs more than two batches.
161func TestHintCaptureFrames(t *testing.T) {
162 const (
163 displayed = "labels"
164 bare = "bare"
165 )
166 tests := []struct {
167 name string
168 frame hintFrame
169 reqs func(t *testing.T) []ports.CaptureRequest
170 renders []string
171 batches []int
172 failed map[uint64]error // want errors (ErrorIs); every other request succeeds
173 }{
174 {
175 name: "off: labels leave every capture, one plain frame serves both",
176 frame: hintFrame{labels: true},
177 reqs: func(t *testing.T) []ports.CaptureRequest {
178 return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
179 },
180 renders: []string{bare, displayed},
181 batches: []int{2},
182 },
183 {
184 name: "on: a client capture reads the displayed frame, labels included",
185 frame: hintFrame{capture: true, labels: true},
186 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{client(t, 1)} },
187 renders: []string{displayed},
188 batches: []int{1},
189 },
190 {
191 name: "on: the indicator is dropped, the labels are kept",
192 frame: hintFrame{capture: true, labels: true, indicator: true},
193 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{client(t, 1)} },
194 renders: []string{"labels", "labels+indicator"},
195 batches: []int{1},
196 },
197 {
198 name: "on: the position hint is dropped, the labels are kept",
199 frame: hintFrame{capture: true, labels: true, posHint: true},
200 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{client(t, 1)} },
201 renders: []string{"labels", "labels+poshint"},
202 batches: []int{1},
203 },
204 {
205 name: "on: an internal capture of a labelled frame fails, the client is served",
206 frame: hintFrame{capture: true, labels: true, indicator: true},
207 reqs: func(t *testing.T) []ports.CaptureRequest {
208 return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
209 },
210 renders: []string{"labels", "labels+indicator"},
211 batches: []int{1},
212 failed: map[uint64]error{internalID: ErrInternalLabelled},
213 },
214 {
215 name: "on: an internal capture alone fails and renders nothing for it",
216 frame: hintFrame{capture: true, labels: true},
217 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{internalReq(t, false)} },
218 // the displayed frame is still drawn by the output, with no batch
219 renders: []string{displayed},
220 failed: map[uint64]error{internalID: ErrInternalLabelled},
221 },
222 {
223 name: "on: an internal Exclude capture fails, its excluded frame has labels",
224 frame: hintFrame{capture: true, labels: true, exclusion: true},
225 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{internalReq(t, true)} },
226 // no excluded frame is rendered for a failed request
227 renders: []string{displayed},
228 failed: map[uint64]error{internalID: ErrInternalExclude},
229 },
230 {
231 name: "on: client, internal and Exclude requests over an indicator fit two batches",
232 frame: hintFrame{capture: true, labels: true, indicator: true, posHint: true, exclusion: true},
233 reqs: func(t *testing.T) []ports.CaptureRequest {
234 return []ports.CaptureRequest{client(t, 1), internalReq(t, false), clientExclude(t, 3)}
235 },
236 // plain frame for the client, excluded frame (labels kept, no
237 // indicator, no position hint) for Exclude, then the displayed one
238 renders: []string{"labels", "labels+excluded", "labels+indicator+poshint"},
239 batches: []int{1, 1},
240 failed: map[uint64]error{internalID: ErrInternalLabelled},
241 },
242 {
243 name: "on: labels alone do not force the excluded frame",
244 frame: hintFrame{capture: true, labels: true, unshown: true},
245 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{clientExclude(t, 1)} },
246 renders: []string{displayed},
247 batches: []int{1},
248 },
249 {
250 name: "off: labels force the excluded frame, without labels",
251 frame: hintFrame{labels: true, exclusion: true},
252 reqs: func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{clientExclude(t, 1)} },
253 renders: []string{"excluded", displayed},
254 batches: []int{1},
255 },
256 {
257 name: "on without labels shown: internal and client share the plain frame",
258 frame: hintFrame{capture: true, indicator: true},
259 reqs: func(t *testing.T) []ports.CaptureRequest {
260 return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
261 },
262 renders: []string{bare, "indicator"},
263 batches: []int{2},
264 },
265 {
266 name: "nothing to hide: the displayed frame serves everyone",
267 frame: hintFrame{capture: true},
268 reqs: func(t *testing.T) []ports.CaptureRequest {
269 return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
270 },
271 renders: []string{bare},
272 batches: []int{2},
273 },
274 }
275 for _, tt := range tests {
276 t.Run(tt.name, func(t *testing.T) {
277 run := runFrame(t, tt.frame.scene(), tt.reqs(t)...)
278 require.Equal(t, tt.renders, run.renders)
279 require.Equal(t, tt.batches, run.batches)
280 require.LessOrEqual(t, len(run.batches), pipelineSlots)
281 for id, err := range run.results {
282 if want, ok := tt.failed[id]; ok {
283 require.ErrorIs(t, err, want, "request %#x", id)
284 require.ErrorIs(t, err, ports.ErrCaptureTransient)
285 } else {
286 require.NoError(t, err, "request %#x", id)
287 }
288 }
289 })
290 }
291}
292
293// Splitting and building the excluded scene of a frame that keeps labels
294// allocates nothing.
295func TestSplitNoAllocHintCapture(t *testing.T) {
296 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 8))
297 defer p.Close(nil)
298 s := hintFrame{capture: true, labels: true, indicator: true, exclusion: true}.scene()
299 reqs := make([]ports.CaptureRequest, 3)
300 if allocs := testing.AllocsPerRun(100, func() {
301 reqs[0] = ports.CaptureRequest{ID: 1}
302 reqs[1] = ports.CaptureRequest{ID: 2, Exclude: true, Session: 7}
303 reqs[2] = ports.CaptureRequest{ID: 3, Exclude: true, Session: 7}
304 _ = p.ExcludedScene(s)
305 p.Split(s, reqs)
306 }); allocs != 0 {
307 t.Fatalf("%.1f allocs, want 0", allocs)
308 }
309}