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}