1package wayland
2
3import (
4 "context"
5 "os"
6 "runtime"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/nefercap/internal/adapters/wayland/testserver"
13 "github.com/bnema/nefercap/internal/ports"
14)
15
16func newSource(t testing.TB, cfg testserver.Config) (*Source, *testserver.Server) {
17 t.Helper()
18 srv := testserver.Start(t, cfg)
19 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
20 defer cancel()
21 s, err := New(ctx, srv.Path)
22 require.NoError(t, err)
23 t.Cleanup(func() { _ = s.Close() })
24 return s, srv
25}
26
27func firstOutput(t testing.TB, s *Source) ports.Output {
28 t.Helper()
29 outs, err := s.Outputs(context.Background())
30 require.NoError(t, err)
31 require.NotEmpty(t, outs)
32 return outs[0]
33}
34
35func fdCount(t *testing.T) int {
36 t.Helper()
37 entries, err := os.ReadDir("/proc/self/fd")
38 require.NoError(t, err)
39 return len(entries)
40}
41
42func TestOutputs(t *testing.T) {
43 s, _ := newSource(t, testserver.Config{Outputs: []testserver.OutputSpec{
44 {Name: "A-1", Width: 8, Height: 4, Scale: 1}, {Name: "B-2", Width: 16, Height: 8, Scale: 2},
45 }})
46 outs, err := s.Outputs(context.Background())
47 require.NoError(t, err)
48 require.Len(t, outs, 2)
49 require.Equal(t, "A-1", outs[0].Name)
50 require.Equal(t, 16, outs[1].Width)
51 require.Equal(t, 2, outs[1].Scale)
52}
53
54func TestCapabilitiesBaseline(t *testing.T) {
55 s, _ := newSource(t, testserver.Config{})
56 require.Equal(t, ports.Capabilities{}, s.Capabilities())
57}
58
59func TestCaptureFullOutput(t *testing.T) {
60 s, srv := newSource(t, testserver.Config{})
61 out := firstOutput(t, s)
62 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID})
63 require.NoError(t, err)
64 require.NoError(t, f.Validate())
65 require.Equal(t, [3]int{8, 4, 32}, [3]int{f.Width, f.Height, f.Stride})
66 require.Equal(t, ports.XRGB8888, f.Format)
67 want := testserver.Pixel(3, 2, 1)
68 require.Equal(t, want[:], f.Row(2)[12:16])
69 require.Equal(t, "output", srv.Stats().LastSource)
70}
71
72func TestCapturePicksSupportedFormat(t *testing.T) {
73 s, _ := newSource(t, testserver.Config{Formats: []uint32{0x34325258 /* XR24 big */, testserver.ARGB8888}})
74 f, err := s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID})
75 require.NoError(t, err)
76 require.Equal(t, ports.ARGB8888, f.Format)
77}
78
79func TestCaptureUnsupportedFormat(t *testing.T) {
80 s, _ := newSource(t, testserver.Config{Formats: []uint32{0x34325258}})
81 _, err := s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID})
82 require.ErrorIs(t, err, ports.ErrUnsupportedFormat)
83 _, err = s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID})
84 require.ErrorIs(t, err, ports.ErrUnsupportedFormat, "a negotiation failure leaves the Source usable")
85}
86
87func TestCaptureNoShmFormat(t *testing.T) {
88 s, _ := newSource(t, testserver.Config{NoShmFormat: true})
89 _, err := s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID})
90 require.ErrorIs(t, err, ports.ErrUnsupportedFormat)
91}
92
93func TestCaptureUnknownOutputAndInvalidRegion(t *testing.T) {
94 s, _ := newSource(t, testserver.Config{})
95 _, err := s.Capture(context.Background(), ports.Target{OutputID: 999})
96 require.ErrorIs(t, err, ports.ErrOutputNotFound)
97 out := firstOutput(t, s)
98 for _, r := range []ports.Region{{X: -1, Width: 1, Height: 1}, {Width: 0, Height: 1, X: 1}, {Width: 1, Height: -1}, {Width: ports.MaxDimension + 1, Height: 1}} {
99 _, err = s.Capture(context.Background(), ports.Target{OutputID: out.ID, Region: r})
100 require.ErrorIs(t, err, ports.ErrInvalidRegion, "%+v", r)
101 }
102}
103
104func TestMissingCopyCapture(t *testing.T) {
105 srv := testserver.Start(t, testserver.Config{NoCopyCapture: true})
106 _, err := New(context.Background(), srv.Path)
107 require.ErrorContains(t, err, "ext-image-copy-capture")
108}
109
110func TestOutputRemovedDuringCapture(t *testing.T) {
111 s, srv := newSource(t, testserver.Config{Hold: true})
112 out := firstOutput(t, s)
113 errc := make(chan error, 1)
114 go func() { _, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID}); errc <- err }()
115 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond)
116 srv.RemoveOutput("TEST-1")
117 srv.Release()
118 require.ErrorIs(t, <-errc, ports.ErrOutputNotFound)
119 _, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID})
120 require.ErrorIs(t, err, ports.ErrOutputNotFound)
121}
122
123func TestSessionStoppedEndsCapture(t *testing.T) {
124 s, srv := newSource(t, testserver.Config{})
125 target := ports.Target{OutputID: firstOutput(t, s).ID}
126 _, err := s.Capture(context.Background(), target)
127 require.NoError(t, err)
128 srv.StopSessions()
129 _, err = s.Capture(context.Background(), target)
130 require.ErrorIs(t, err, ports.ErrCaptureStopped)
131 _, err = s.Capture(context.Background(), target)
132 require.NoError(t, err, "a stopped session is replaced by the next capture")
133}
134
135func TestFailedStoppedReasonEndsCapture(t *testing.T) {
136 s, srv := newSource(t, testserver.Config{})
137 target := ports.Target{OutputID: firstOutput(t, s).ID}
138 srv.Fail(1, 2)
139 _, err := s.Capture(context.Background(), target)
140 require.ErrorIs(t, err, ports.ErrCaptureStopped)
141}
142
143func TestFailedUnknownRetries(t *testing.T) {
144 s, srv := newSource(t, testserver.Config{})
145 target := ports.Target{OutputID: firstOutput(t, s).ID}
146 srv.Fail(2, 0)
147 _, err := s.Capture(context.Background(), target)
148 require.NoError(t, err, "recoverable failures are retried")
149 s, srv = newSource(t, testserver.Config{})
150 srv.Fail(maxFrameAttempts, 0)
151 _, err = s.Capture(context.Background(), target)
152 require.ErrorIs(t, err, errCompositorFailedCapture, "retries without a last frame are bounded")
153 srv.Fail(0, 0)
154 _, err = s.Capture(context.Background(), target)
155 require.NoError(t, err, "and the Source stays usable")
156}
157
158// With a last frame, a transient unknown failure repeats it, like a pending
159// capture does; the failures are counted across calls and stay bounded.
160func TestFailedUnknownRepeatsLastFrame(t *testing.T) {
161 s, srv := newSource(t, testserver.Config{})
162 target := ports.Target{OutputID: firstOutput(t, s).ID}
163 first, err := s.Capture(context.Background(), target)
164 require.NoError(t, err)
165 want := append([]byte(nil), first.Pixels...)
166 srv.Fail(1, 0)
167 repeated, err := s.Capture(context.Background(), target)
168 require.NoError(t, err, "a transient unknown failure repeats the last frame")
169 require.Equal(t, want, repeated.Pixels)
170 fresh, err := s.Capture(context.Background(), target)
171 require.NoError(t, err, "recording continues")
172 require.NotEqual(t, want, fresh.Pixels, "with a new frame")
173
174 // Capture prefetched the next frame. Wait for the server to answer it
175 // before arming failures, so it cannot consume the first injected failure.
176 require.NoError(t, s.roundtrip())
177 srv.Fail(maxFrameAttempts, 0)
178 // Consume the successful prefetched frame before counting failures.
179 _, err = s.Capture(context.Background(), target)
180 require.NoError(t, err)
181 for range maxFrameAttempts - 1 {
182 _, err = s.Capture(context.Background(), target)
183 require.NoError(t, err)
184 }
185 _, err = s.Capture(context.Background(), target)
186 require.ErrorIs(t, err, errCompositorFailedCapture, "the failure count is kept across calls")
187 srv.Fail(0, 0)
188 _, err = s.Capture(context.Background(), target)
189 require.NoError(t, err, "and the Source stays usable")
190}
191
192// A session the compositor stops before its first frame is a refusal: the
193// error says so, and it is not retried with a second session.
194func TestSessionStoppedBeforeFirstFrameIsRefusal(t *testing.T) {
195 s, srv := newSource(t, testserver.Config{StopAtCreate: true})
196 target := ports.Target{OutputID: firstOutput(t, s).ID}
197 _, err := s.Capture(context.Background(), target)
198 require.ErrorIs(t, err, ports.ErrCaptureStopped)
199 require.ErrorContains(t, err, "compositor refused capture")
200 require.ErrorContains(t, err, "/etc/neferwl/capture-allow")
201 require.Equal(t, 1, srv.Stats().Sessions, "no second session within the call")
202}
203
204// A stop after frames were served is a plain stop, not a refusal.
205func TestSessionStoppedAfterFrameIsNotRefusal(t *testing.T) {
206 s, srv := newSource(t, testserver.Config{})
207 target := ports.Target{OutputID: firstOutput(t, s).ID}
208 _, err := s.Capture(context.Background(), target)
209 require.NoError(t, err)
210 srv.StopSessions()
211 _, err = s.Capture(context.Background(), target)
212 require.ErrorIs(t, err, ports.ErrCaptureStopped)
213 require.NotContains(t, err.Error(), "refused")
214}
215
216func TestFailedBufferConstraintsRenegotiates(t *testing.T) {
217 s, srv := newSource(t, testserver.Config{})
218 out := firstOutput(t, s)
219 target := ports.Target{OutputID: out.ID}
220 _, err := s.Capture(context.Background(), target)
221 require.NoError(t, err)
222 // The constraints change while no frame is in flight; the next frame is
223 // created with a fresh buffer of the new size.
224 srv.Resize("TEST-1", 16, 8)
225 var f ports.Frame
226 require.Eventually(t, func() bool {
227 var err error
228 f, err = s.Capture(context.Background(), target)
229 require.NoError(t, err)
230 return f.Width == 16
231 }, 5*time.Second, time.Millisecond)
232 require.Equal(t, [2]int{16, 8}, [2]int{f.Width, f.Height})
233}
234
235// After a layout change the last frame no longer fits: a failed(unknown)
236// cannot repeat it. The frame is retried, bounded, and never the stale one.
237func TestFailedUnknownAfterLayoutChangeRetriesBounded(t *testing.T) {
238 s, srv := newSource(t, testserver.Config{})
239 target := ports.Target{OutputID: firstOutput(t, s).ID}
240 _, err := s.Capture(context.Background(), target)
241 require.NoError(t, err)
242 srv.Resize("TEST-1", 16, 8)
243 _ = firstOutput(t, s) // a roundtrip: the session's new constraints are read
244 srv.Fail(2, 0)
245 f, err := s.Capture(context.Background(), target)
246 require.NoError(t, err, "recoverable failures are retried")
247 require.Equal(t, [2]int{16, 8}, [2]int{f.Width, f.Height}, "never the repeated 8x4 frame")
248
249 srv.Resize("TEST-1", 32, 16)
250 _ = firstOutput(t, s)
251 srv.Fail(maxFrameAttempts, 0)
252 _, err = s.Capture(context.Background(), target)
253 require.ErrorIs(t, err, errCompositorFailedCapture, "the retries are bounded")
254 srv.Fail(0, 0)
255 f, err = s.Capture(context.Background(), target)
256 require.NoError(t, err, "and the Source stays usable")
257 require.Equal(t, [2]int{32, 16}, [2]int{f.Width, f.Height})
258}
259
260func TestConstraintsChangeWhileFrameInFlight(t *testing.T) {
261 s, srv := newSource(t, testserver.Config{Hold: true})
262 target := ports.Target{OutputID: firstOutput(t, s).ID}
263 type result struct {
264 f ports.Frame
265 err error
266 }
267 res := make(chan result, 1)
268 go func() { f, err := s.Capture(context.Background(), target); res <- result{f, err} }()
269 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond)
270 srv.Resize("TEST-1", 16, 8) // the held 8x4 frame now mismatches: failed(buffer_constraints)
271 srv.Release()
272 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond, "the retry uses the new size")
273 srv.Release()
274 r := <-res
275 require.NoError(t, r.err)
276 require.Equal(t, [2]int{16, 8}, [2]int{r.f.Width, r.f.Height})
277}
278
279func TestReuseAndBuffers(t *testing.T) {
280 s, srv := newSource(t, testserver.Config{})
281 target := ports.Target{OutputID: firstOutput(t, s).ID}
282 for range 5 {
283 _, err := s.Capture(context.Background(), target)
284 require.NoError(t, err)
285 }
286 st := srv.Stats()
287 require.Equal(t, 2, st.Buffers, "two shm buffers alternate: the last frame stays readable while the next is written")
288 require.Equal(t, 1, st.Sessions, "one session serves consecutive frames")
289 require.GreaterOrEqual(t, st.Ready, 5)
290 srv.Resize("TEST-1", 16, 8)
291 var f ports.Frame
292 require.Eventually(t, func() bool { // a resize is seen by the next calls, never mid-poll
293 var err error
294 f, err = s.Capture(context.Background(), target)
295 require.NoError(t, err)
296 return f.Width == 16
297 }, 5*time.Second, time.Millisecond)
298 st = srv.Stats()
299 require.GreaterOrEqual(t, st.Buffers, 3)
300 require.GreaterOrEqual(t, st.DestroyedBuffers, 1, "a buffer of the old size is destroyed")
301}
302
303func TestOneFrameInFlight(t *testing.T) {
304 // The server raises duplicate_frame when a frame is created before the
305 // previous one was destroyed, so successful back to back captures prove it.
306 s, _ := newSource(t, testserver.Config{})
307 target := ports.Target{OutputID: firstOutput(t, s).ID}
308 for range 20 {
309 _, err := s.Capture(context.Background(), target)
310 require.NoError(t, err)
311 }
312}
313
314func TestNoDescriptorLeak(t *testing.T) {
315 s, _ := newSource(t, testserver.Config{})
316 target := ports.Target{OutputID: firstOutput(t, s).ID}
317 _, err := s.Capture(context.Background(), target)
318 require.NoError(t, err)
319 before := fdCount(t)
320 for range 50 {
321 _, err = s.Capture(context.Background(), target)
322 require.NoError(t, err)
323 }
324 require.LessOrEqual(t, fdCount(t), before+1)
325}
326
327func TestCaptureWaitsForReady(t *testing.T) {
328 s, srv := newSource(t, testserver.Config{Hold: true})
329 target := ports.Target{OutputID: firstOutput(t, s).ID}
330 done := make(chan error, 1)
331 go func() { _, err := s.Capture(context.Background(), target); done <- err }()
332 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond)
333 select {
334 case <-done:
335 t.Fatal("capture returned before the compositor completed the copy")
336 case <-time.After(50 * time.Millisecond):
337 }
338 srv.Release()
339 require.NoError(t, <-done)
340}
341
342func TestCancelInterruptsBlockedCapture(t *testing.T) {
343 s, srv := newSource(t, testserver.Config{Hold: true})
344 target := ports.Target{OutputID: firstOutput(t, s).ID}
345 ctx, cancel := context.WithCancel(context.Background())
346 done := make(chan error, 1)
347 go func() { _, err := s.Capture(ctx, target); done <- err }()
348 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond)
349 cancel()
350 require.ErrorIs(t, <-done, context.Canceled)
351 _, err := s.Capture(context.Background(), target)
352 require.ErrorIs(t, err, ports.ErrClosed)
353}
354
355func TestCloseInterruptsBlockedCapture(t *testing.T) {
356 s, srv := newSource(t, testserver.Config{Hold: true})
357 target := ports.Target{OutputID: firstOutput(t, s).ID}
358 done := make(chan error, 1)
359 go func() { _, err := s.Capture(context.Background(), target); done <- err }()
360 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond)
361 require.NoError(t, s.Close())
362 require.ErrorIs(t, <-done, ports.ErrClosed)
363 require.NoError(t, s.Close(), "idempotent")
364}
365
366func TestCancelledContextBeforeCall(t *testing.T) {
367 s, _ := newSource(t, testserver.Config{})
368 ctx, cancel := context.WithCancel(context.Background())
369 cancel()
370 _, err := s.Capture(ctx, ports.Target{OutputID: firstOutput(t, s).ID})
371 require.ErrorIs(t, err, context.Canceled)
372}
373
374func TestConstructorContextIsScopedToConstructor(t *testing.T) {
375 srv := testserver.Start(t, testserver.Config{})
376 ctx, cancel := context.WithCancel(context.Background())
377 s, err := New(ctx, srv.Path)
378 require.NoError(t, err)
379 defer s.Close()
380 cancel()
381 _, err = s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID})
382 require.NoError(t, err)
383}
384
385func TestConstructorConnectFailure(t *testing.T) {
386 _, err := New(context.Background(), t.TempDir()+"/none")
387 require.Error(t, err)
388}
389
390func TestIdleCancellationDoesNotKillSource(t *testing.T) {
391 s, _ := newSource(t, testserver.Config{})
392 target := ports.Target{OutputID: firstOutput(t, s).ID}
393 ctx, cancel := context.WithCancel(context.Background())
394 _, err := s.Capture(ctx, target)
395 require.NoError(t, err)
396 _, err = s.Capture(ctx, target) // same context: the watcher stays registered
397 require.NoError(t, err)
398 cancel()
399 _, err = s.Capture(context.Background(), target)
400 require.NoError(t, err, "a cancelled context affects only calls given that context")
401}
402
403func TestConcurrentCallsAreSerialized(t *testing.T) {
404 s, _ := newSource(t, testserver.Config{})
405 target := ports.Target{OutputID: firstOutput(t, s).ID}
406 errc := make(chan error, 8)
407 for range 8 {
408 go func() { _, err := s.Capture(context.Background(), target); errc <- err }()
409 }
410 for range 8 {
411 require.NoError(t, <-errc)
412 }
413}
414
415// allocBudget is measured (see the log of TestCaptureAllocations: 17 to 18),
416// with a small margin. The session builds the frame handlers once, so a frame
417// only allocates its proxy and the handler slices of the generated binding.
418const allocBudget = 20
419
420// Steady state cost of one capture over the real transport. The measurement
421// includes the in-process libwayland server, so it is an upper bound for the
422// Source alone; the budget is set from measured values (see the benchmark).
423func TestCaptureAllocations(t *testing.T) {
424 if raceEnabled {
425 t.Skip("race instrumentation changes allocation counts")
426 }
427 s, _ := newSource(t, testserver.Config{Outputs: []testserver.OutputSpec{{Name: "X", Width: 64, Height: 64, Scale: 1}}})
428 target := ports.Target{OutputID: firstOutput(t, s).ID}
429 ctx := context.Background()
430 for range 3 { // warm pools and buffers
431 _, err := s.Capture(ctx, target)
432 require.NoError(t, err)
433 }
434 runtime.GC()
435 avg := testing.AllocsPerRun(200, func() {
436 if _, err := s.Capture(ctx, target); err != nil {
437 panic(err)
438 }
439 })
440 t.Logf("allocations per capture (client and in-process server): %g", avg)
441 require.LessOrEqual(t, avg, float64(allocBudget))
442}
443
444// BenchmarkCapture measures Capture in a tight loop. The compositor needs
445// longer than the poll to fill a frame, so many calls repeat the last frame;
446// fresh-frames/op says how many were new.
447func BenchmarkCapture(b *testing.B) {
448 s, srv := newSource(b, testserver.Config{Outputs: []testserver.OutputSpec{{Name: "BENCH", Width: 1920, Height: 1080, Scale: 1}}})
449 target := ports.Target{OutputID: firstOutput(b, s).ID}
450 ctx := context.Background()
451 b.ReportAllocs()
452 b.SetBytes(1920 * 1080 * 4)
453 b.ResetTimer()
454 for b.Loop() {
455 if _, err := s.Capture(ctx, target); err != nil {
456 b.Fatal(err)
457 }
458 }
459 b.ReportMetric(float64(srv.Stats().Ready)/float64(b.N), "fresh-frames/op")
460}
461
462// Opt-in: NEFERCAP_WAYLAND_SOCKET names the socket of a disposable headless
463// compositor that supports ext-image-copy-capture. Never point it at a real
464// session.
465func TestHeadlessCompositor(t *testing.T) {
466 socket := os.Getenv("NEFERCAP_WAYLAND_SOCKET")
467 if socket == "" {
468 t.Skip("NEFERCAP_WAYLAND_SOCKET not set")
469 }
470 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
471 defer cancel()
472 s, err := New(ctx, socket)
473 require.NoError(t, err)
474 defer s.Close()
475 out := firstOutput(t, s)
476 for range 3 {
477 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID})
478 require.NoError(t, err)
479 require.NoError(t, f.Validate())
480 }
481 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID, Region: ports.Region{Width: 16, Height: 8}})
482 require.NoError(t, err)
483 require.Equal(t, [2]int{16, 8}, [2]int{f.Width, f.Height})
484}
485
486// A compositor that waits for damage holds every frame after the first. The
487// capture stays pending, at most one frame is in flight, and Capture returns
488// the last frame at once instead of blocking a timed recording.
489func TestStaticScreenRepeatsLastFrame(t *testing.T) {
490 s, srv := newSource(t, testserver.Config{HoldAfterFirst: true})
491 target := ports.Target{OutputID: firstOutput(t, s).ID}
492 first, err := s.Capture(context.Background(), target)
493 require.NoError(t, err)
494 want := testserver.Pixel(2, 1, 1)
495 require.Equal(t, want[:], first.Row(1)[8:12])
496 for range 5 {
497 began := time.Now()
498 f, err := s.Capture(context.Background(), target)
499 require.NoError(t, err)
500 require.Less(t, time.Since(began), time.Second, "a pending capture does not block the caller")
501 require.Equal(t, want[:], f.Row(1)[8:12], "the last frame is repeated")
502 }
503 require.Eventually(t, func() bool { return srv.Held() == 1 }, 5*time.Second, time.Millisecond)
504 st := srv.Stats()
505 require.Equal(t, 2, st.Frames, "one served frame and one pending: a single frame in flight")
506 srv.Release() // the screen changed
507 var f ports.Frame
508 require.Eventually(t, func() bool {
509 f, err = s.Capture(context.Background(), target)
510 require.NoError(t, err)
511 return f.Row(1)[10] == 2
512 }, 5*time.Second, time.Millisecond, "the pending capture completes into the other buffer")
513}
514
515// A timed recording over a static screen keeps its schedule.
516func TestTimedRecordingOverStaticScreen(t *testing.T) {
517 s, _ := newSource(t, testserver.Config{HoldAfterFirst: true})
518 target := ports.Target{OutputID: firstOutput(t, s).ID}
519 _, err := s.Capture(context.Background(), target)
520 require.NoError(t, err)
521 began := time.Now()
522 for range 20 { // a 30 fps recording would wait 20 × 33 ms: each call must be far shorter
523 _, err = s.Capture(context.Background(), target)
524 require.NoError(t, err)
525 }
526 require.Less(t, time.Since(began), 500*time.Millisecond)
527}
528
529// NeferWL sends no new constraint batch after failed(buffer_constraints): the
530// frame is retried with the current constraints after a roundtrip.
531func TestFailedBufferConstraintsRetriesWithoutNewBatch(t *testing.T) {
532 s, srv := newSource(t, testserver.Config{})
533 target := ports.Target{OutputID: firstOutput(t, s).ID}
534 srv.Fail(1, 1)
535 _, err := s.Capture(context.Background(), target)
536 require.NoError(t, err)
537 srv.Fail(maxFrameAttempts, 1)
538 _, err = s.Capture(context.Background(), target)
539 require.ErrorIs(t, err, errCompositorFailedCapture, "the retries are bounded")
540 srv.Fail(0, 0)
541 _, err = s.Capture(context.Background(), target)
542 require.NoError(t, err)
543}
544
545func TestNonNormalTransformIsRejected(t *testing.T) {
546 for _, region := range []ports.Region{{}, {X: 1, Y: 1, Width: 2, Height: 2}} {
547 s, _ := newSource(t, testserver.Config{Transform: 3})
548 _, err := s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID, Region: region})
549 require.ErrorIs(t, err, ports.ErrUnsupportedTransform)
550 require.ErrorContains(t, err, "output transform 3 is not supported")
551 }
552}
553
554// A workspace source stops when its workspace goes away.
555func TestWorkspaceRemovedIsUnavailable(t *testing.T) {
556 cfg := wsConfig(testserver.WorkspaceSpec{ID: "7f3a-2", Name: "b", Output: "TEST-1"})
557 cfg.NeferwlSource = true
558 s, srv := newSource(t, cfg)
559 target := ports.Target{OutputID: firstOutput(t, s).ID, WorkspaceID: 1}
560 _, err := s.Capture(context.Background(), target)
561 require.NoError(t, err)
562 srv.RemoveWorkspace("b")
563 _, err = s.Capture(context.Background(), target)
564 require.ErrorIs(t, err, ports.ErrWorkspaceUnavailable)
565}
566
567// A workspace session that stops because its workspace was removed is a plain
568// stop, not a refusal: the allowlist hint is for a live workspace.
569func TestWorkspaceRemovedBeforeFirstFrameIsNotRefusal(t *testing.T) {
570 cfg := wsConfig(testserver.WorkspaceSpec{ID: "7f3a-2", Name: "b", Output: "TEST-1"})
571 cfg.NeferwlSource = true
572 s, srv := newSource(t, cfg)
573 target := ports.Target{OutputID: firstOutput(t, s).ID, WorkspaceID: 1}
574 srv.RemoveWorkspace("b") // not read yet: the session is created, then stopped
575 _, err := s.Capture(context.Background(), target)
576 require.ErrorIs(t, err, ports.ErrWorkspaceUnavailable)
577 require.ErrorIs(t, err, ports.ErrCaptureStopped)
578 require.NotContains(t, err.Error(), "capture-allow")
579 require.NotContains(t, err.Error(), "refused")
580}
581
582// A live workspace whose session is refused keeps the allowlist hint.
583func TestWorkspaceSessionRefusedKeepsAllowlistHint(t *testing.T) {
584 cfg := wsConfig(testserver.WorkspaceSpec{ID: "7f3a-2", Name: "b", Output: "TEST-1"})
585 cfg.NeferwlSource, cfg.StopAtCreate = true, true
586 s, _ := newSource(t, cfg)
587 _, err := s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID, WorkspaceID: 1})
588 require.ErrorIs(t, err, ports.ErrCaptureStopped)
589 require.ErrorContains(t, err, "/etc/neferwl/capture-allow")
590}
591
592// A cancellable context costs no more than a background one.
593func TestCaptureAllocationsCancellable(t *testing.T) {
594 if raceEnabled {
595 t.Skip("race instrumentation changes allocation counts")
596 }
597 s, _ := newSource(t, testserver.Config{Outputs: []testserver.OutputSpec{{Name: "X", Width: 64, Height: 64, Scale: 1}}})
598 target := ports.Target{OutputID: firstOutput(t, s).ID}
599 ctx, cancel := context.WithCancel(context.Background())
600 defer cancel()
601 for range 3 {
602 _, err := s.Capture(ctx, target)
603 require.NoError(t, err)
604 }
605 runtime.GC()
606 avg := testing.AllocsPerRun(200, func() {
607 if _, err := s.Capture(ctx, target); err != nil {
608 panic(err)
609 }
610 })
611 t.Logf("allocations per capture with a cancellable context: %g", avg)
612 require.LessOrEqual(t, avg, float64(allocBudget))
613}
614
615// A constraint batch that repeats the same layout (a compositor may re-send
616// its constraints) must not drop the last frame or the pending capture: on a
617// static screen the next Capture would block for damage that never comes.
618func TestSameGeometryConstraintBatchKeepsPendingFrame(t *testing.T) {
619 s, srv := newSource(t, testserver.Config{HoldAfterFirst: true})
620 target := ports.Target{OutputID: firstOutput(t, s).ID}
621 // Every call is bounded, so a regression fails instead of hanging.
622 capture := func() (ports.Frame, error) {
623 ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
624 defer cancel()
625 return s.Capture(ctx, target)
626 }
627 for range 3 {
628 _, err := capture()
629 require.NoError(t, err)
630 }
631 srv.Resize("TEST-1", 8, 4) // same size: a new batch, same layout
632 f, err := capture()
633 require.NoError(t, err, "the last frame is repeated, not waited for")
634 require.Equal(t, [2]int{8, 4}, [2]int{f.Width, f.Height})
635 require.Equal(t, 2, srv.Stats().Frames, "the pending capture was kept, not replaced")
636}