1package libinput
2
3import (
4 "bytes"
5 "errors"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/bnema/neferwl/internal/adapters/xkb"
11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/zerowrap"
14)
15
16func TestHotkey(t *testing.T) {
17 ca := ports.ModCtrl | ports.ModAlt
18 for _, tc := range []struct {
19 name string
20 ev ports.KeyEvent
21 action hotkeyAction
22 vt int
23 }{
24 {"ctrl alt backspace", ports.KeyEvent{Keysym: "BackSpace", Keycode: 14, Mods: ca, Pressed: true}, hotkeyQuit, 0},
25 {"release ignored", ports.KeyEvent{Keysym: "BackSpace", Keycode: 14, Mods: ca}, hotkeyNone, 0},
26 {"ctrl only", ports.KeyEvent{Keysym: "BackSpace", Keycode: 14, Mods: ports.ModCtrl, Pressed: true}, hotkeyNone, 0},
27 {"xkb vt sym", ports.KeyEvent{Keysym: "XF86Switch_VT_3", Keycode: 61, Mods: ca, Pressed: true}, hotkeyVT, 3},
28 {"ctrl alt f2 raw", ports.KeyEvent{Keysym: "F2", Keycode: 60, Mods: ca, Pressed: true}, hotkeyVT, 2},
29 {"ctrl alt f12 raw", ports.KeyEvent{Keysym: "F12", Keycode: 88, Mods: ca, Pressed: true}, hotkeyVT, 12},
30 {"plain f2", ports.KeyEvent{Keysym: "F2", Keycode: 60, Pressed: true}, hotkeyNone, 0},
31 } {
32 for _, protected := range []bool{false, true} {
33 want := tc.action
34 if protected && want == hotkeyQuit {
35 want = hotkeyNone
36 }
37 a, vt := hotkey(tc.ev, protected)
38 if a != want || vt != tc.vt {
39 t.Errorf("%s protected=%v: got %v %d", tc.name, protected, a, vt)
40 }
41 }
42 }
43}
44
45func TestGesturesIgnoreSecondTouchpad(t *testing.T) {
46 var g gestures
47 const pad, other = 1, 2
48 g.begin(pad, ports.GestureSwipe, 3, time.Second)
49 if ev := g.begin(other, ports.GestureSwipe, 3, time.Second); ev != nil {
50 t.Fatalf("second touchpad began: %v", ev)
51 }
52 if ev := g.swipeUpdate(other, 1, 1, 1, 1, time.Second); ev != nil {
53 t.Fatalf("second touchpad moved: %v", ev)
54 }
55 if ev := g.end(other, ports.GestureSwipe, false, time.Second); ev != nil {
56 t.Fatalf("second touchpad ended the swipe: %v", ev)
57 }
58 if ev := g.swipeUpdate(pad, 1, 1, 1, 1, 2*time.Second); ev == nil {
59 t.Fatal("first touchpad lost its swipe")
60 }
61 g.end(pad, ports.GestureSwipe, false, 3*time.Second)
62 if ev := g.begin(other, ports.GestureSwipe, 3, 4*time.Second); ev == nil {
63 t.Fatal("second touchpad cannot swipe after the first ended")
64 }
65}
66
67func TestGesturesStreamSwipe(t *testing.T) {
68 var g gestures
69 const pad, other = 1, 2
70 if ev := g.begin(pad, ports.GestureSwipe, 0, time.Second); ev != nil {
71 t.Fatalf("no fingers: %v", ev)
72 }
73 if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
74 t.Fatalf("update without a swipe: %v", ev)
75 }
76 if ev := g.begin(pad, ports.GestureSwipe, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
77 t.Fatalf("begin: %v", ev)
78 }
79 if ev := g.swipeUpdate(pad, -3, 2, -6, 4, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, AccelDX: -6, AccelDY: 4, Time: 2 * time.Second}) {
80 t.Fatalf("update: %v", ev)
81 }
82 if ev := g.end(other, ports.GestureSwipe, false, 0); ev != nil {
83 t.Fatalf("end on another device: %v", ev)
84 }
85 if ev := g.end(pad, ports.GestureSwipe, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
86 t.Fatalf("end: %v", ev)
87 }
88 if ev := g.end(pad, ports.GestureSwipe, false, 0); ev != nil {
89 t.Fatalf("second end: %v", ev)
90 }
91}
92
93// Swipes of every finger count stream; core picks which ones it keeps.
94func TestGesturesStreamAnyFingerCount(t *testing.T) {
95 for _, fingers := range []int{2, 3, 4, 5} {
96 var g gestures
97 if ev := g.begin(1, ports.GestureSwipe, fingers, time.Second); ev != (ports.SwipeBegin{Fingers: fingers, Time: time.Second}) {
98 t.Fatalf("%d fingers: %v", fingers, ev)
99 }
100 if ev := g.begin(1, ports.GestureSwipe, 5, time.Second); ev != nil {
101 t.Fatalf("a second swipe began during a swipe: %v", ev)
102 }
103 }
104}
105
106func TestGesturesStreamPinch(t *testing.T) {
107 var g gestures
108 const pad = 1
109 if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != (ports.PinchBegin{Fingers: 2, Time: time.Second}) {
110 t.Fatalf("begin: %v", ev)
111 }
112 if ev := g.swipeUpdate(pad, 1, 1, 1, 1, time.Second); ev != nil {
113 t.Fatalf("swipe update during a pinch: %v", ev)
114 }
115 if ev := g.end(pad, ports.GestureHold, false, time.Second); ev != nil {
116 t.Fatalf("hold end during a pinch: %v", ev)
117 }
118 want := ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.5, Rotation: -10, Time: 2 * time.Second}
119 if ev := g.pinchUpdate(pad, 1, -2, 1.5, -10, 2*time.Second); ev != want {
120 t.Fatalf("update: %v", ev)
121 }
122 if ev := g.end(pad, ports.GesturePinch, false, 3*time.Second); ev != (ports.PinchEnd{Time: 3 * time.Second}) {
123 t.Fatalf("end: %v", ev)
124 }
125 if ev := g.pinchUpdate(pad, 1, 1, 1, 0, 4*time.Second); ev != nil {
126 t.Fatalf("update after the end: %v", ev)
127 }
128}
129
130func TestGesturesStreamHold(t *testing.T) {
131 var g gestures
132 const pad = 1
133 if ev := g.begin(pad, ports.GestureHold, 3, time.Second); ev != (ports.HoldBegin{Fingers: 3, Time: time.Second}) {
134 t.Fatalf("begin: %v", ev)
135 }
136 if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != nil {
137 t.Fatalf("pinch began during a hold: %v", ev)
138 }
139 if ev := g.end(pad, ports.GestureHold, true, 2*time.Second); ev != (ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}) {
140 t.Fatalf("end: %v", ev)
141 }
142}
143
144// A device that goes during a gesture ends it cancelled, whatever its kind.
145func TestGesturesAbortOnRemoval(t *testing.T) {
146 for kind, want := range map[ports.GestureKind]ports.InputEvent{
147 ports.GestureSwipe: ports.SwipeEnd{Cancelled: true},
148 ports.GesturePinch: ports.PinchEnd{Cancelled: true},
149 ports.GestureHold: ports.HoldEnd{Cancelled: true},
150 } {
151 var g gestures
152 g.begin(1, kind, 3, time.Second)
153 if ev := g.abort(2); ev != nil {
154 t.Fatalf("kind %d: another device aborted: %v", kind, ev)
155 }
156 if ev := g.abort(1); ev != want {
157 t.Fatalf("kind %d: abort %v, want %v", kind, ev, want)
158 }
159 if ev := g.abort(1); ev != nil {
160 t.Fatalf("kind %d: second abort %v", kind, ev)
161 }
162 if ev := g.begin(1, kind, 3, time.Second); ev == nil {
163 t.Fatalf("kind %d: no gesture after the abort", kind)
164 }
165 }
166}
167
168func TestAccelProfile(t *testing.T) {
169 if accelProfile(ports.AccelFlat) != 1 || accelProfile(ports.AccelAdaptive) != 2 {
170 t.Fatal(accelProfile(ports.AccelFlat), accelProfile(ports.AccelAdaptive))
171 }
172}
173
174func TestTimestamps(t *testing.T) {
175 if usec(1500) != 1500*time.Microsecond {
176 t.Fatal(usec(1500))
177 }
178}
179
180func TestPointerAcrossOutputs(t *testing.T) {
181 layout := ports.Layout{
182 {Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
183 {Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 2},
184 }
185 p := newPointer(layout)
186 if p.x != 50 || p.y != 25 {
187 t.Fatal(p.x, p.y)
188 }
189 // Off the bottom of A: clamped to A.
190 if m := p.move(-80, 100); m.X != 0 || m.Y != 49 {
191 t.Fatalf("got %v %v", m.X, m.Y)
192 }
193 // Into B, where deltas count in physical pixels (scale 2).
194 p.set(150, 20)
195 if m := p.move(20, 10); m.X != 160 || m.Y != 25 || m.DX != 10 || m.DY != 5 {
196 t.Fatalf("got %+v", m)
197 }
198 var out string
199 var px, py float64
200 p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true)
201 if out != "B" || px != 120 || py != 50 {
202 t.Fatal(out, px, py)
203 }
204 // B unplugged: the pointer lands on A.
205 p.setLayout(layout[:1])
206 if p.x != 99 || p.y != 25 {
207 t.Fatal(p.x, p.y)
208 }
209}
210
211func TestPointerStartsOnPrimary(t *testing.T) {
212 p := newPointer(ports.Layout{
213 {Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
214 {Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 1, Primary: true},
215 })
216 if p.x != 200 || p.y != 50 {
217 t.Fatal(p.x, p.y)
218 }
219}
220
221func TestPointerConstraints(t *testing.T) {
222 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})
223 p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 50, Y: 50})
224 if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {
225 t.Fatalf("locked: %+v", m)
226 }
227 // Confining moves the pointer inside, then keeps it there.
228 p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}, X: 50, Y: 50})
229 if p.x != 60 || p.y != 29 {
230 t.Fatal(p.x, p.y)
231 }
232 if m := p.move(100, 0); m.X != 79 || m.DX != 100 {
233 t.Fatalf("confined: %+v", m)
234 }
235 p.constrain(ports.PointerConstraint{})
236 if m := p.move(10, 0); m.X != 89 {
237 t.Fatalf("free: %+v", m)
238 }
239 // A lock takes core's cursor position, and so does its release.
240 p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 20, Y: 30})
241 if p.x != 20 || p.y != 30 {
242 t.Fatal(p.x, p.y)
243 }
244 p.move(40, 40)
245 p.constrain(ports.PointerConstraint{X: 21, Y: 31})
246 if p.x != 21 || p.y != 31 {
247 t.Fatal(p.x, p.y)
248 }
249 // A warp moves a free pointer to core's cursor.
250 p.constrain(ports.PointerConstraint{X: 70, Y: 40, Warp: true})
251 if p.x != 70 || p.y != 40 {
252 t.Fatal(p.x, p.y)
253 }
254}
255
256func TestProtectedKeyboardPolicy(t *testing.T) {
257 ca := ports.ModCtrl | ports.ModAlt
258 for _, key := range []ports.KeyEvent{
259 {Keysym: "credential-secret", Base: "raw-secret", Keycode: 424242, Pressed: true},
260 {Keysym: "credential-secret", Keycode: 424242},
261 {Keysym: "BackSpace", Mods: ca, Pressed: true},
262 {Keysym: "raw-secret", Keycode: 14, Mods: ca, Pressed: true},
263 } {
264 t.Run(key.Keysym, func(t *testing.T) {
265 var buf bytes.Buffer
266 security := portsmocks.NewMockSessionSecurity(t)
267 state := ports.SecurityState{Generation: 7, Protected: true}
268 security.EXPECT().Snapshot().Return(state).Once()
269 opts := Options{Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
270 produced := securitySnapshot(opts.Security)
271 ev, err := translateKey(key, opts, produced)
272 if err != nil || errors.Is(err, ErrEmergencyQuit) || ev != key {
273 t.Fatalf("protected key lost/quit: event=%#v err=%v", ev, err)
274 }
275 if got := secureInput(ev, opts.Security, produced); got != (ports.SecurityInput{State: state, Event: key}) {
276 t.Fatalf("wrong production epoch: %#v", got)
277 }
278 if buf.Len() != 0 {
279 t.Fatalf("protected key logged identifiers: %s", &buf)
280 }
281 })
282 }
283}
284
285func TestKeyboardPolicyUnlockedAndVT(t *testing.T) {
286 ca := ports.ModCtrl | ports.ModAlt
287 for _, protected := range []bool{false, true} {
288 t.Run(map[bool]string{false: "unlocked", true: "protected"}[protected], func(t *testing.T) {
289 var buf bytes.Buffer
290 security := portsmocks.NewMockSessionSecurity(t)
291 state := ports.SecurityState{Generation: 9, Protected: protected}
292 security.EXPECT().Snapshot().Return(state).Once()
293 seat := portsmocks.NewMockSeat(t)
294 seat.EXPECT().SwitchVT(2).Once()
295 opts := Options{Security: security, Seat: seat, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
296 ev, err := translateKey(ports.KeyEvent{Keysym: "XF86Switch_VT_2", Keycode: 60, Mods: ca, Pressed: true}, opts, securitySnapshot(opts.Security))
297 if ev != nil || err != nil {
298 t.Fatalf("VT not consumed: %#v %v", ev, err)
299 }
300 if protected && buf.Len() != 0 {
301 t.Fatalf("protected VT key logged: %s", &buf)
302 }
303 if !protected && !strings.Contains(buf.String(), "XF86Switch_VT_2") {
304 t.Fatalf("unlocked debug logging changed: %s", &buf)
305 }
306 })
307 }
308 var buf bytes.Buffer
309 opts := Options{LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
310 key := ports.KeyEvent{Keysym: "q", Keycode: 16, Pressed: true}
311 state := securitySnapshot(nil)
312 ev, err := translateKey(key, opts, state)
313 if err != nil || secureInput(ev, nil, state) != key || !strings.Contains(buf.String(), `"keysym":"q"`) {
314 t.Fatalf("nil gate changed unlocked behavior: %#v %v %s", ev, err, &buf)
315 }
316 ev, err = translateKey(ports.KeyEvent{Keysym: "BackSpace", Mods: ca, Pressed: true}, opts, state)
317 if ev != nil || !errors.Is(err, ErrEmergencyQuit) || !strings.Contains(buf.String(), "emergency-key") {
318 t.Fatalf("unlocked emergency quit changed: %#v %v %s", ev, err, &buf)
319 }
320}
321
322func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
323 security := portsmocks.NewMockSessionSecurity(t)
324 old := ports.SecurityState{Generation: 11, Protected: true}
325 newState := ports.SecurityState{Generation: 12}
326 events := []ports.InputEvent{
327 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
328 ports.PointerButton{Button: 272}, ports.PointerAxis{},
329 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
330 ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{},
331 }
332 security.EXPECT().Snapshot().Return(old).Times(len(events))
333 security.EXPECT().Snapshot().Return(newState).Once()
334 f := newForwarder(zerowrap.Default())
335 for _, ev := range events {
336 f.push(secureInput(ev, security, securitySnapshot(security)))
337 }
338 if securitySnapshot(security) != newState {
339 t.Fatal("did not transition")
340 }
341 for _, want := range events {
342 got, ok := f.pop()
343 if !ok || got != (ports.SecurityInput{State: old, Event: want}) {
344 t.Fatalf("queued event changed epoch: %#v", got)
345 }
346 }
347 if secureInput(nil, security, old) != nil {
348 t.Fatal("nil translated event was wrapped")
349 }
350}
351
352func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) {
353 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
354 if err != nil {
355 t.Fatal(err)
356 }
357 defer km.Close()
358 initial := ports.SecurityState{}
359 locked := ports.SecurityState{Generation: 1, Protected: true}
360 unlocked := ports.SecurityState{Generation: 2}
361 security := portsmocks.NewMockSessionSecurity(t)
362 security.EXPECT().Snapshot().Return(initial).Times(3)
363 security.EXPECT().Snapshot().Return(locked).Times(4)
364 security.EXPECT().Snapshot().Return(unlocked).Times(5)
365 var buf bytes.Buffer
366 opts := Options{Keymap: km, Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
367 produce := func(code uint32, down bool) ports.InputEvent {
368 t.Helper()
369 state := securitySnapshot(opts.Security)
370 ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state)
371 if err != nil {
372 t.Fatal(err)
373 }
374 return secureInput(ev, opts.Security, state)
375 }
376 for _, code := range []uint32{125, 29, 56} {
377 produce(code, true)
378 }
379 buf.Reset()
380 for _, code := range []uint32{125, 29, 56} {
381 if ev := produce(code, true); ev != nil {
382 t.Fatalf("inherited repeat delivered: %#v", ev)
383 }
384 }
385 // Ctrl+Alt held before lock must not activate emergency quit, even on a
386 // fresh Backspace. Super must not contaminate the credential either.
387 if ev := produce(14, true).(ports.SecurityInput).Event.(ports.KeyEvent); ev.Mods != 0 || ev.State != (ports.ModState{}) {
388 t.Fatalf("protected fresh key inherited native state: %+v", ev)
389 }
390 if buf.Len() != 0 {
391 t.Fatalf("protected keyboard logged: %s", &buf)
392 }
393 if ev := produce(16, true).(ports.SecurityInput); ev.State != unlocked || ev.Event.(ports.KeyEvent).Mods != 0 || ev.Event.(ports.KeyEvent).State != (ports.ModState{}) {
394 t.Fatalf("unlocked q inherited state: %#v", ev)
395 }
396 buf.Reset()
397 for _, code := range []uint32{14, 56, 29, 125} {
398 if ev := produce(code, false); ev != nil {
399 t.Fatalf("inherited release delivered: %#v", ev)
400 }
401 }
402 if buf.Len() != 0 {
403 t.Fatalf("quarantined release logged: %s", &buf)
404 }
405}
406
407// The cursor is placed in target pixels: after the output transform.
408func TestPointerMovedTransform(t *testing.T) {
409 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}})
410 p.set(10, 20)
411 var out string
412 var px, py float64
413 p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true)
414 if out != "A" || px != 20 || py != 90 {
415 t.Fatal(out, px, py)
416 }
417}