1package capture
2
3import (
4 "context"
5 "image"
6 "os"
7 "testing"
8
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13)
14
15func indicatorScene() ports.Scene {
16 s := excludedTestScene()
17 s.Capture = nil
18 s.CaptureIndicators = []ports.CaptureIndicator{{Rect: ports.Rect{W: 10, H: 10}}, {Pill: true, Rect: ports.Rect{X: 1, Y: 1, W: 3, H: 3}}}
19 return s
20}
21
22// No capture scene ever carries the indicator: excluded scenes, plain scenes
23// and the off-screen child are derived without it.
24func TestCaptureScenesNeverCarryTheIndicator(t *testing.T) {
25 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
26 defer p.Close(nil)
27 s := indicatorScene()
28 require.Nil(t, p.ExcludedScene(s).CaptureIndicators)
29 s.Capture = excludedTestScene().Capture
30 require.Nil(t, p.ExcludedScene(s).CaptureIndicators)
31 require.Nil(t, plain(s).CaptureIndicators)
32 require.Len(t, s.CaptureIndicators, 2, "the displayed scene keeps its indicator")
33 // Even an exclusion-less request is fine: it is drawn from the plain frame.
34 normal, excluded, _ := p.Split(s, []ports.CaptureRequest{{ID: 1}, {ID: 2, Exclude: true, Session: 7}})
35 require.Equal(t, []uint64{1}, ids(normal))
36 require.Equal(t, []uint64{2}, ids(excluded), "an Exclude request is drawn from ExcludedScene, never the displayed frame")
37}
38
39// The requests of the displayed frame are answered from a frame rendered
40// without the indicator before the displayed one.
41func TestSubmitPlainRendersWithoutIndicatorFirst(t *testing.T) {
42 replies := make(chan ports.CaptureDone, 1)
43 p := NewPipeline(context.Background(), replies)
44 r := portsmocks.NewMockRenderer(t)
45 frame := portsmocks.NewMockCaptureFrame(t)
46 frame.EXPECT().Done().Return((*os.File)(nil))
47 frame.EXPECT().Read(image.Rect(0, 0, 2, 2), mock.Anything, 8).Return(nil).Once()
48 var rendered []ports.Scene
49 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
50 rendered = append(rendered, s)
51 return nil, nil
52 }).Once()
53 r.EXPECT().BeginCapture().Return(frame, nil).Once()
54 r.EXPECT().EndCapture(frame).Return().Once()
55 q := pipelineRequest(t, 1)
56 reqs := []ports.CaptureRequest{q}
57 took, done, err := p.SubmitPlain(r, indicatorScene(), nil, reqs)
58 require.NoError(t, err)
59 require.Nil(t, done)
60 require.True(t, took)
61 require.True(t, Handed(reqs[0]))
62 require.Len(t, rendered, 1)
63 require.Nil(t, rendered[0].CaptureIndicators)
64 require.Zero(t, rendered[0].Seq)
65 require.Equal(t, uint64(1), awaitCapture(t, replies).ID)
66 p.Close(r)
67 frame.Calls = nil
68}
69
70// Without indicator, or without requests, nothing is rendered and nothing
71// allocates: the feature is free while no capture is shown.
72func TestSubmitPlainIdleCostsNothing(t *testing.T) {
73 p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
74 defer p.Close(nil)
75 r := portsmocks.NewMockRenderer(t)
76 s := excludedTestScene()
77 reqs := []ports.CaptureRequest{{ID: 1}}
78 if took, _, err := p.SubmitPlain(r, s, nil, reqs); took || err != nil {
79 t.Fatalf("took %v err %v without an indicator", took, err)
80 }
81 if took, _, err := p.SubmitPlain(r, indicatorScene(), nil, nil); took || err != nil {
82 t.Fatalf("took %v err %v without requests", took, err)
83 }
84 if n := testing.AllocsPerRun(100, func() { _, _, _ = p.SubmitPlain(r, s, nil, reqs) }); n != 0 {
85 t.Fatalf("%v allocations", n)
86 }
87}
88
89// A scene that carries the indicator is still gated: once the session is
90// protected its plain frame is never copied, and the requests fail with the
91// security error, closed, exactly once.
92func TestSubmitPlainRejectsProtectedEpoch(t *testing.T) {
93 locked := ports.SecurityState{Generation: 3, Protected: true}
94 replies := make(chan ports.CaptureDone, 2)
95 p := NewPipeline(context.Background(), replies)
96 security := portsmocks.NewMockSessionSecurity(t)
97 security.EXPECT().Snapshot().Return(locked).Maybe()
98 p.Security = security
99 r := portsmocks.NewMockRenderer(t)
100 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
101 q := pipelineRequest(t, 1)
102 file := q.Dst.File
103 s := indicatorScene()
104 s.Security = locked
105 took, _, err := p.SubmitPlain(r, s, nil, []ports.CaptureRequest{q})
106 require.NoError(t, err)
107 require.True(t, took)
108 p.Close(r)
109 require.Len(t, replies, 1)
110 require.ErrorIs(t, (<-replies).Err, ErrSecurityState)
111 r.AssertNotCalled(t, "BeginCapture")
112 _, statErr := file.Stat()
113 require.ErrorIs(t, statErr, os.ErrClosed)
114}