1package headless
2
3import (
4 "context"
5 "image"
6 "os"
7 "testing"
8 "time"
9
10 "github.com/bnema/neferwl/internal/adapters/capture"
11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/stretchr/testify/mock"
14)
15
16func TestScreenshotReadbackFailureDoesNotWriteBlackPNG(t *testing.T) {
17 r := portsmocks.NewMockRenderer(t)
18 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
19 scenes := make(chan ports.Scene, 1)
20 scenes <- ports.Scene{Seq: 1, Scale: 1}
21 dir := t.TempDir()
22 ctx, cancel := context.WithCancel(context.Background())
23 defer cancel()
24 attempted := make(chan struct{})
25 // Synchronize on the actual readback attempt without assuming timing.
26 r.EXPECT().Pixels().Run(func() { close(attempted) }).Return(nil).Once()
27 r.EXPECT().Close().Return().Once()
28 done := make(chan error, 1)
29 go func() {
30 done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil, nil, nil)
31 }()
32 select {
33 case <-attempted:
34 case <-time.After(time.Second):
35 t.Fatal("no readback attempt")
36 }
37 select {
38 case err := <-done:
39 t.Fatalf("readback unavailable stopped output: %v", err)
40 default:
41 }
42 cancel()
43 if err := <-done; err != nil {
44 t.Fatal(err)
45 }
46 files, err := os.ReadDir(dir)
47 if err != nil || len(files) != 0 {
48 t.Fatalf("failed screenshot wrote files: %v (%v)", files, err)
49 }
50}
51
52func TestCaptureBatchBoundedAndRecycledWhileIdle(t *testing.T) {
53 r, _ := recordingRenderer(t, nil)
54 frame := portsmocks.NewMockCaptureFrame(t)
55 frame.EXPECT().Done().Return(nil)
56 frame.EXPECT().Read(mock.Anything, mock.Anything, 8).Return(nil).Times(capture.MaxRequests)
57 recycled := make(chan struct{})
58 r.EXPECT().BeginCapture().Return(frame, nil).Once()
59 r.EXPECT().EndCapture(frame).Run(func(ports.CaptureFrame) { close(recycled) }).Return().Once()
60 ctx, cancel := context.WithCancel(context.Background())
61 defer cancel()
62 incoming := make(chan ports.CaptureRequest)
63 scenes := make(chan ports.Scene)
64 replies := make(chan ports.CaptureDone, capture.MaxRequests+1)
65 done := make(chan error, 1)
66 go func() {
67 done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }, Captured: replies}, scenes, nil, nil, incoming)
68 }()
69 for i := range capture.MaxRequests + 1 {
70 f, err := os.CreateTemp(t.TempDir(), "capture")
71 if err != nil {
72 t.Fatal(err)
73 }
74 t.Cleanup(func() { _ = f.Close() })
75 if err := f.Truncate(16); err != nil {
76 t.Fatal(err)
77 }
78 select {
79 case incoming <- ports.CaptureRequest{ID: uint64(i), Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: ports.SHMBuffer{File: f}}:
80 case <-time.After(time.Second):
81 t.Fatal("capture admission blocked")
82 }
83 }
84 select {
85 case rejected := <-replies:
86 if rejected.ID != capture.MaxRequests || rejected.Err == nil {
87 t.Fatalf("batch overflow %+v", rejected)
88 }
89 case <-time.After(time.Second):
90 t.Fatal("batch overflow not answered")
91 }
92 scenes <- ports.Scene{Seq: 1, Scale: 1}
93 for range capture.MaxRequests {
94 select {
95 case result := <-replies:
96 if result.Err != nil {
97 t.Fatalf("capture %+v", result)
98 }
99 case <-time.After(time.Second):
100 t.Fatal("accepted capture not answered")
101 }
102 }
103 select {
104 case <-recycled:
105 case <-time.After(time.Second):
106 t.Fatal("idle output did not return readback lease")
107 }
108 cancel()
109 if err := <-done; err != nil {
110 t.Fatal(err)
111 }
112 frame.Calls = nil
113}