hint_capture_test.go RawHistoryFolder

1package capture
2
3import (
4	"context"
5	"image"
6	"os"
7	"strings"
8	"sync"
9	"testing"
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
17const internalID = ports.CaptureIDInternal | 1
18
19// hintFrame describes a scene for hint.capture tests: what it draws and
20// whether its exclusion is active.
21type hintFrame struct {
22	capture   bool // hint.capture
23	labels    bool // hint mode is shown on this output
24	indicator bool
25	posHint   bool
26	exclusion bool // the scene carries an exclusion session (7) hiding drawn layers
27	unshown   bool // ... hiding a surface the scene does not draw
28}
29
30func (f hintFrame) scene() ports.Scene {
31	s := excludedTestScene()
32	s.Capture = nil
33	if f.exclusion {
34		s.Capture = &ports.SceneCapture{Session: 7, Excluded: []ports.WindowID{10, 20}}
35	}
36	if f.unshown {
37		s.Capture = &ports.SceneCapture{Session: 7, Excluded: []ports.WindowID{99}}
38	}
39	s.HintStyle.Capture = f.capture
40	if f.labels {
41		s.HintLabels = []ports.HintLabel{{Text: "f"}}
42	}
43	if f.indicator {
44		s.CaptureIndicators = []ports.CaptureIndicator{{Rect: ports.Rect{W: 10, H: 10}}}
45	}
46	if f.posHint {
47		s.PositionHint = ports.PositionHint{Opacity: 1, Columns: 1}
48	}
49	return s
50}
51
52// drawn names what a rendered scene shows: its labels, indicator and
53// position hint, and "excluded" when the excluded layers are gone.
54func drawn(s ports.Scene) string {
55	var parts []string
56	if len(s.HintLabels) > 0 {
57		parts = append(parts, "labels")
58	}
59	if len(s.CaptureIndicators) > 0 {
60		parts = append(parts, "indicator")
61	}
62	if s.PositionHint.Shown() {
63		parts = append(parts, "poshint")
64	}
65	if len(s.Layers) == 2 {
66		parts = append(parts, "excluded")
67	}
68	if len(parts) == 0 {
69		return "bare"
70	}
71	return strings.Join(parts, "+")
72}
73
74// frameRun is what one output frame did, as the renderer saw it.
75type frameRun struct {
76	renders []string         // drawn() of every Render, in call order
77	batches []int            // requests copied by each BeginCapture, in call order
78	results map[uint64]error // reply of every request
79}
80
81// runFrame serves the requests the way the outputs do: Split, SubmitPlain,
82// the excluded frame, then the displayed frame and what still reads it.
83func runFrame(t *testing.T, s ports.Scene, reqs ...ports.CaptureRequest) frameRun {
84	t.Helper()
85	replies := make(chan ports.CaptureDone, len(reqs))
86	p := NewPipeline(context.Background(), replies)
87	r := portsmocks.NewMockRenderer(t)
88	var mu sync.Mutex
89	var run frameRun
90	var frames []*portsmocks.MockCaptureFrame
91	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
92		mu.Lock()
93		defer mu.Unlock()
94		run.renders = append(run.renders, drawn(s))
95		return nil, nil
96	}).Maybe()
97	r.EXPECT().BeginCapture().RunAndReturn(func() (ports.CaptureFrame, error) {
98		mu.Lock()
99		defer mu.Unlock()
100		n := len(run.batches)
101		run.batches = append(run.batches, 0)
102		f := portsmocks.NewMockCaptureFrame(t)
103		f.EXPECT().Done().Return((*os.File)(nil)).Maybe()
104		f.EXPECT().Read(image.Rect(0, 0, 2, 2), mock.Anything, 8).RunAndReturn(func(image.Rectangle, []byte, int) error {
105			mu.Lock()
106			defer mu.Unlock()
107			run.batches[n]++
108			return nil
109		}).Maybe()
110		frames = append(frames, f)
111		return f, nil
112	}).Maybe()
113	r.EXPECT().EndCapture(mock.Anything).Return().Maybe()
114
115	normal, excluded, hidden := p.Split(s, reqs)
116	require.Empty(t, hidden)
117	took, done, err := p.SubmitPlain(r, s, nil, normal)
118	require.NoError(t, err)
119	require.Nil(t, done)
120	if took {
121		normal = nil
122	}
123	if len(excluded) > 0 {
124		_, _ = r.Render(p.ExcludedScene(s), nil)
125		p.SubmitScoped(s.Security, r, excluded)
126	}
127	_, _ = r.Render(s, nil)
128	p.SubmitScoped(s.Security, r, normal)
129
130	run.results = map[uint64]error{}
131	for range reqs {
132		d := awaitCapture(t, replies)
133		run.results[d.ID] = d.Err
134	}
135	p.Close(r)
136	for _, f := range frames {
137		f.Calls = nil // Read's arguments point into unmapped destinations
138	}
139	return run
140}
141
142func client(t *testing.T, id uint64) ports.CaptureRequest { return pipelineRequest(t, id) }
143
144func clientExclude(t *testing.T, id uint64) ports.CaptureRequest {
145	q := pipelineRequest(t, id)
146	q.Exclude, q.Session = true, 7
147	return q
148}
149
150func internalReq(t *testing.T, exclude bool) ports.CaptureRequest {
151	q := pipelineRequest(t, internalID)
152	if exclude {
153		q.Exclude, q.Session = true, 7
154	}
155	return q
156}
157
158// Every frame shape hint.capture can produce: what is rendered, how many
159// captures each batch copies, which requests fail. The displayed frame is
160// always the last render. No frame needs more than two batches.
161func TestHintCaptureFrames(t *testing.T) {
162	const (
163		displayed = "labels"
164		bare      = "bare"
165	)
166	tests := []struct {
167		name    string
168		frame   hintFrame
169		reqs    func(t *testing.T) []ports.CaptureRequest
170		renders []string
171		batches []int
172		failed  map[uint64]error // want errors (ErrorIs); every other request succeeds
173	}{
174		{
175			name:  "off: labels leave every capture, one plain frame serves both",
176			frame: hintFrame{labels: true},
177			reqs: func(t *testing.T) []ports.CaptureRequest {
178				return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
179			},
180			renders: []string{bare, displayed},
181			batches: []int{2},
182		},
183		{
184			name:    "on: a client capture reads the displayed frame, labels included",
185			frame:   hintFrame{capture: true, labels: true},
186			reqs:    func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{client(t, 1)} },
187			renders: []string{displayed},
188			batches: []int{1},
189		},
190		{
191			name:    "on: the indicator is dropped, the labels are kept",
192			frame:   hintFrame{capture: true, labels: true, indicator: true},
193			reqs:    func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{client(t, 1)} },
194			renders: []string{"labels", "labels+indicator"},
195			batches: []int{1},
196		},
197		{
198			name:    "on: the position hint is dropped, the labels are kept",
199			frame:   hintFrame{capture: true, labels: true, posHint: true},
200			reqs:    func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{client(t, 1)} },
201			renders: []string{"labels", "labels+poshint"},
202			batches: []int{1},
203		},
204		{
205			name:  "on: an internal capture of a labelled frame fails, the client is served",
206			frame: hintFrame{capture: true, labels: true, indicator: true},
207			reqs: func(t *testing.T) []ports.CaptureRequest {
208				return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
209			},
210			renders: []string{"labels", "labels+indicator"},
211			batches: []int{1},
212			failed:  map[uint64]error{internalID: ErrInternalLabelled},
213		},
214		{
215			name:  "on: an internal capture alone fails and renders nothing for it",
216			frame: hintFrame{capture: true, labels: true},
217			reqs:  func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{internalReq(t, false)} },
218			// the displayed frame is still drawn by the output, with no batch
219			renders: []string{displayed},
220			failed:  map[uint64]error{internalID: ErrInternalLabelled},
221		},
222		{
223			name:  "on: an internal Exclude capture fails, its excluded frame has labels",
224			frame: hintFrame{capture: true, labels: true, exclusion: true},
225			reqs:  func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{internalReq(t, true)} },
226			// no excluded frame is rendered for a failed request
227			renders: []string{displayed},
228			failed:  map[uint64]error{internalID: ErrInternalExclude},
229		},
230		{
231			name:  "on: client, internal and Exclude requests over an indicator fit two batches",
232			frame: hintFrame{capture: true, labels: true, indicator: true, posHint: true, exclusion: true},
233			reqs: func(t *testing.T) []ports.CaptureRequest {
234				return []ports.CaptureRequest{client(t, 1), internalReq(t, false), clientExclude(t, 3)}
235			},
236			// plain frame for the client, excluded frame (labels kept, no
237			// indicator, no position hint) for Exclude, then the displayed one
238			renders: []string{"labels", "labels+excluded", "labels+indicator+poshint"},
239			batches: []int{1, 1},
240			failed:  map[uint64]error{internalID: ErrInternalLabelled},
241		},
242		{
243			name:    "on: labels alone do not force the excluded frame",
244			frame:   hintFrame{capture: true, labels: true, unshown: true},
245			reqs:    func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{clientExclude(t, 1)} },
246			renders: []string{displayed},
247			batches: []int{1},
248		},
249		{
250			name:    "off: labels force the excluded frame, without labels",
251			frame:   hintFrame{labels: true, exclusion: true},
252			reqs:    func(t *testing.T) []ports.CaptureRequest { return []ports.CaptureRequest{clientExclude(t, 1)} },
253			renders: []string{"excluded", displayed},
254			batches: []int{1},
255		},
256		{
257			name:  "on without labels shown: internal and client share the plain frame",
258			frame: hintFrame{capture: true, indicator: true},
259			reqs: func(t *testing.T) []ports.CaptureRequest {
260				return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
261			},
262			renders: []string{bare, "indicator"},
263			batches: []int{2},
264		},
265		{
266			name:  "nothing to hide: the displayed frame serves everyone",
267			frame: hintFrame{capture: true},
268			reqs: func(t *testing.T) []ports.CaptureRequest {
269				return []ports.CaptureRequest{client(t, 1), internalReq(t, false)}
270			},
271			renders: []string{bare},
272			batches: []int{2},
273		},
274	}
275	for _, tt := range tests {
276		t.Run(tt.name, func(t *testing.T) {
277			run := runFrame(t, tt.frame.scene(), tt.reqs(t)...)
278			require.Equal(t, tt.renders, run.renders)
279			require.Equal(t, tt.batches, run.batches)
280			require.LessOrEqual(t, len(run.batches), pipelineSlots)
281			for id, err := range run.results {
282				if want, ok := tt.failed[id]; ok {
283					require.ErrorIs(t, err, want, "request %#x", id)
284					require.ErrorIs(t, err, ports.ErrCaptureTransient)
285				} else {
286					require.NoError(t, err, "request %#x", id)
287				}
288			}
289		})
290	}
291}
292
293// Splitting and building the excluded scene of a frame that keeps labels
294// allocates nothing.
295func TestSplitNoAllocHintCapture(t *testing.T) {
296	p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 8))
297	defer p.Close(nil)
298	s := hintFrame{capture: true, labels: true, indicator: true, exclusion: true}.scene()
299	reqs := make([]ports.CaptureRequest, 3)
300	if allocs := testing.AllocsPerRun(100, func() {
301		reqs[0] = ports.CaptureRequest{ID: 1}
302		reqs[1] = ports.CaptureRequest{ID: 2, Exclude: true, Session: 7}
303		reqs[2] = ports.CaptureRequest{ID: 3, Exclude: true, Session: 7}
304		_ = p.ExcludedScene(s)
305		p.Split(s, reqs)
306	}); allocs != 0 {
307		t.Fatalf("%.1f allocs, want 0", allocs)
308	}
309}

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 23

Tags 10

↑↓ moveenter openesc close