feat/data-dnd e05cb15

forward_test.go RawHistoryFolder

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

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