main 3d41bc5

source_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close