main f2d166b

source_test.go RawHistoryFolder

1package atspi
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"runtime"
8	"testing"
9	"time"
10
11	"github.com/stretchr/testify/mock"
12	"github.com/stretchr/testify/require"
13
14	"github.com/bnema/neferwl/internal/ports"
15)
16
17const (
18	stActive  = 1 << stateActive
19	stShowing = 1 << stateShowing
20)
21
22// tree answers calls like an app: registry with one app (:1.5, pid 77) with
23// one showing frame whose links sit at window-relative extents.
24func answerTree(frameExt [4]int32, matches []ref, ext map[string][4]int32, roles map[string]uint32, noCollection bool) func(time.Time, time.Duration, []call) []reply {
25	return func(_ time.Time, _ time.Duration, calls []call) []reply {
26		out := make([]reply, len(calls))
27		for i, c := range calls {
28			switch {
29			case c.op == opChildren && c.name == registryName:
30				out[i].refs = []ref{{":1.5", "/root"}, {":1.9", "/root"}}
31			case c.op == opPID:
32				out[i].u = map[string]uint32{":1.5": 77, ":1.9": 99}[c.name]
33			case c.op == opChildren && c.path == "/root":
34				out[i].refs = []ref{{":1.5", "/frame"}}
35			case c.op == opRole && c.path == "/frame":
36				out[i].u = roleFrame
37			case c.op == opState && c.path == "/frame":
38				out[i].state = stShowing | stActive
39			case c.op == opExtents && c.path == "/frame":
40				out[i].rect = frameExt
41			case c.op == opMatches:
42				if noCollection {
43					out[i].err = &busError{name: "org.freedesktop.DBus.Error.UnknownMethod"}
44				} else {
45					out[i].refs = matches
46				}
47			case c.op == opChildren && c.path == "/frame":
48				out[i].refs = matches
49			case c.op == opChildren:
50			case c.op == opRole:
51				out[i].u = roles[c.path]
52			case c.op == opState:
53				out[i].state = stShowing | 1<<stateVisible
54			case c.op == opExtents:
55				out[i].rect = ext[c.path]
56			}
57		}
58		return out
59	}
60}
61
62func TestTargetsCoversWindowAndConverts(t *testing.T) {
63	tr := newMocktransport(t)
64	matches := []ref{{":1.5", "/a"}, {":1.5", "/b"}, {":1.5", "/zero"}}
65	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
66		[4]int32{4, 30, 800, 600}, matches,
67		map[string][4]int32{"/a": {14, 130, 50, 16}, "/b": {104, 230, 20, 20}, "/zero": {5, 5, 0, 0}},
68		map[string]uint32{"/a": roleLink, "/b": rolePushButton, "/zero": roleLink}, false))
69	s := &Source{tr: tr}
70
71	windows := []ports.HintWindow{
72		{ID: 1, PID: 77, Rect: ports.Rect{X: 100, Y: 200, W: 800, H: 600}, Focused: true},
73		{ID: 2, PID: 5, Rect: ports.Rect{X: 0, Y: 0, W: 50, H: 50}},
74	}
75	targets, covered, err := s.Targets(context.Background(), windows, false)
76	require.NoError(t, err)
77	require.Equal(t, map[ports.WindowID]bool{1: true}, covered)
78	require.Equal(t, []ports.HintTarget{
79		{Rect: ports.Rect{X: 110, Y: 300, W: 50, H: 16}, Kind: ports.HintLink, Window: 1},
80		{Rect: ports.Rect{X: 200, Y: 400, W: 20, H: 20}, Kind: ports.HintButton, Window: 1},
81	}, targets)
82}
83
84func TestTargetsLinksOnlyDropsButtons(t *testing.T) {
85	tr := newMocktransport(t)
86	matches := []ref{{":1.5", "/a"}, {":1.5", "/b"}}
87	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
88		[4]int32{0, 0, 800, 600}, matches,
89		map[string][4]int32{"/a": {14, 130, 50, 16}, "/b": {104, 230, 20, 20}},
90		map[string]uint32{"/a": roleLink, "/b": rolePushButton}, false))
91	s := &Source{tr: tr}
92	targets, _, err := s.Targets(context.Background(), []ports.HintWindow{{ID: 1, PID: 77, Rect: ports.Rect{W: 800, H: 600}}}, true)
93	require.NoError(t, err)
94	require.Len(t, targets, 1)
95	require.Equal(t, ports.HintLink, targets[0].Kind)
96}
97
98func TestTargetsWalkWithoutCollection(t *testing.T) {
99	tr := newMocktransport(t)
100	matches := []ref{{":1.5", "/a"}}
101	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
102		[4]int32{0, 0, 800, 600}, matches,
103		map[string][4]int32{"/a": {14, 130, 50, 16}},
104		map[string]uint32{"/a": roleLink}, true))
105	s := &Source{tr: tr}
106	targets, covered, err := s.Targets(context.Background(), []ports.HintWindow{{ID: 1, PID: 77, Rect: ports.Rect{W: 800, H: 600}}}, false)
107	require.NoError(t, err)
108	require.Len(t, targets, 1)
109	require.True(t, covered[1])
110}
111
112func TestTargetsNoPIDMatchCoversNothing(t *testing.T) {
113	tr := newMocktransport(t)
114	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree([4]int32{}, nil, nil, nil, false))
115	s := &Source{tr: tr}
116	targets, covered, err := s.Targets(context.Background(), []ports.HintWindow{{ID: 1, PID: 1234, Rect: ports.Rect{W: 8, H: 6}}}, false)
117	require.NoError(t, err)
118	require.Empty(t, targets)
119	require.Empty(t, covered)
120}
121
122func TestTargetsWithoutPIDsAsksNothing(t *testing.T) {
123	s := &Source{tr: newMocktransport(t)}
124	_, covered, err := s.Targets(context.Background(), []ports.HintWindow{{ID: 1, Rect: ports.Rect{W: 8, H: 6}}}, false)
125	require.NoError(t, err)
126	require.Empty(t, covered)
127}
128
129func TestCoverageRequiresLinkOrDocument(t *testing.T) {
130	for _, collection := range []bool{true, false} {
131		for _, role := range []uint32{rolePushButton, roleDocumentFrame, roleDocumentWeb, roleLink} {
132			t.Run(fmt.Sprintf("collection=%t/role=%d", collection, role), func(t *testing.T) {
133				tr := newMocktransport(t)
134				tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
135					[4]int32{0, 0, 800, 600}, []ref{{":1.5", "/a"}}, map[string][4]int32{"/a": {10, 20, 30, 40}}, map[string]uint32{"/a": role}, !collection))
136				s := &Source{tr: tr}
137				targets, covered, err := s.Targets(context.Background(), []ports.HintWindow{{ID: 1, PID: 77, Rect: ports.Rect{W: 800, H: 600}}}, false)
138				require.NoError(t, err)
139				require.Equal(t, role != rolePushButton, covered[1])
140				if isDocument(role) {
141					require.Empty(t, targets)
142				} else {
143					require.Len(t, targets, 1)
144				}
145			})
146		}
147	}
148}
149
150var oneWindow = []ports.HintWindow{{ID: 1, PID: 77, Rect: ports.Rect{W: 800, H: 600}}}
151
152// New does not touch the bus; the first Targets call connects, once.
153func TestConnectsLazilyOnFirstTargets(t *testing.T) {
154	tr := newMocktransport(t)
155	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
156		[4]int32{0, 0, 800, 600}, []ref{{":1.5", "/a"}}, map[string][4]int32{"/a": {1, 1, 9, 9}}, map[string]uint32{"/a": roleLink}, false))
157	c := newMockconnector(t)
158	c.EXPECT().Connect(mock.Anything).Return(tr, nil).Once()
159	s := &Source{dial: c}
160	for range 2 {
161		targets, _, err := s.Targets(context.Background(), oneWindow, false)
162		require.NoError(t, err)
163		require.Len(t, targets, 1)
164	}
165}
166
167// Without PIDs nothing is asked and nothing is connected.
168func TestNoConnectWithoutWindows(t *testing.T) {
169	s := &Source{dial: newMockconnector(t)}
170	_, _, err := s.Targets(context.Background(), []ports.HintWindow{{ID: 1}}, false)
171	require.NoError(t, err)
172}
173
174// A failed connection is retried no sooner than retryEvery; meanwhile no
175// targets are returned.
176func TestConnectRetriesWithBackoff(t *testing.T) {
177	c := newMockconnector(t)
178	c.EXPECT().Connect(mock.Anything).Return(nil, errors.New("down")).Once()
179	s := &Source{dial: c}
180	for range 3 {
181		targets, covered, err := s.Targets(context.Background(), oneWindow, false)
182		require.NoError(t, err)
183		require.Empty(t, targets)
184		require.Empty(t, covered)
185	}
186	tr := newMocktransport(t)
187	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
188		[4]int32{0, 0, 800, 600}, []ref{{":1.5", "/a"}}, map[string][4]int32{"/a": {1, 1, 9, 9}}, map[string]uint32{"/a": roleLink}, false))
189	c.EXPECT().Connect(mock.Anything).Return(tr, nil).Once()
190	s.retryAt = time.Now().Add(-time.Second)
191	targets, _, err := s.Targets(context.Background(), oneWindow, false)
192	require.NoError(t, err)
193	require.Len(t, targets, 1)
194}
195
196// A broken connection is closed and dropped; the next attempt reconnects.
197func TestBrokenConnectionIsDropped(t *testing.T) {
198	tr := newMocktransport(t)
199	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
200		func(_ time.Time, _ time.Duration, calls []call) []reply {
201			out := make([]reply, len(calls))
202			for i := range out {
203				out[i].err = ErrBroken
204			}
205			return out
206		}).Once()
207	tr.EXPECT().Close().Return(nil).Once()
208	c := newMockconnector(t)
209	c.EXPECT().Connect(mock.Anything).Return(tr, nil).Once()
210	s := &Source{dial: c}
211	_, _, err := s.Targets(context.Background(), oneWindow, false)
212	require.Error(t, err)
213	require.Nil(t, s.tr)
214	targets, _, err := s.Targets(context.Background(), oneWindow, false)
215	require.NoError(t, err)
216	require.Empty(t, targets) // backoff: no new attempt yet
217}
218
219const testWait = 20 * time.Millisecond
220
221// blockedConnector returns a connector whose Connect blocks until release is
222func blockedConnector(t *testing.T, release <-chan struct{}, tr transport, err error, times int) *mockconnector {
223	t.Helper()
224	c := newMockconnector(t)
225	c.EXPECT().Connect(mock.Anything).RunAndReturn(func(context.Context) (transport, error) {
226		<-release
227		return tr, err
228	}).Times(times)
229	return c
230}
231
232// A silent service cannot hold Targets beyond the wait, is not doubled while
233// in flight, and its late transport is used.
234func TestConnectIsBoundedAndNotDoubled(t *testing.T) {
235	release := make(chan struct{})
236	tr := newMocktransport(t)
237	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
238		[4]int32{0, 0, 800, 600}, []ref{{":1.5", "/a"}}, map[string][4]int32{"/a": {1, 1, 9, 9}}, map[string]uint32{"/a": roleLink}, false))
239	c := blockedConnector(t, release, tr, nil, 1)
240	s := &Source{dial: c, wait: testWait}
241	t0 := time.Now()
242	targets, _, err := s.Targets(context.Background(), oneWindow, false)
243	require.NoError(t, err)
244	require.Empty(t, targets)
245	require.Less(t, time.Since(t0), testWait+200*time.Millisecond)
246	require.NotNil(t, s.attempt)
247	// Still in flight: no second Connect (Times(1)), and no blocking.
248	targets, _, err = s.Targets(context.Background(), oneWindow, false)
249	require.NoError(t, err)
250	require.Empty(t, targets)
251	// It connects late: the next call uses it.
252	close(release)
253	require.Eventually(t, func() bool { return len(s.attempt) == 1 }, time.Second, time.Millisecond)
254	targets, _, err = s.Targets(context.Background(), oneWindow, false)
255	require.NoError(t, err)
256	require.Len(t, targets, 1)
257	require.Nil(t, s.attempt)
258}
259
260// An abandoned attempt that fails applies the backoff, then one new attempt.
261func TestAbandonedAttemptErrorBacksOff(t *testing.T) {
262	release := make(chan struct{})
263	tr := newMocktransport(t)
264	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(answerTree(
265		[4]int32{0, 0, 800, 600}, []ref{{":1.5", "/a"}}, map[string][4]int32{"/a": {1, 1, 9, 9}}, map[string]uint32{"/a": roleLink}, false))
266	c := newMockconnector(t)
267	c.EXPECT().Connect(mock.Anything).RunAndReturn(func(context.Context) (transport, error) {
268		<-release
269		return nil, errors.New("down")
270	}).Once()
271	c.EXPECT().Connect(mock.Anything).Return(tr, nil).Once()
272	s := &Source{dial: c, wait: testWait}
273	targets, _, err := s.Targets(context.Background(), oneWindow, false)
274	require.NoError(t, err)
275	require.Empty(t, targets)
276	close(release)
277	require.Eventually(t, func() bool { return len(s.attempt) == 1 }, time.Second, time.Millisecond)
278	// The error is collected; the backoff runs from its finish time.
279	targets, _, err = s.Targets(context.Background(), oneWindow, false)
280	require.NoError(t, err)
281	require.Empty(t, targets)
282	require.Nil(t, s.attempt)
283	require.WithinDuration(t, time.Now().Add(retryEvery), s.retryAt, time.Second)
284	targets, _, err = s.Targets(context.Background(), oneWindow, false) // backoff: no Connect
285	require.NoError(t, err)
286	require.Empty(t, targets)
287	s.retryAt = time.Now().Add(-time.Second)
288	targets, _, err = s.Targets(context.Background(), oneWindow, false)
289	require.NoError(t, err)
290	require.Len(t, targets, 1)
291}
292
293// Close with an attempt in flight closes the late transport; afterwards
294// Targets finds nothing and never connects.
295func TestCloseReapsLateTransport(t *testing.T) {
296	release := make(chan struct{})
297	closed := make(chan struct{})
298	tr := newMocktransport(t)
299	tr.EXPECT().Close().RunAndReturn(func() error { close(closed); return nil }).Once()
300	c := blockedConnector(t, release, tr, nil, 1)
301	s := &Source{dial: c, wait: testWait}
302	_, _, err := s.Targets(context.Background(), oneWindow, false)
303	require.NoError(t, err)
304	require.NoError(t, s.Close())
305	close(release)
306	select {
307	case <-closed:
308	case <-time.After(5 * time.Second):
309		t.Fatal("late transport not closed")
310	}
311	targets, covered, err := s.Targets(context.Background(), oneWindow, false)
312	require.NoError(t, err)
313	require.Empty(t, targets)
314	require.Empty(t, covered)
315	require.NoError(t, s.Close()) // idempotent
316}
317
318func TestWalkWideTreeStaysWithinBudget(t *testing.T) {
319	tr := newMocktransport(t)
320	children := make([]ref, 4096)
321	for i := range children {
322		children[i] = ref{":1.5", fmt.Sprintf("/node%d", i)}
323	}
324	visited := 0
325	tr.EXPECT().Do(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
326		func(_ time.Time, _ time.Duration, calls []call) []reply {
327			visited += len(calls) / 3
328			require.LessOrEqual(t, visited, maxWalkNodes)
329			rep := make([]reply, len(calls))
330			for i, c := range calls {
331				switch c.op {
332				case opChildren:
333					rep[i].refs = children
334				case opRole:
335					rep[i].u = roleLink
336				case opState:
337					rep[i].state = stShowing | 1<<stateVisible
338				}
339			}
340			return rep
341		}).Twice()
342	s := &Source{tr: tr}
343	// A full second level replies with another 4096 children each. There is
344	// no remaining budget, so none of those millions of refs may be copied.
345	var out []ref
346	var before, after runtime.MemStats
347	runtime.ReadMemStats(&before)
348	out = s.walk(func(calls []call) []reply { return tr.Do(time.Time{}, time.Second, calls) }, ref{":1.5", "/frame"}, false)
349	runtime.ReadMemStats(&after)
350	require.Len(t, out, maxWalkNodes)
351	require.Equal(t, maxWalkNodes, visited)
352	// Normal replies and call batches take only a few MB; the old append-
353	// then-truncate path copied hundreds of MB for this tree.
354	require.Less(t, after.TotalAlloc-before.TotalAlloc, uint64(32<<20))
355}

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