offscreen_test.go RawHistoryFolder

1package capture
2
3import (
4	"context"
5	"errors"
6	"image"
7	"os"
8	"testing"
9	"time"
10
11	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/stretchr/testify/mock"
14	"github.com/stretchr/testify/require"
15)
16
17func hiddenScene() ports.Scene {
18	return ports.Scene{
19		Scale: 2, OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{{ID: 1}},
20		Capture: &ports.SceneCapture{Workspace: 9},
21		CaptureScene: &ports.Scene{
22			OutputWidth: 100, OutputHeight: 50, Scale: 2, Seq: 9,
23			Windows: []ports.SceneWindow{{ID: 5}, {ID: 10, Popup: true}},
24			Layers:  []ports.SceneLayer{{ID: 10}, {ID: 11}},
25		},
26	}
27}
28
29func hiddenRequest(t *testing.T, id uint64, region image.Rectangle) ports.CaptureRequest {
30	q := pipelineRequest(t, id)
31	q.Workspace, q.OffScreen = 9, true
32	q.Region, q.Width, q.Height, q.Stride = region, region.Dx(), region.Dy(), region.Dx()*4
33	require.NoError(t, q.Dst.File.Truncate(int64(q.Stride*q.Height)))
34	return q
35}
36
37func childMock(t *testing.T, w, h int) (*portsmocks.MockRenderer, *portsmocks.MockCaptureFrame) {
38	r := portsmocks.NewMockRenderer(t)
39	frame := portsmocks.NewMockCaptureFrame(t)
40	frame.EXPECT().Done().Return(nil).Maybe()
41	frame.EXPECT().Read(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
42	r.EXPECT().EndCapture(frame).Return().Maybe()
43	r.EXPECT().Close().Return().Once()
44	r.EXPECT().BeginCapture().Return(frame, nil).Once()
45	return r, frame
46}
47
48func TestSubmitHiddenDrawsChildAndRebasesRegion(t *testing.T) {
49	replies := make(chan ports.CaptureDone, 4)
50	p := NewPipeline(context.Background(), replies)
51	child, _ := childMock(t, 200, 100)
52	var scene ports.Scene
53	child.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
54		scene = s
55		return nil, nil
56	}).Once()
57	var made [2]int
58	p.EnableOffscreen(func(w, h int) (ports.Renderer, error) { made = [2]int{w, h}; return child, nil })
59	s := hiddenScene()
60	p.SubmitHidden(s, nil, []ports.CaptureRequest{hiddenRequest(t, 1, image.Rect(480, 360, 680, 460))})
61	require.Equal(t, [2]int{200, 100}, made, "child image is the frame in physical pixels")
62	require.Nil(t, scene.Capture)
63	require.Zero(t, scene.Seq)
64	require.Equal(t, []ports.SceneWindow{{ID: 5}, {ID: 10, Popup: true}}, scene.Windows)
65	require.Equal(t, []ports.SceneLayer{{ID: 10}, {ID: 11}}, scene.Layers)
66	require.NoError(t, awaitCapture(t, replies).Err)
67	p.Close(nil)
68}
69
70func TestSubmitHiddenFailsClosed(t *testing.T) {
71	for _, tc := range []struct {
72		name string
73		edit func(*ports.Scene)
74		want error
75	}{
76		{"output off", func(s *ports.Scene) { s.Off = true }, nil},
77		{"no hidden workspace in the scene", func(s *ports.Scene) { s.Capture.Workspace = 0 }, ErrOffscreenUnavailable},
78		{"frame too large", func(s *ports.Scene) { s.CaptureScene.OutputWidth = 20000 }, ErrOffscreenGeometry},
79		{"frame empty", func(s *ports.Scene) { s.CaptureScene.OutputWidth = 0 }, ErrOffscreenGeometry},
80	} {
81		t.Run(tc.name, func(t *testing.T) {
82			replies := make(chan ports.CaptureDone, 2)
83			p := NewPipeline(context.Background(), replies)
84			defer p.Close(nil)
85			p.EnableOffscreen(func(int, int) (ports.Renderer, error) { t.Fatal("factory called"); return nil, nil })
86			s := hiddenScene()
87			tc.edit(&s)
88			p.SubmitHidden(s, nil, []ports.CaptureRequest{hiddenRequest(t, 1, image.Rect(0, 0, 200, 100))})
89			got := awaitCapture(t, replies)
90			require.Error(t, got.Err)
91			if tc.want != nil {
92				require.ErrorIs(t, got.Err, tc.want)
93			}
94		})
95	}
96}
97
98func TestSubmitHiddenFactoryFailureAndNoFactory(t *testing.T) {
99	replies := make(chan ports.CaptureDone, 4)
100	p := NewPipeline(context.Background(), replies)
101	defer p.Close(nil)
102	p.SubmitHidden(hiddenScene(), nil, []ports.CaptureRequest{hiddenRequest(t, 1, image.Rect(0, 0, 200, 100))})
103	require.ErrorIs(t, awaitCapture(t, replies).Err, ErrOffscreenUnavailable)
104	p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return nil, errors.New("no gpu") })
105	p.SubmitHidden(hiddenScene(), nil, []ports.CaptureRequest{hiddenRequest(t, 2, image.Rect(0, 0, 200, 100))})
106	require.ErrorIs(t, awaitCapture(t, replies).Err, ErrOffscreenUnavailable)
107}
108
109func TestSubmitHiddenRegionOutsideFrameFailsOthersServed(t *testing.T) {
110	replies := make(chan ports.CaptureDone, 4)
111	p := NewPipeline(context.Background(), replies)
112	child, _ := childMock(t, 200, 100)
113	child.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
114	p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
115	p.SubmitHidden(hiddenScene(), nil, []ports.CaptureRequest{
116		hiddenRequest(t, 1, image.Rect(0, 0, 300, 100)), // not the child's size
117		hiddenRequest(t, 2, image.Rect(0, 0, 200, 100)),
118	})
119	byID := map[uint64]error{}
120	for range 2 {
121		d := awaitCapture(t, replies)
122		byID[d.ID] = d.Err
123	}
124	require.ErrorIs(t, byID[1], ErrOffscreenGeometry)
125	require.NoError(t, byID[2])
126	p.Close(nil)
127}
128
129// A hidden-workspace capture submitted while the indicator gate is open is held
130// by the child pipeline: no reply until the owner's verdict, then it is
131// released (nil) or failed (error) like a displayed capture.
132func TestSubmitHiddenHeldByGateUntilVerdict(t *testing.T) {
133	for _, tc := range []struct {
134		name    string
135		verdict error
136	}{
137		{"released", nil},
138		{"failed", GateVerdict(errors.New("frame dropped"))},
139	} {
140		t.Run(tc.name, func(t *testing.T) {
141			replies := make(chan ports.CaptureDone, 4)
142			p := NewPipeline(context.Background(), replies)
143			child, _ := childMock(t, 200, 100)
144			child.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
145			p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
146			p.BeginGate()
147			p.SubmitHidden(hiddenScene(), nil, []ports.CaptureRequest{hiddenRequest(t, 1, image.Rect(0, 0, 200, 100))})
148			select {
149			case d := <-replies:
150				t.Fatalf("answered before the gate verdict: %v", d.Err)
151			case <-time.After(100 * time.Millisecond):
152			}
153			p.EndGate(tc.verdict)
154			got := awaitCapture(t, replies)
155			if tc.verdict == nil {
156				require.NoError(t, got.Err)
157			} else {
158				require.ErrorIs(t, got.Err, ErrIndicatorMissing)
159			}
160			p.Close(nil)
161		})
162	}
163}
164
165func TestSubmitHiddenChildClosesWhenSessionEnds(t *testing.T) {
166	replies := make(chan ports.CaptureDone, 4)
167	p := NewPipeline(context.Background(), replies)
168	child, _ := childMock(t, 200, 100)
169	child.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
170	p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
171	p.SubmitHidden(hiddenScene(), nil, []ports.CaptureRequest{hiddenRequest(t, 1, image.Rect(0, 0, 200, 100))})
172	require.NoError(t, awaitCapture(t, replies).Err)
173	// Wait for the worker to hand the lease back, then the session ends.
174	b := <-p.HiddenCompleted()
175	p.RecycleHidden(b)
176	p.Retire(ports.Scene{})
177	require.Nil(t, p.off.r, "child closed once the scene has no CaptureScene")
178	p.Close(nil)
179}
180
181func TestHiddenNilPipelineSafe(t *testing.T) {
182	var p *Pipeline
183	require.False(t, p.HiddenReading())
184	p.Retire(ports.Scene{})
185}
186
187// A window is drawn by the child like a hidden workspace; another window or
188// a workspace needs a new child.
189func TestSubmitHiddenWindow(t *testing.T) {
190	replies := make(chan ports.CaptureDone, 4)
191	p := NewPipeline(context.Background(), replies)
192	child, _ := childMock(t, 200, 100)
193	child.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
194	made := 0
195	p.EnableOffscreen(func(w, h int) (ports.Renderer, error) { made++; return child, nil })
196	s := hiddenScene()
197	s.Capture = &ports.SceneCapture{Window: 5}
198	q := hiddenRequest(t, 1, image.Rect(0, 0, 200, 100))
199	q.Workspace, q.Window = 0, 5
200	p.SubmitHidden(s, nil, []ports.CaptureRequest{q})
201	require.NoError(t, awaitCapture(t, replies).Err)
202	require.Equal(t, 1, made)
203	require.Equal(t, offTarget{window: 5}, p.off.target)
204	p.Close(nil)
205}

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 25

Tags 10

↑↓ moveenter openesc close