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}