forward_test.go RawHistoryFolder

1package libinput
2
3import (
4	"bytes"
5	"context"
6	"strings"
7	"sync"
8	"testing"
9	"time"
10
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/bnema/zerowrap"
13	"github.com/rs/zerolog"
14)
15
16// A slow consumer must not block push, and stale motion must collapse into
17// the latest position while keys and buttons keep their order.
18func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
19	f := newForwarder(zerowrap.Default())
20	input := make(chan ports.InputEvent) // unbuffered: core is busy
21	start := time.Now()
22	for i := range 1000 {
23		f.push(ports.PointerMotion{X: float64(i)})
24	}
25	f.push(ports.PointerButton{Button: 272, Pressed: true})
26	f.push(ports.PointerMotion{X: 2000})
27	f.push(ports.PointerMotion{X: 2001})
28	if d := time.Since(start); d > 100*time.Millisecond {
29		t.Fatalf("push blocked for %v", d)
30	}
31	ctx, cancel := context.WithCancel(context.Background())
32	defer cancel()
33	go f.run(ctx, input)
34	want := []ports.InputEvent{
35		ports.PointerMotion{X: 999},
36		ports.PointerButton{Button: 272, Pressed: true},
37		ports.PointerMotion{X: 2001},
38	}
39	for i, w := range want {
40		select {
41		case got := <-input:
42			if got != w {
43				t.Fatalf("event %d: got %#v, want %#v", i, got, w)
44			}
45		case <-time.After(2 * time.Second):
46			t.Fatalf("event %d not delivered", i)
47		}
48	}
49	select {
50	case got := <-input:
51		t.Fatalf("unexpected extra event %#v", got)
52	case <-time.After(20 * time.Millisecond):
53	}
54	s := f.take()
55	if s.Motions != 1002 || s.Coalesced != 1000 || s.Sent != 3 {
56		t.Fatalf("stats %+v", s)
57	}
58	if s := f.take(); s != (forwardStats{}) {
59		t.Fatalf("take did not reset: %+v", s)
60	}
61}
62
63func TestForwarderStopsOnCancel(t *testing.T) {
64	f := newForwarder(zerowrap.Default())
65	f.push(ports.PointerMotion{X: 1})
66	ctx, cancel := context.WithCancel(context.Background())
67	done := make(chan struct{})
68	go func() { f.run(ctx, make(chan ports.InputEvent)); close(done) }()
69	cancel()
70	select {
71	case <-done:
72	case <-time.After(2 * time.Second):
73		t.Fatal("run did not stop while blocked on send")
74	}
75}
76
77// Coalesced motions keep the sum of their relative deltas.
78func TestForwarderSumsDeltas(t *testing.T) {
79	f := newForwarder(zerowrap.Default())
80	f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4})
81	f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40})
82	ev, _ := f.pop()
83	if want := (ports.PointerMotion{X: 2, DX: 11, DY: 22, UnaccelDX: 33, UnaccelDY: 44}); ev != want {
84		t.Fatalf("got %+v, want %+v", ev, want)
85	}
86}
87
88// 10 000 motions with core stalled are one event with exact delta sums.
89func TestForwarderManyMotionsOneEvent(t *testing.T) {
90	f := newForwarder(zerowrap.Default())
91	for i := range 10000 {
92		f.push(ports.PointerMotion{X: float64(i), DX: 0.5, UnaccelDY: 1})
93	}
94	if len(f.queue) != 1 {
95		t.Fatalf("%d queued", len(f.queue))
96	}
97	ev, _ := f.pop()
98	if m := ev.(ports.PointerMotion); m.X != 9999 || m.DX != 5000 || m.UnaccelDY != 10000 {
99		t.Fatalf("%+v", m)
100	}
101}
102
103// Past forwardWarn queued events the forwarder warns once, and once more
104// when the queue drains below it; nothing is dropped.
105func TestForwarderWarnsOnceEachWay(t *testing.T) {
106	var buf lockedBuffer
107	f := newForwarder(zerowrap.Logger{Logger: zerolog.New(&buf)})
108	for range forwardWarn + 100 {
109		f.push(ports.PointerButton{Button: 272})
110	}
111	if n := strings.Count(buf.String(), "events queue up"); n != 1 {
112		t.Fatalf("%d warnings", n)
113	}
114	for range 200 {
115		f.pop()
116	}
117	if n := strings.Count(buf.String(), "input queue drained"); n != 1 {
118		t.Fatalf("%d drained logs", n)
119	}
120	if len(f.queue) != forwardWarn-100 {
121		t.Fatalf("dropped: %d left", len(f.queue))
122	}
123}
124
125// lockedBuffer is an io.Writer for a logger.
126type lockedBuffer struct {
127	mu sync.Mutex
128	b  bytes.Buffer
129}
130
131func (l *lockedBuffer) Write(p []byte) (int, error) {
132	l.mu.Lock()
133	defer l.mu.Unlock()
134	return l.b.Write(p)
135}
136
137func (l *lockedBuffer) String() string {
138	l.mu.Lock()
139	defer l.mu.Unlock()
140	return l.b.String()
141}
142
143func TestForwarderSecurityMotionEpochs(t *testing.T) {
144	states := []ports.SecurityState{
145		{Generation: 1}, {Generation: 2, Protected: true}, {Generation: 3},
146		{Generation: 3, Protected: true}, // exact state, not just generation
147	}
148	f := newForwarder(zerowrap.Default())
149	for _, state := range states {
150		for i := range 1000 {
151			f.push(ports.SecurityInput{State: state, Event: ports.PointerMotion{X: float64(i), DX: 0.5, DY: 2, UnaccelDX: 3, UnaccelDY: 4}})
152		}
153	}
154	// An unwrapped motion must not absorb or be absorbed by wrapped motion.
155	f.push(ports.PointerMotion{X: 5})
156	f.push(ports.SecurityInput{State: states[len(states)-1], Event: ports.PointerMotion{X: 6}})
157	if len(f.queue) != len(states)+2 {
158		t.Fatalf("wrong epoch coalescing: %d entries", len(f.queue))
159	}
160	for _, state := range states {
161		ev, ok := f.pop()
162		want := ports.SecurityInput{State: state, Event: ports.PointerMotion{X: 999, DX: 500, DY: 2000, UnaccelDX: 3000, UnaccelDY: 4000}}
163		if !ok || ev != want {
164			t.Fatalf("lost epoch/deltas: got %#v, want %#v", ev, want)
165		}
166	}
167	if ev, _ := f.pop(); ev != (ports.PointerMotion{X: 5}) {
168		t.Fatalf("raw motion changed: %#v", ev)
169	}
170	if ev, _ := f.pop(); ev != (ports.SecurityInput{State: states[len(states)-1], Event: ports.PointerMotion{X: 6}}) {
171		t.Fatalf("wrapped tail changed: %#v", ev)
172	}
173	s := f.take()
174	if s.Motions != 4002 || s.Coalesced != 3996 {
175		t.Fatalf("stats %+v", s)
176	}
177}

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