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 TestSwipesIgnoreSecondTouchpad(t *testing.T) {
46 var s swipes
47 const pad, other = 1, 2
48 s.begin(pad, 3, time.Second)
49 if ev := s.begin(other, 3, time.Second); ev != nil {
50 t.Fatalf("second touchpad began: %v", ev)
51 }
52 if ev := s.update(other, 1, 1, time.Second); ev != nil {
53 t.Fatalf("second touchpad moved: %v", ev)
54 }
55 if ev := s.end(other, false, time.Second); ev != nil {
56 t.Fatalf("second touchpad ended the swipe: %v", ev)
57 }
58 if ev := s.update(pad, 1, 1, 2*time.Second); ev == nil {
59 t.Fatal("first touchpad lost its swipe")
60 }
61 s.end(pad, false, 3*time.Second)
62 if ev := s.begin(other, 3, 4*time.Second); ev == nil {
63 t.Fatal("second touchpad cannot swipe after the first ended")
64 }
65}
66
67func TestSwipesStreamThreeFingers(t *testing.T) {
68 var s swipes
69 const pad, other = 1, 2
70 if ev := s.begin(pad, 2, time.Second); ev != nil {
71 t.Fatalf("two fingers: %v", ev)
72 }
73 if ev := s.update(pad, 1, 1, time.Second); ev != nil {
74 t.Fatalf("update without a swipe: %v", ev)
75 }
76 if ev := s.begin(pad, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
77 t.Fatalf("begin: %v", ev)
78 }
79 if ev := s.update(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {
80 t.Fatalf("update: %v", ev)
81 }
82 if ev := s.end(other, false, 0); ev != nil {
83 t.Fatalf("end on another device: %v", ev)
84 }
85 if ev := s.end(pad, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
86 t.Fatalf("end: %v", ev)
87 }
88 if ev := s.end(pad, false, 0); ev != nil {
89 t.Fatalf("second end: %v", ev)
90 }
91}
92
93func TestSwipesStreamFourFingers(t *testing.T) {
94 var s swipes
95 if ev := s.begin(1, 4, time.Second); ev != (ports.SwipeBegin{Fingers: 4, Time: time.Second}) {
96 t.Fatalf("four fingers: %v", ev)
97 }
98 if ev := s.begin(1, 5, time.Second); ev != nil {
99 t.Fatalf("five fingers during a swipe: %v", ev)
100 }
101}
102
103func TestAccelProfile(t *testing.T) {
104 if accelProfile(ports.AccelFlat) != 1 || accelProfile(ports.AccelAdaptive) != 2 {
105 t.Fatal(accelProfile(ports.AccelFlat), accelProfile(ports.AccelAdaptive))
106 }
107}
108
109func TestTimestamps(t *testing.T) {
110 if usec(1500) != 1500*time.Microsecond {
111 t.Fatal(usec(1500))
112 }
113}
114
115func TestPointerAcrossOutputs(t *testing.T) {
116 layout := ports.Layout{
117 {Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
118 {Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 2},
119 }
120 p := newPointer(layout)
121 if p.x != 50 || p.y != 25 {
122 t.Fatal(p.x, p.y)
123 }
124 // Off the bottom of A: clamped to A.
125 if m := p.move(-80, 100); m.X != 0 || m.Y != 49 {
126 t.Fatalf("got %v %v", m.X, m.Y)
127 }
128 // Into B, where deltas count in physical pixels (scale 2).
129 p.set(150, 20)
130 if m := p.move(20, 10); m.X != 160 || m.Y != 25 || m.DX != 10 || m.DY != 5 {
131 t.Fatalf("got %+v", m)
132 }
133 var out string
134 var px, py float64
135 p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true)
136 if out != "B" || px != 120 || py != 50 {
137 t.Fatal(out, px, py)
138 }
139 // B unplugged: the pointer lands on A.
140 p.setLayout(layout[:1])
141 if p.x != 99 || p.y != 25 {
142 t.Fatal(p.x, p.y)
143 }
144}
145
146func TestPointerStartsOnPrimary(t *testing.T) {
147 p := newPointer(ports.Layout{
148 {Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 50, Scale: 1},
149 {Info: ports.OutputInfo{Name: "B"}, X: 100, Width: 200, Height: 100, Scale: 1, Primary: true},
150 })
151 if p.x != 200 || p.y != 50 {
152 t.Fatal(p.x, p.y)
153 }
154}
155
156func TestPointerConstraints(t *testing.T) {
157 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})
158 p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 50, Y: 50})
159 if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {
160 t.Fatalf("locked: %+v", m)
161 }
162 // Confining moves the pointer inside, then keeps it there.
163 p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}, X: 50, Y: 50})
164 if p.x != 60 || p.y != 29 {
165 t.Fatal(p.x, p.y)
166 }
167 if m := p.move(100, 0); m.X != 79 || m.DX != 100 {
168 t.Fatalf("confined: %+v", m)
169 }
170 p.constrain(ports.PointerConstraint{})
171 if m := p.move(10, 0); m.X != 89 {
172 t.Fatalf("free: %+v", m)
173 }
174 // A lock takes core's cursor position, and so does its release.
175 p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 20, Y: 30})
176 if p.x != 20 || p.y != 30 {
177 t.Fatal(p.x, p.y)
178 }
179 p.move(40, 40)
180 p.constrain(ports.PointerConstraint{X: 21, Y: 31})
181 if p.x != 21 || p.y != 31 {
182 t.Fatal(p.x, p.y)
183 }
184 // A warp moves a free pointer to core's cursor.
185 p.constrain(ports.PointerConstraint{X: 70, Y: 40, Warp: true})
186 if p.x != 70 || p.y != 40 {
187 t.Fatal(p.x, p.y)
188 }
189}
190
191func TestProtectedKeyboardPolicy(t *testing.T) {
192 ca := ports.ModCtrl | ports.ModAlt
193 for _, key := range []ports.KeyEvent{
194 {Keysym: "credential-secret", Base: "raw-secret", Keycode: 424242, Pressed: true},
195 {Keysym: "credential-secret", Keycode: 424242},
196 {Keysym: "BackSpace", Mods: ca, Pressed: true},
197 {Keysym: "raw-secret", Keycode: 14, Mods: ca, Pressed: true},
198 } {
199 t.Run(key.Keysym, func(t *testing.T) {
200 var buf bytes.Buffer
201 security := portsmocks.NewMockSessionSecurity(t)
202 state := ports.SecurityState{Generation: 7, Protected: true}
203 security.EXPECT().Snapshot().Return(state).Once()
204 opts := Options{Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
205 produced := securitySnapshot(opts.Security)
206 ev, err := translateKey(key, opts, produced)
207 if err != nil || errors.Is(err, ErrEmergencyQuit) || ev != key {
208 t.Fatalf("protected key lost/quit: event=%#v err=%v", ev, err)
209 }
210 if got := secureInput(ev, opts.Security, produced); got != (ports.SecurityInput{State: state, Event: key}) {
211 t.Fatalf("wrong production epoch: %#v", got)
212 }
213 if buf.Len() != 0 {
214 t.Fatalf("protected key logged identifiers: %s", &buf)
215 }
216 })
217 }
218}
219
220func TestKeyboardPolicyUnlockedAndVT(t *testing.T) {
221 ca := ports.ModCtrl | ports.ModAlt
222 for _, protected := range []bool{false, true} {
223 t.Run(map[bool]string{false: "unlocked", true: "protected"}[protected], func(t *testing.T) {
224 var buf bytes.Buffer
225 security := portsmocks.NewMockSessionSecurity(t)
226 state := ports.SecurityState{Generation: 9, Protected: protected}
227 security.EXPECT().Snapshot().Return(state).Once()
228 seat := portsmocks.NewMockSeat(t)
229 seat.EXPECT().SwitchVT(2).Once()
230 opts := Options{Security: security, Seat: seat, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
231 ev, err := translateKey(ports.KeyEvent{Keysym: "XF86Switch_VT_2", Keycode: 60, Mods: ca, Pressed: true}, opts, securitySnapshot(opts.Security))
232 if ev != nil || err != nil {
233 t.Fatalf("VT not consumed: %#v %v", ev, err)
234 }
235 if protected && buf.Len() != 0 {
236 t.Fatalf("protected VT key logged: %s", &buf)
237 }
238 if !protected && !strings.Contains(buf.String(), "XF86Switch_VT_2") {
239 t.Fatalf("unlocked debug logging changed: %s", &buf)
240 }
241 })
242 }
243 var buf bytes.Buffer
244 opts := Options{LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
245 key := ports.KeyEvent{Keysym: "q", Keycode: 16, Pressed: true}
246 state := securitySnapshot(nil)
247 ev, err := translateKey(key, opts, state)
248 if err != nil || secureInput(ev, nil, state) != key || !strings.Contains(buf.String(), `"keysym":"q"`) {
249 t.Fatalf("nil gate changed unlocked behavior: %#v %v %s", ev, err, &buf)
250 }
251 ev, err = translateKey(ports.KeyEvent{Keysym: "BackSpace", Mods: ca, Pressed: true}, opts, state)
252 if ev != nil || !errors.Is(err, ErrEmergencyQuit) || !strings.Contains(buf.String(), "emergency-key") {
253 t.Fatalf("unlocked emergency quit changed: %#v %v %s", ev, err, &buf)
254 }
255}
256
257func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
258 security := portsmocks.NewMockSessionSecurity(t)
259 old := ports.SecurityState{Generation: 11, Protected: true}
260 newState := ports.SecurityState{Generation: 12}
261 events := []ports.InputEvent{
262 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
263 ports.PointerButton{Button: 272}, ports.PointerAxis{},
264 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
265 }
266 security.EXPECT().Snapshot().Return(old).Times(len(events))
267 security.EXPECT().Snapshot().Return(newState).Once()
268 f := newForwarder(zerowrap.Default())
269 for _, ev := range events {
270 f.push(secureInput(ev, security, securitySnapshot(security)))
271 }
272 if securitySnapshot(security) != newState {
273 t.Fatal("did not transition")
274 }
275 for _, want := range events {
276 got, ok := f.pop()
277 if !ok || got != (ports.SecurityInput{State: old, Event: want}) {
278 t.Fatalf("queued event changed epoch: %#v", got)
279 }
280 }
281 if secureInput(nil, security, old) != nil {
282 t.Fatal("nil translated event was wrapped")
283 }
284}
285
286func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) {
287 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
288 if err != nil {
289 t.Fatal(err)
290 }
291 defer km.Close()
292 initial := ports.SecurityState{}
293 locked := ports.SecurityState{Generation: 1, Protected: true}
294 unlocked := ports.SecurityState{Generation: 2}
295 security := portsmocks.NewMockSessionSecurity(t)
296 security.EXPECT().Snapshot().Return(initial).Times(3)
297 security.EXPECT().Snapshot().Return(locked).Times(4)
298 security.EXPECT().Snapshot().Return(unlocked).Times(5)
299 var buf bytes.Buffer
300 opts := Options{Keymap: km, Security: security, LogKeys: true, Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &buf})}
301 produce := func(code uint32, down bool) ports.InputEvent {
302 t.Helper()
303 state := securitySnapshot(opts.Security)
304 ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state)
305 if err != nil {
306 t.Fatal(err)
307 }
308 return secureInput(ev, opts.Security, state)
309 }
310 for _, code := range []uint32{125, 29, 56} {
311 produce(code, true)
312 }
313 buf.Reset()
314 for _, code := range []uint32{125, 29, 56} {
315 if ev := produce(code, true); ev != nil {
316 t.Fatalf("inherited repeat delivered: %#v", ev)
317 }
318 }
319 // Ctrl+Alt held before lock must not activate emergency quit, even on a
320 // fresh Backspace. Super must not contaminate the credential either.
321 if ev := produce(14, true).(ports.SecurityInput).Event.(ports.KeyEvent); ev.Mods != 0 || ev.State != (ports.ModState{}) {
322 t.Fatalf("protected fresh key inherited native state: %+v", ev)
323 }
324 if buf.Len() != 0 {
325 t.Fatalf("protected keyboard logged: %s", &buf)
326 }
327 if ev := produce(16, true).(ports.SecurityInput); ev.State != unlocked || ev.Event.(ports.KeyEvent).Mods != 0 || ev.Event.(ports.KeyEvent).State != (ports.ModState{}) {
328 t.Fatalf("unlocked q inherited state: %#v", ev)
329 }
330 buf.Reset()
331 for _, code := range []uint32{14, 56, 29, 125} {
332 if ev := produce(code, false); ev != nil {
333 t.Fatalf("inherited release delivered: %#v", ev)
334 }
335 }
336 if buf.Len() != 0 {
337 t.Fatalf("quarantined release logged: %s", &buf)
338 }
339}
340
341// The cursor is placed in target pixels: after the output transform.
342func TestPointerMovedTransform(t *testing.T) {
343 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}})
344 p.set(10, 20)
345 var out string
346 var px, py float64
347 p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true)
348 if out != "A" || px != 20 || py != 90 {
349 t.Fatal(out, px, py)
350 }
351}