1package capture
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "image"
8 "os"
9 "sync/atomic"
10 "testing"
11
12 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16)
17
18func TestSubmitScopedRejectsChangedAndProtectedEpoch(t *testing.T) {
19 for _, state := range []ports.SecurityState{{Generation: 4}, {Generation: 3, Protected: true}} {
20 t.Run(fmt.Sprintf("generation-%d-protected-%v", state.Generation, state.Protected), func(t *testing.T) {
21 replies := make(chan ports.CaptureDone, 2)
22 p := NewPipeline(context.Background(), replies)
23 security := portsmocks.NewMockSessionSecurity(t)
24 security.EXPECT().Snapshot().Return(state)
25 p.Security = security
26 r := portsmocks.NewMockRenderer(t)
27 q := pipelineRequest(t, 1)
28 file := q.Dst.File
29 p.SubmitScoped(ports.SecurityState{Generation: 3}, r, []ports.CaptureRequest{q})
30 p.Close(r)
31 require.Len(t, replies, 1)
32 require.ErrorIs(t, (<-replies).Err, ErrSecurityState)
33 r.AssertNotCalled(t, "BeginCapture")
34 _, err := file.Stat()
35 require.ErrorIs(t, err, os.ErrClosed)
36 })
37 }
38}
39
40func TestSubmitHiddenEngageDuringRenderRejectsCopyRetainsReads(t *testing.T) {
41 replies := make(chan ports.CaptureDone, 4)
42 p := NewPipeline(context.Background(), replies)
43 security := portsmocks.NewMockSessionSecurity(t)
44 var engaged atomic.Bool
45 security.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
46 if engaged.Load() {
47 return ports.SecurityState{Generation: 1, Protected: true}
48 }
49 return ports.SecurityState{}
50 })
51 p.Security = security
52 child := portsmocks.NewMockRenderer(t)
53 f, w, err := os.Pipe()
54 require.NoError(t, err)
55 t.Cleanup(func() { f.Close(); w.Close() })
56 child.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
57 engaged.Store(true)
58 return f, nil
59 }).Once()
60 child.EXPECT().Close().Return().Once()
61 p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
62 s := hiddenScene()
63 s.CaptureScene.Windows = []ports.SceneWindow{{ID: 5}}
64 surfaces := map[ports.WindowID]ports.SurfaceContent{5: {ID: 5, Seq: 8}}
65 reqs := []ports.CaptureRequest{hiddenRequest(t, 1, image.Rect(0, 0, 200, 100)), hiddenRequest(t, 2, image.Rect(0, 0, 200, 100))}
66 files := []*os.File{reqs[0].Dst.File, reqs[1].Dst.File}
67 p.SubmitHidden(s, surfaces, reqs)
68 child.AssertNotCalled(t, "BeginCapture")
69 require.True(t, p.HiddenReading(), "rejected native capture still owns GPU reads")
70 seen, reads, limited := p.CapHiddenSeen(map[ports.WindowID]uint64{5: 9})
71 require.True(t, limited)
72 require.Equal(t, uint64(8), seen[5])
73 require.Equal(t, uint64(8), reads[5])
74 for range 2 {
75 reply := awaitCapture(t, replies)
76 require.ErrorIs(t, reply.Err, ErrSecurityState)
77 }
78 for _, file := range files {
79 _, err := file.Stat()
80 require.ErrorIs(t, err, os.ErrClosed)
81 }
82 require.Empty(t, replies, "exactly one failure reply for each request credit")
83 _, err = f.Stat()
84 require.NoError(t, err, "render fence stays owned until signalling")
85 _, err = w.Write([]byte{1})
86 require.NoError(t, err)
87 require.False(t, p.HiddenReading())
88 _, err = f.Stat()
89 require.True(t, errors.Is(err, os.ErrClosed), "signalled child fence closes")
90 p.Close(nil)
91 require.Empty(t, replies, "closing rejected pipeline creates no duplicate reply")
92}