1package neferclient
2
3import (
4 "os"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8 "golang.org/x/sys/unix"
9
10 "github.com/bnema/purego-xkbcommon/raw"
11)
12
13// Evdev codes used below (US QWERTY positions).
14const (
15 keyQ = 16
16 keyE = 18
17 keyBackspace = 14
18 keyEnter = 28
19 keyU = 22
20 keyLeftShift = 42
21 keyCircum = 41 // de: dead_circumflex
22)
23
24// testKeyboard is a keyboard on a real xkb keymap compiled from rules.
25func testKeyboard(t testing.TB, locale, layout string) *keyboard {
26 t.Helper()
27 k, err := newKeyboard(locale)
28 require.NoError(t, err)
29 t.Cleanup(k.close)
30 km, err := k.ctx.NewKeymapRules("evdev", "pc105", layout, "", "")
31 require.NoError(t, err)
32 require.NoError(t, k.replace(km))
33 k.setFocus(true)
34 return k
35}
36
37func pressKey(t testing.TB, k *keyboard, code uint32) (KeyEvent, bool) {
38 t.Helper()
39 var ev KeyEvent
40 ok, err := k.press(code, &ev)
41 require.NoError(t, err)
42 return ev, ok
43}
44
45func releaseKey(k *keyboard, code uint32) (KeyEvent, bool) {
46 var ev KeyEvent
47 ok := k.release(code, &ev)
48 return ev, ok
49}
50
51func TestKeyboardLayoutsAndModifiers(t *testing.T) {
52 for _, tc := range []struct {
53 layout string
54 code uint32
55 want string
56 }{{"us", keyQ, "q"}, {"fr", keyQ, "a"}, {"de", 21, "z"}} {
57 k := testKeyboard(t, "en_US.UTF-8", tc.layout)
58 ev, ok := pressKey(t, k, tc.code)
59 require.True(t, ok)
60 require.Equal(t, tc.want, string(ev.Text), tc.layout)
61 require.True(t, ev.Pressed)
62 require.NotZero(t, ev.Keysym)
63 require.Equal(t, tc.code, ev.Keycode)
64 rel, ok := releaseKey(k, tc.code)
65 require.True(t, ok)
66 require.False(t, rel.Pressed)
67 require.Nil(t, rel.Text)
68 require.Equal(t, ev.Keysym, rel.Keysym)
69 }
70
71 k := testKeyboard(t, "en_US.UTF-8", "us")
72 idx, err := k.keymap.ModIndex("Shift")
73 require.NoError(t, err)
74 require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
75 ev, _ := pressKey(t, k, keyQ)
76 require.Equal(t, "Q", string(ev.Text))
77 require.Equal(t, ModShift, ev.Modifiers)
78
79 // The compositor's mask survives a keymap change.
80 km, err := k.ctx.NewKeymapRules("evdev", "pc105", "fr", "", "")
81 require.NoError(t, err)
82 require.NoError(t, k.replace(km))
83 k.setFocus(true)
84 ev, _ = pressKey(t, k, keyQ)
85 require.Equal(t, "A", string(ev.Text))
86}
87
88func TestKeyboardKeymapFD(t *testing.T) {
89 data, err := os.ReadFile("testdata/fr.xkb")
90 require.NoError(t, err)
91 f, err := os.CreateTemp(t.TempDir(), "keymap")
92 require.NoError(t, err)
93 defer f.Close()
94 _, err = f.Write(append(data, 0))
95 require.NoError(t, err)
96
97 k := testKeyboard(t, "C", "us")
98 fd, err := unix.Dup(int(f.Fd()))
99 require.NoError(t, err)
100 require.NoError(t, k.replaceFD(fd, len(data)+1))
101 _, err = unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
102 require.ErrorIs(t, err, unix.EBADF, "replaceFD consumes the descriptor")
103 k.setFocus(true)
104 ev, _ := pressKey(t, k, keyQ)
105 require.Equal(t, "a", string(ev.Text))
106
107 // A failing map keeps the previous one and still consumes its fd.
108 bad, err := unix.Dup(int(f.Fd()))
109 require.NoError(t, err)
110 require.Error(t, k.replaceFD(bad, 5))
111 _, err = unix.FcntlInt(uintptr(bad), unix.F_GETFD, 0)
112 require.ErrorIs(t, err, unix.EBADF)
113 ev, _ = pressKey(t, k, keyQ)
114 require.Equal(t, "a", string(ev.Text))
115}
116
117func TestKeyboardComposeAndFocus(t *testing.T) {
118 k := testKeyboard(t, "en_US.UTF-8", "de")
119 dead, _ := pressKey(t, k, keyCircum)
120 require.Equal(t, uint32(raw.XKB_KEY_dead_circumflex), dead.Keysym)
121 require.Nil(t, dead.Text)
122 releaseKey(k, keyCircum)
123 composed, _ := pressKey(t, k, keyE)
124 require.Equal(t, "ê", string(composed.Text))
125 releaseKey(k, keyE)
126
127 pressKey(t, k, keyCircum)
128 k.setFocus(false)
129 _, ok := pressUnchecked(k, keyE)
130 require.False(t, ok, "no key without focus")
131 k.setFocus(true)
132 ev, _ := pressKey(t, k, keyE)
133 require.Equal(t, "e", string(ev.Text), "focus loss resets compose")
134}
135
136func pressUnchecked(k *keyboard, code uint32) (KeyEvent, bool) {
137 var ev KeyEvent
138 ok, _ := k.press(code, &ev)
139 return ev, ok
140}
141
142func TestKeyboardRepeatTimer(t *testing.T) {
143 k := testKeyboard(t, "C", "us")
144 k.setRepeat(20, 50)
145 _, ok := pressKey(t, k, keyQ)
146 require.True(t, ok)
147 require.Equal(t, uint32(keyQ), k.repeating)
148 var ev KeyEvent
149 require.Eventually(t, k.expired, 2e9, 5e6)
150 ok, err := k.repeat(&ev)
151 require.NoError(t, err)
152 require.True(t, ok)
153 require.True(t, ev.Repeat)
154 require.Equal(t, "q", string(ev.Text))
155
156 releaseKey(k, keyQ)
157 require.Zero(t, k.repeating)
158 require.False(t, k.expired(), "timer disarmed on release")
159
160 // Modifiers never repeat.
161 pressKey(t, k, keyLeftShift)
162 require.Zero(t, k.repeating)
163 // A rate of 0 disables repeat.
164 k.setRepeat(0, 0)
165 pressKey(t, k, keyQ)
166 require.Zero(t, k.repeating)
167}
168
169func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {
170 k := testKeyboard(t, "C", "us")
171 k.setRepeat(100, 1)
172 pressKey(t, k, keyQ)
173 idx, err := k.keymap.ModIndex("Shift")
174 require.NoError(t, err)
175 require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
176 require.Eventually(t, k.expired, 2e9, 1e6)
177 var ev KeyEvent
178 ok, err := k.repeat(&ev)
179 require.NoError(t, err)
180 require.True(t, ok)
181 require.Equal(t, "Q", string(ev.Text))
182}
183
184// TestSecretKeysNeverReachText covers the secret rules end to end on the
185// keyboard: printable text only into the buffer, BackSpace by code point,
186// every other key delivered without text.
187func TestSecretKeysNeverReachText(t *testing.T) {
188 k := testKeyboard(t, "en_US.UTF-8", "de")
189 buf := NewSecretBuffer(8)
190 k.setSecret(buf)
191
192 press := func(code uint32) KeyEvent {
193 ev, ok := pressKey(t, k, code)
194 require.True(t, ok)
195 require.Nil(t, ev.Text, "secret mode never fills KeyEvent.Text")
196 rel, ok := releaseKey(k, code)
197 require.True(t, ok)
198 require.Equal(t, ev.Secret, rel.Secret, "the release is hidden like the press")
199 if ev.Secret {
200 require.Zero(t, rel.Keycode)
201 require.Zero(t, rel.Keysym)
202 }
203 return ev
204 }
205 for _, code := range []uint32{keyQ, keyCircum, keyE} { // text, dead key, composed ê
206 ev := press(code)
207 require.True(t, ev.Secret, "text keys are hidden")
208 require.Zero(t, ev.Keycode)
209 require.Zero(t, ev.Keysym)
210 }
211 require.Equal(t, "qê", string(buf.Bytes()))
212 require.Equal(t, 2, buf.Len())
213
214 bs := press(keyBackspace)
215 require.False(t, bs.Secret)
216 require.Equal(t, uint32(raw.XKB_KEY_BackSpace), bs.Keysym, "BackSpace keeps its keysym")
217 require.Equal(t, 1, buf.Len(), "backspace removes one code point")
218 press(keyBackspace)
219 press(keyBackspace) // empty: nothing to remove
220 require.Zero(t, buf.Len())
221
222 press(keyQ)
223 enter := press(keyEnter)
224 require.False(t, enter.Secret)
225 require.Equal(t, uint32(raw.XKB_KEY_Return), enter.Keysym)
226 require.Equal(t, 1, buf.Len(), "Enter is delivered, not typed")
227
228 // Control combinations are keys, not text.
229 idx, err := k.keymap.ModIndex("Control")
230 require.NoError(t, err)
231 require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
232 ev := press(keyU)
233 require.False(t, ev.Secret)
234 require.NotZero(t, ev.Keysym, "Control combinations keep their keysym")
235 require.Equal(t, ModCtrl, ev.Modifiers)
236 require.Equal(t, 1, buf.Len())
237 require.NoError(t, k.setMask(0, 0, 0, 0))
238
239 // Capacity: extra text is dropped, the key is still delivered.
240 for range 10 {
241 press(keyQ)
242 }
243 require.Equal(t, 8, buf.Len())
244
245 // Scratch and the work area are wiped after use.
246 require.Equal(t, make([]byte, scratchSize), k.scratch[:])
247 require.Equal(t, secretWork{}, k.w)
248 for _, h := range k.held {
249 require.Zero(t, h, "no stale held key")
250 }
251
252 k.setSecret(nil)
253 ev, _ = pressKey(t, k, keyQ)
254 require.False(t, ev.Secret)
255 require.Equal(t, "q", string(ev.Text), "ordinary text again once the buffer is removed")
256}
257
258func TestSecretChangedFlag(t *testing.T) {
259 k := testKeyboard(t, "C", "us")
260 buf := NewSecretBuffer(2)
261 k.setSecret(buf)
262 pressKey(t, k, keyQ)
263 require.True(t, k.changed)
264 pressKey(t, k, keyEnter)
265 require.False(t, k.changed)
266 pressKey(t, k, keyBackspace)
267 require.True(t, k.changed)
268 pressKey(t, k, keyBackspace)
269 require.False(t, k.changed, "nothing left to remove")
270}
271
272func TestSecretRepeat(t *testing.T) {
273 k := testKeyboard(t, "C", "us")
274 buf := NewSecretBuffer(16)
275 k.setSecret(buf)
276 k.setRepeat(100, 1)
277 var ev KeyEvent
278 pressKey(t, k, keyQ)
279 for range 2 {
280 require.Eventually(t, k.expired, 2e9, 1e6)
281 ok, err := k.repeat(&ev)
282 require.NoError(t, err)
283 require.True(t, ok)
284 require.Nil(t, ev.Text)
285 }
286 require.Equal(t, "qqq", string(buf.Bytes()))
287
288 // Enter cancels a previous printable repeat.
289 pressKey(t, k, keyEnter)
290 require.Zero(t, k.repeating)
291 releaseKey(k, keyQ)
292 releaseKey(k, keyEnter)
293
294 // Held backspace repeats.
295 pressKey(t, k, keyBackspace)
296 require.Equal(t, uint32(keyBackspace), k.repeating)
297 require.Eventually(t, k.expired, 2e9, 1e6)
298 ok, err := k.repeat(&ev)
299 require.NoError(t, err)
300 require.True(t, ok)
301 require.Equal(t, 1, buf.Len())
302}
303
304func TestSecretRepeatIsHiddenToo(t *testing.T) {
305 k := testKeyboard(t, "C", "us")
306 k.setSecret(NewSecretBuffer(8))
307 k.setRepeat(100, 1)
308 pressKey(t, k, keyQ)
309 require.Eventually(t, k.expired, 2e9, 1e6)
310 var ev KeyEvent
311 ok, err := k.repeat(&ev)
312 require.NoError(t, err)
313 require.True(t, ok)
314 require.True(t, ev.Secret)
315 require.Zero(t, ev.Keycode)
316 require.Zero(t, ev.Keysym)
317}
318
319func TestKeyboardHeldTailAndResetAreCleared(t *testing.T) {
320 k := testKeyboard(t, "C", "us")
321 k.setSecret(NewSecretBuffer(8))
322 pressKey(t, k, keyQ)
323 pressKey(t, k, keyE)
324 releaseKey(k, keyQ)
325 require.Equal(t, heldKey{}, k.held[:cap(k.held)][1], "vacated slot cleared")
326 k.setFocus(false)
327 for _, h := range k.held[:cap(k.held)] {
328 require.Zero(t, h)
329 }
330}
331
332func TestKeyboardIdenticalMaskIsNoOp(t *testing.T) {
333 k := testKeyboard(t, "C", "us")
334 idx, err := k.keymap.ModIndex("Shift")
335 require.NoError(t, err)
336 require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
337 require.Equal(t, ModShift, k.mods)
338 require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
339 require.Equal(t, ModShift, k.mods)
340 require.NoError(t, k.setMask(0, 0, 0, 0))
341 require.Zero(t, k.mods)
342}