v0.2.0 13c6a90

excluded_test.go RawHistoryFolder

1package capture
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/stretchr/testify/require"
9)
10
11func excludedTestScene() ports.Scene {
12	return ports.Scene{
13		Seq: 5,
14		Windows: []ports.SceneWindow{
15			{ID: 1}, {ID: 20, Popup: true, OverLayers: true}, {ID: 21, Popup: true, OverLayers: true}, {ID: 3},
16		},
17		Layers:  []ports.SceneLayer{{ID: 10, Layer: ports.LayerTop}, {ID: 11, Layer: ports.LayerTop}, {ID: 12, Layer: ports.LayerOverlay}},
18		Capture: &ports.SceneCapture{Session: 7, Excluded: []ports.WindowID{10, 20}},
19	}
20}
21
22func ids(q []ports.CaptureRequest) (out []uint64) {
23	for _, r := range q {
24		out = append(out, r.ID)
25	}
26	return out
27}
28
29func TestExcludedSceneDropsOnlyExcluded(t *testing.T) {
30	p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
31	defer p.Close(nil)
32	s := excludedTestScene()
33	c := p.ExcludedScene(s)
34	require.Nil(t, c.Capture)
35	require.Zero(t, c.Seq)
36	require.Equal(t, []ports.SceneWindow{{ID: 1}, {ID: 21, Popup: true, OverLayers: true}, {ID: 3}}, c.Windows)
37	require.Equal(t, []ports.SceneLayer{{ID: 11, Layer: ports.LayerTop}, {ID: 12, Layer: ports.LayerOverlay}}, c.Layers)
38	// The displayed scene is not mutated.
39	require.Len(t, s.Windows, 4)
40	require.Len(t, s.Layers, 3)
41	require.NotNil(t, s.Capture)
42	require.Equal(t, uint64(5), s.Seq)
43}
44
45func TestSplitNoAlloc(t *testing.T) {
46	p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
47	defer p.Close(nil)
48	for name, s := range map[string]ports.Scene{"no capture": {Windows: excludedTestScene().Windows}, "exclusion": excludedTestScene()} {
49		reqs := make([]ports.CaptureRequest, 3)
50		for i := range reqs {
51			reqs[i].ID = uint64(i + 1)
52		}
53		if allocs := testing.AllocsPerRun(100, func() {
54			_ = p.ExcludedScene(s)
55			p.Split(s, reqs)
56		}); allocs != 0 {
57			t.Fatalf("%s: %.1f allocs, want 0", name, allocs)
58		}
59	}
60}
61
62func TestSplitOrdersAndFailsClosed(t *testing.T) {
63	replies := make(chan ports.CaptureDone, 8)
64	p := NewPipeline(context.Background(), replies)
65	defer p.Close(nil)
66	s := excludedTestScene()
67	s.Capture.Revision = 1
68	reqs := []ports.CaptureRequest{
69		{ID: 1, Exclude: true, Session: 7, CaptureRevision: 1},
70		{ID: 2},
71		{ID: 3, Exclude: true, Session: 8, CaptureRevision: 1}, // another session: never served
72		{ID: 4, Exclude: true, Session: 7, CaptureRevision: 1},
73		{ID: 5},
74	}
75	normal, excluded, hidden := p.Split(s, reqs)
76	require.ElementsMatch(t, []uint64{2, 5}, ids(normal))
77	require.ElementsMatch(t, []uint64{1, 4}, ids(excluded))
78	require.Empty(t, hidden)
79	for _, q := range excluded {
80		require.True(t, q.Exclude)
81	}
82	got := awaitCapture(t, replies)
83	require.Equal(t, uint64(3), got.ID)
84	require.ErrorIs(t, got.Err, ErrSessionInactive)
85	for _, q := range reqs[len(normal)+len(excluded):] {
86		require.True(t, Handed(q), "tail must be zeroed")
87	}
88}
89
90func TestSplitExcludeWithoutSceneExclusionFails(t *testing.T) {
91	replies := make(chan ports.CaptureDone, 4)
92	p := NewPipeline(context.Background(), replies)
93	defer p.Close(nil)
94	s := excludedTestScene()
95	s.Capture = nil
96	normal, excluded, _ := p.Split(s, []ports.CaptureRequest{{ID: 1, Exclude: true, Session: 7}, {ID: 2}})
97	require.Equal(t, []uint64{2}, ids(normal))
98	require.Empty(t, excluded)
99	got := awaitCapture(t, replies)
100	require.Equal(t, uint64(1), got.ID)
101	require.ErrorIs(t, got.Err, ErrSessionInactive)
102}
103
104// An exclusion with nothing to hide in the scene is served from the displayed
105// frame: no second composition.
106func TestSplitNoSecondCompositionWhenNothingExcludedIsShown(t *testing.T) {
107	p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
108	defer p.Close(nil)
109	s := excludedTestScene()
110	s.Capture.Excluded = []ports.WindowID{99}
111	normal, excluded, _ := p.Split(s, []ports.CaptureRequest{{ID: 1, Exclude: true, Session: 7}, {ID: 2}})
112	require.Len(t, normal, 2)
113	require.Empty(t, excluded)
114}
115
116// Every other capture keeps the HUD, even while an exclusion is live.
117func TestSplitStandardCapturesKeepExclusionScene(t *testing.T) {
118	p := NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
119	defer p.Close(nil)
120	normal, excluded, _ := p.Split(excludedTestScene(), []ports.CaptureRequest{{ID: 1}, {ID: 2}})
121	require.Len(t, normal, 2)
122	require.Empty(t, excluded)
123}
124
125// A request stamped with a revision the scene has not reached is refused:
126// the scene's exclusion list may predate a newly mapped HUD layer.
127func TestSplitStaleRevisionFailsClosed(t *testing.T) {
128	replies := make(chan ports.CaptureDone, 4)
129	p := NewPipeline(context.Background(), replies)
130	defer p.Close(nil)
131	s := excludedTestScene()
132	s.Capture.Revision = 3
133	normal, excluded, _ := p.Split(s, []ports.CaptureRequest{
134		{ID: 1, Exclude: true, Session: 7, CaptureRevision: 4},
135		{ID: 2, Exclude: true, Session: 7, CaptureRevision: 3},
136		{ID: 3, Exclude: true, Session: 7, CaptureRevision: 2},
137	})
138	require.Empty(t, normal)
139	require.Len(t, excluded, 2)
140	got := awaitCapture(t, replies)
141	require.Equal(t, uint64(1), got.ID)
142	require.ErrorIs(t, got.Err, ErrSessionStale)
143}
144
145// A workspace request is served only if its workspace is still where the
146// request expected: on screen (Shown), or the one rendered off screen.
147func TestSplitWorkspaceMovedFailsClosed(t *testing.T) {
148	replies := make(chan ports.CaptureDone, 8)
149	p := NewPipeline(context.Background(), replies)
150	defer p.Close(nil)
151	s := ports.Scene{Capture: &ports.SceneCapture{Shown: 3, Workspace: 4}}
152	reqs := []ports.CaptureRequest{
153		{ID: 1, Workspace: 3},                  // on screen, still shown
154		{ID: 2, Workspace: 4, OffScreen: true}, // off screen, still rendered
155		{ID: 3, Workspace: 4},                  // was on screen, now hidden
156		{ID: 4, Workspace: 3, OffScreen: true}, // was hidden, now shown
157		{ID: 5, Workspace: 9, OffScreen: true}, // not rendered at all
158		{ID: 6},
159	}
160	normal, excluded, hidden := p.Split(s, reqs)
161	require.ElementsMatch(t, []uint64{1, 6}, ids(normal))
162	require.Empty(t, excluded)
163	require.Equal(t, []uint64{2}, ids(hidden))
164	failed := map[uint64]bool{}
165	for range 3 {
166		d := awaitCapture(t, replies)
167		require.ErrorIs(t, d.Err, ErrWorkspaceMoved)
168		failed[d.ID] = true
169	}
170	require.Equal(t, map[uint64]bool{3: true, 4: true, 5: true}, failed)
171	// Without any capture state no workspace request is served.
172	normal, _, hidden = p.Split(ports.Scene{}, []ports.CaptureRequest{{ID: 7, Workspace: 3}})
173	require.Empty(t, normal)
174	require.Empty(t, hidden)
175	require.ErrorIs(t, awaitCapture(t, replies).Err, ErrWorkspaceMoved)
176}

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