main 5d6fa26

commands_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"reflect"
6	"strings"
7	"sync"
8	"testing"
9	"time"
10
11	"github.com/bnema/neferwl/internal/adapters/logging"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/zerowrap"
14)
15
16// Adjacent motions for one window fold into the last queued command, summing
17// the relative deltas; nothing is merged across another command, and the
18// order of arrival is kept.
19func TestCommandRingCollapsesMotionInOrder(t *testing.T) {
20	s, _, _, _, _, _ := presentServer(t)
21	locked := ports.SecurityState{Generation: 2, Protected: true}
22	unlocked := ports.SecurityState{Generation: 3}
23	var got []ports.ClientCommand // display goroutine only
24	r := newCommandRing(s.display, func(q queuedCommand) { got = append(got, q.command()) })
25	release := make(chan struct{})
26	stalled := make(chan struct{})
27	s.display.Post(func() { close(stalled); <-release })
28	<-stalled
29	for _, c := range []ports.ClientCommand{
30		ports.PointerMotionTo{ID: 1, X: 1, DX: 0.25, UnaccelDX: 0.5},
31		ports.PointerMotionTo{ID: 1, X: 2, DX: 0.25, UnaccelDX: 0.5},
32		ports.PointerMotionTo{ID: 1, X: 3, DX: 0.25, DY: -0.5, UnaccelDX: 0.5, UnaccelDY: -1},
33		ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true},
34		ports.PointerMotionTo{ID: 1, X: 4},
35		ports.PointerFocus{ID: 2},
36		ports.PointerMotionTo{ID: 2, X: 5},
37		ports.PointerMotionTo{ID: 2, X: 6},
38		ports.PointerMotionTo{ID: 1, X: 7}, // other window: not adjacent-same
39		// Enveloped, as core sends them: same state merges, a different
40		// state, a bare motion, or another window does not.
41		ports.SecurityCommand{State: locked, Command: ports.PointerMotionTo{ID: 1, X: 8, DX: 1, UnaccelDY: 2}},
42		ports.SecurityCommand{State: locked, Command: ports.PointerMotionTo{ID: 1, X: 9, DX: 2, UnaccelDY: 3}},
43		ports.SecurityCommand{State: locked, Command: ports.PointerMotionTo{ID: 1, X: 10, DY: 4}},
44		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 1, X: 11, DX: 1}},
45		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 1, X: 12, DX: 1}},
46		ports.PointerMotionTo{ID: 1, X: 13},
47		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 1, X: 14}},
48		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 2, X: 15}},
49		ports.SecurityCommand{State: unlocked, Command: ports.PointerButtonTo{ID: 2}},
50	} {
51		if !r.Push(queue(c)) {
52			t.Fatal("push refused")
53		}
54	}
55	close(release)
56	want := []ports.ClientCommand{
57		// Slow motions add up: none of the sub-pixel deltas is lost.
58		ports.PointerMotionTo{ID: 1, X: 3, DX: 0.75, DY: -0.5, UnaccelDX: 1.5, UnaccelDY: -1},
59		ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true},
60		ports.PointerMotionTo{ID: 1, X: 4},
61		ports.PointerFocus{ID: 2},
62		ports.PointerMotionTo{ID: 2, X: 6},
63		ports.PointerMotionTo{ID: 1, X: 7},
64		ports.SecurityCommand{State: locked, Command: ports.PointerMotionTo{ID: 1, X: 10, DX: 3, DY: 4, UnaccelDY: 5}},
65		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 1, X: 12, DX: 2}},
66		ports.PointerMotionTo{ID: 1, X: 13},
67		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 1, X: 14}},
68		ports.SecurityCommand{State: unlocked, Command: ports.PointerMotionTo{ID: 2, X: 15}},
69		ports.SecurityCommand{State: unlocked, Command: ports.PointerButtonTo{ID: 2}},
70	}
71	var final []ports.ClientCommand
72	s.display.Do(func() { final = got }) // runs after the drain posted before it
73	if !reflect.DeepEqual(final, want) {
74		t.Fatalf("applied %#v, want %#v", final, want)
75	}
76}
77
78// A merge needs no room: motion keeps folding into the last queued command
79// while the ring is full of other commands, and the push does not block.
80func TestCommandRingMergesWhenFull(t *testing.T) {
81	s, _, _, _, _, _ := presentServer(t)
82	var got []ports.ClientCommand
83	r := newCommandRing(s.display, func(q queuedCommand) { got = append(got, q.command()) })
84	release := releaseOnce(t)
85	stalled := make(chan struct{})
86	s.display.Post(func() { close(stalled); <-release.ch })
87	<-stalled
88	for i := range commandRingSize - 1 {
89		r.Push(queue(ports.PointerFocus{ID: ports.WindowID(i)}))
90	}
91	done := make(chan struct{})
92	go func() {
93		defer close(done)
94		for range 1000 { // fills the ring with one motion, then folds the rest
95			r.Push(queue(ports.PointerMotionTo{ID: 1, DX: 1}))
96		}
97	}()
98	select {
99	case <-done:
100	case <-time.After(2 * time.Second):
101		t.Fatal("motion push blocked on a full ring")
102	}
103	release.do()
104	var last ports.ClientCommand
105	s.display.Do(func() { last = got[len(got)-1] })
106	if m, ok := last.(ports.PointerMotionTo); !ok || m.DX != 1000 {
107		t.Fatalf("last applied %#v, want one motion with DX 1000", last)
108	}
109}
110
111// A busy display goroutine must not stall the forwarder: it keeps draining
112// the Commands channel into the ring up to the ring capacity, and only then
113// does it wait (and core with it, through the full channel).
114func TestForwarderDrainsWhileDisplayBusy(t *testing.T) {
115	cmds := make(chan ports.ClientCommand, 32)
116	s, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, Channels{Commands: cmds}, logging.For(context.Background(), "wayland"))
117	if err != nil {
118		t.Fatal(err)
119	}
120	ctx, cancel := context.WithCancel(context.Background())
121	done := make(chan error, 1)
122	go func() { done <- s.Run(ctx) }()
123	defer func() { cancel(); <-done }()
124
125	// Deferred after the shutdown above, so it runs first: a failing
126	// assertion never leaves the display stalled while Run is joined.
127	release := releaseOnce(t)
128	defer release.do()
129	stalled := make(chan struct{})
130	s.display.Post(func() { close(stalled); <-release.ch })
131	<-stalled
132
133	// Not mergeable: every command takes a slot.
134	const total = commandRingSize + 100
135	sent := make(chan struct{})
136	go func() {
137		defer close(sent)
138		for i := range total {
139			select {
140			case cmds <- ports.PointerFocus{ID: ports.WindowID(i + 1)}:
141			case <-ctx.Done():
142				return
143			}
144		}
145	}()
146	queued := func() int {
147		s.commands.mu.Lock()
148		defer s.commands.mu.Unlock()
149		return len(s.commands.ring)
150	}
151	// Ring full, one command in the blocked forwarder, a full channel.
152	deadline := time.Now().Add(5 * time.Second)
153	for queued() != commandRingSize || len(cmds) != cap(cmds) {
154		if time.Now().After(deadline) {
155			t.Fatalf("ring holds %d (want %d), channel %d (want %d)", queued(), commandRingSize, len(cmds), cap(cmds))
156		}
157		time.Sleep(time.Millisecond)
158	}
159	select {
160	case <-sent:
161		t.Fatal("sender finished although the ring is full")
162	default:
163	}
164	release.do()
165	select {
166	case <-sent:
167	case <-time.After(5 * time.Second):
168		t.Fatal("forwarder did not resume after the display was released")
169	}
170}
171
172// Each command used to cost one display.Do round trip (up to one 5 ms
173// event-loop dispatch), capping pointer motion at ~150/s. A 1 kHz mouse
174// must be absorbed without core blocking.
175func TestCommandThroughput(t *testing.T) {
176	cmds := make(chan ports.ClientCommand, 32)
177	s, err := New(Options{RuntimeDir: t.TempDir(), Outputs: testOutputs}, Channels{Commands: cmds}, logging.For(context.Background(), "wayland"))
178	if err != nil {
179		t.Fatal(err)
180	}
181	ctx, cancel := context.WithCancel(context.Background())
182	done := make(chan error, 1)
183	go func() { done <- s.Run(ctx) }()
184	defer func() { cancel(); <-done }()
185	start := time.Now()
186	for i := range 2000 {
187		cmds <- ports.PointerMotionTo{ID: 1, X: float64(i), Y: 1}
188	}
189	if d := time.Since(start); d > time.Second {
190		t.Fatalf("2000 motion commands took %v (%.0f/s); want well above 1000/s", d, 2000/d.Seconds())
191	}
192}
193
194// release unblocks a stalled display goroutine once, from the test or its
195// cleanup, so a failed assertion never hangs the server shutdown.
196type release struct {
197	ch   chan struct{}
198	once sync.Once
199}
200
201func (r *release) do() { r.once.Do(func() { close(r.ch) }) }
202
203// releaseOnce returns a release that the test cleanup also fires, before
204// the cleanups registered earlier (the server's).
205func releaseOnce(t *testing.T) *release {
206	r := &release{ch: make(chan struct{})}
207	t.Cleanup(r.do)
208	return r
209}
210
211// A merged motion keeps the push time of the first motion folded into it, so
212// the reported latency is the longest wait any of its motions had.
213func TestMergeMotionKeepsOldestPushTime(t *testing.T) {
214	t0 := time.Unix(100, 0)
215	first := queue(ports.PointerMotionTo{ID: 1, DX: 1})
216	first.at = t0
217	second := queue(ports.PointerMotionTo{ID: 1, DX: 2})
218	second.at = t0.Add(5 * time.Millisecond)
219	if !mergeMotion(&first, second) {
220		t.Fatal("adjacent motions did not merge")
221	}
222	if !first.at.Equal(t0) {
223		t.Fatalf("merged at = %v, want the oldest %v", first.at, t0)
224	}
225}
226
227// commandStats keeps the largest push-to-apply wait and the applied count,
228// and take resets both.
229func TestCommandStatsObserveAndTake(t *testing.T) {
230	var c commandStats
231	t0 := time.Unix(100, 0)
232	c.observe(t0, t0.Add(3*time.Millisecond))
233	c.observe(t0, t0.Add(9*time.Millisecond))
234	c.observe(t0, t0.Add(time.Millisecond))
235	if n, d := c.take(); n != 3 || d != 9*time.Millisecond {
236		t.Fatalf("take = %d, %v; want 3, 9ms", n, d)
237	}
238	if n, d := c.take(); n != 0 || d != 0 {
239		t.Fatalf("second take = %d, %v; want zeros", n, d)
240	}
241}
242
243// The ring counts the largest queued length and the folded pushes, and
244// takeStats returns then resets them.
245func TestCommandRingStats(t *testing.T) {
246	s, _, _, _, _, _ := presentServer(t)
247	r := newCommandRing(s.display, func(queuedCommand) {})
248	release := releaseOnce(t)
249	stalled := make(chan struct{})
250	s.display.Post(func() { close(stalled); <-release.ch })
251	<-stalled
252	r.Push(queue(ports.PointerFocus{ID: 1}))
253	r.Push(queue(ports.PointerMotionTo{ID: 1, DX: 1}))
254	r.Push(queue(ports.PointerMotionTo{ID: 1, DX: 1})) // folded
255	r.Push(queue(ports.PointerMotionTo{ID: 1, DX: 1})) // folded
256	r.Push(queue(ports.PointerFocus{ID: 2}))
257	st := r.takeStats()
258	if st.maxLen != 3 || st.merged != 2 || st.maxPushBlock != 0 {
259		t.Fatalf("stats = %+v, want maxLen 3, merged 2, no block", st)
260	}
261	if st := r.takeStats(); st != (ringStats{maxLen: 3}) {
262		t.Fatalf("stats after take = %+v, want current occupancy only", st)
263	}
264	release.do()
265}
266
267// The report logs the period's figures and resets them; a period without
268// applied commands logs nothing.
269func TestReportCommandStats(t *testing.T) {
270	var out lockedBuffer
271	log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &out}).WithField("component", "wayland")
272	h := newAdmissionHarnessWith(t, 1, Options{}, log)
273	s := h.s
274	s.reportCommandStats()
275	if strings.Contains(out.String(), "command stats") {
276		t.Fatalf("idle period logged: %s", out.String())
277	}
278	t0 := time.Unix(100, 0)
279	s.commandStats.observe(t0, t0.Add(1500*time.Microsecond))
280	s.reportCommandStats()
281	got := out.String()
282	for _, want := range []string{`"message":"command stats"`, `"commands":1`, `"merged":0`, `"max_queued":0`, `"max_push_block_ms":0`, `"max_latency_ms":1.5`} {
283		if !strings.Contains(got, want) {
284			t.Fatalf("log lacks %s: %s", want, got)
285		}
286	}
287	n := strings.Count(got, "command stats")
288	s.reportCommandStats()
289	if strings.Count(out.String(), "command stats") != n {
290		t.Fatalf("stats not reset: %s", out.String())
291	}
292}
293
294// A stalled display applies nothing but fills its ring: the period still
295// logs, so a long stall is not hidden.
296func TestReportCommandStatsDuringStall(t *testing.T) {
297	var out lockedBuffer
298	log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &out}).WithField("component", "wayland")
299	h := newAdmissionHarnessWith(t, 1, Options{}, log)
300	s := h.s
301	s.commands.mu.Lock()
302	s.commands.stats.maxLen = 5
303	s.commands.mu.Unlock()
304	s.reportCommandStats()
305	got := out.String()
306	for _, want := range []string{`"message":"command stats"`, `"commands":0`, `"max_queued":5`} {
307		if !strings.Contains(got, want) {
308			t.Fatalf("log lacks %s: %s", want, got)
309		}
310	}
311}

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