1package neferclient_test
2
3import (
4 "encoding/binary"
5 "os"
6 "runtime"
7 "sync"
8 "testing"
9 "time"
10
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13 "golang.org/x/sys/unix"
14
15 "github.com/bnema/neferclient"
16 neferclientmocks "github.com/bnema/neferclient/mocks"
17)
18
19const (
20 seatGlobal = 8
21 shapesGlobal = 9
22)
23
24var seatGlobals = append(append([]wireGlobal{}, surfaceGlobals...),
25 wireGlobal{seatGlobal, "wl_seat", 9}, wireGlobal{shapesGlobal, "wp_cursor_shape_manager_v1", 2})
26
27// seatPeer is the compositor side of a seat: it learns the object ids the
28// client creates and scripts input events.
29type seatPeer struct {
30 t *testing.T
31 srv *wireServer
32
33 mu sync.Mutex
34 pointer, keyboard, shape uint32
35 shapeSerial, shapeValue uint32
36 shapeSets int
37 pointerReleased bool
38}
39
40func newSeatPeer(t *testing.T, srv *wireServer) *seatPeer {
41 p := &seatPeer{t: t, srv: srv}
42 srv.mu.Lock()
43 srv.hook = func(obj uint32, op uint16, body []byte) {
44 srv.mu.Lock()
45 seat, shapes := srv.bound[seatGlobal].id, srv.bound[shapesGlobal].id
46 srv.mu.Unlock()
47 p.mu.Lock()
48 defer p.mu.Unlock()
49 switch {
50 case obj == seat && seat != 0 && op == 0 && len(body) == 4:
51 p.pointer = binary.LittleEndian.Uint32(body)
52 case obj == seat && seat != 0 && op == 1 && len(body) == 4:
53 p.keyboard = binary.LittleEndian.Uint32(body)
54 case obj == shapes && shapes != 0 && op == 1 && len(body) == 8:
55 p.shape = binary.LittleEndian.Uint32(body)
56 case obj == p.shape && p.shape != 0 && op == 1 && len(body) == 8:
57 p.shapeSerial = binary.LittleEndian.Uint32(body)
58 p.shapeValue = binary.LittleEndian.Uint32(body[4:])
59 p.shapeSets++
60 case obj == p.pointer && p.pointer != 0 && op == 1:
61 p.pointerReleased = true
62 }
63 }
64 srv.mu.Unlock()
65 return p
66}
67
68func (p *seatPeer) ids() (pointer, keyboard uint32) {
69 p.mu.Lock()
70 defer p.mu.Unlock()
71 return p.pointer, p.keyboard
72}
73
74func (p *seatPeer) seatID() uint32 {
75 p.srv.mu.Lock()
76 defer p.srv.mu.Unlock()
77 return p.srv.bound[seatGlobal].id
78}
79
80func fixed(v float64) uint32 { return uint32(int32(v * 256)) }
81
82func negU32(v int32) uint32 { return uint32(-v) }
83
84func (p *seatPeer) capabilities(caps uint32) {
85 p.srv.write(frame(p.seatID(), 0, appendU32(nil, caps)))
86}
87
88// keymap sends wl_keyboard.keymap with testdata/fr.xkb in a descriptor.
89func (p *seatPeer) keymap(keyboard uint32, format uint32) {
90 p.t.Helper()
91 data, err := os.ReadFile("testdata/fr.xkb")
92 require.NoError(p.t, err)
93 data = append(data, 0)
94 fd, err := unix.MemfdCreate("keymap", unix.MFD_CLOEXEC)
95 require.NoError(p.t, err)
96 defer unix.Close(fd)
97 _, err = unix.Write(fd, data)
98 require.NoError(p.t, err)
99 body := appendU32(nil, format)
100 body = appendU32(body, uint32(len(data)))
101 _, _, err = p.srv.conn.WriteMsgUnix(frame(keyboard, 0, body), unix.UnixRights(fd), nil)
102 require.NoError(p.t, err)
103}
104
105func (p *seatPeer) keyboardEnter(kb, surface uint32) {
106 body := appendU32(appendU32(nil, 1), surface)
107 body = appendU32(body, 0) // empty keys array
108 p.srv.write(frame(kb, 1, body))
109}
110
111func (p *seatPeer) key(kb, code, state uint32) []byte {
112 b := appendU32(appendU32(nil, 1), 0)
113 b = appendU32(appendU32(b, code), state)
114 return frame(kb, 3, b)
115}
116
117func seatSetup(t *testing.T) (*neferclient.Conn, *wireServer, *seatPeer, *neferclient.Surface) {
118 t.Helper()
119 c, srv := connectWire(t, seatGlobals, surfaceOutputs)
120 peer := newSeatPeer(t, srv)
121 s, err := c.NewToplevel("seat", 64, 48)
122 require.NoError(t, err)
123 require.NotNil(t, c.Seat())
124 require.NoError(t, c.Roundtrip())
125 peer.capabilities(3)
126 require.NoError(t, c.Roundtrip())
127 require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
128 require.NoError(t, c.Roundtrip())
129 return c, srv, peer, s
130}
131
132func TestSeatPointerEvents(t *testing.T) {
133 c, srv, peer, s := seatSetup(t)
134 pointer, keyboard := peer.ids()
135 require.NotZero(t, pointer)
136 require.NotZero(t, keyboard)
137 surf := s.SurfaceObjectID()
138
139 var got []neferclient.PointerEvent
140 h := neferclientmocks.NewMockHandler(t)
141 h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
142
143 // A motion outside any surface is ignored; the enter binds the position.
144 srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(1)), appendU32(nil, fixed(2))...)))
145 b := appendU32(appendU32(nil, 77), surf)
146 b = appendU32(appendU32(b, fixed(10.5)), fixed(20.25))
147 srv.write(frame(pointer, 0, b)) // enter
148 srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(11)), appendU32(nil, fixed(21))...)))
149 srv.write(frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 78), 0), 0x110), 1))) // button press
150 srv.write(frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(15)))) // axis vertical
151 srv.write(frame(pointer, 9, appendU32(appendU32(nil, 1), negU32(120)))) // value120 horizontal
152 srv.write(frame(pointer, 7, appendU32(appendU32(nil, 0), 0))) // axis stop
153 srv.write(frame(pointer, 1, appendU32(appendU32(nil, 79), surf))) // leave
154 srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(3)), appendU32(nil, fixed(4))...)))
155 require.NoError(t, c.Roundtrip())
156 dispatchUntil(t, c, h, func() bool { return len(got) >= 7 })
157
158 require.Len(t, got, 7)
159 id := s.ID()
160 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerEnter, X: 10.5, Y: 20.25, Serial: 77}, got[0])
161 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerMotion, X: 11, Y: 21}, got[1])
162 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerButton, X: 11, Y: 21, Button: 0x110, Pressed: true, Serial: 78}, got[2])
163 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 0, DY: 15}, got[3])
164 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 1, Value120: -120}, got[4])
165 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxisStop, X: 11, Y: 21}, got[5])
166 require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerLeave, X: 11, Y: 21}, got[6])
167}
168
169func TestSeatCursorShape(t *testing.T) {
170 c, srv, peer, s := seatSetup(t)
171 pointer, _ := peer.ids()
172 seat := c.Seat()
173 require.NoError(t, seat.SetCursor(neferclient.CursorPointer), "outside: no-op")
174 require.NoError(t, c.Roundtrip())
175 require.Zero(t, peer.shapeSets)
176
177 enter := func(serial uint32) {
178 b := appendU32(appendU32(nil, serial), s.SurfaceObjectID())
179 srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
180 require.NoError(t, c.Roundtrip())
181 require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
182 }
183 enter(40)
184 require.NoError(t, seat.SetCursor(neferclient.CursorText))
185 require.NoError(t, seat.SetCursor(neferclient.CursorText), "unchanged: nothing sent")
186 require.NoError(t, c.Roundtrip())
187 peer.mu.Lock()
188 require.Equal(t, 1, peer.shapeSets)
189 require.Equal(t, uint32(40), peer.shapeSerial)
190 require.Equal(t, uint32(neferclient.CursorText), peer.shapeValue)
191 peer.mu.Unlock()
192
193 enter(41) // a new enter resets the shape and carries a new serial
194 require.NoError(t, seat.SetCursor(neferclient.CursorText))
195 require.NoError(t, c.Roundtrip())
196 peer.mu.Lock()
197 require.Equal(t, 2, peer.shapeSets)
198 require.Equal(t, uint32(41), peer.shapeSerial)
199 peer.mu.Unlock()
200 require.Error(t, seat.SetCursor(0))
201 require.Error(t, seat.SetCursor(neferclient.CursorAllResize+1))
202}
203
204func TestSeatKeyboardFocusTextAndSecret(t *testing.T) {
205 c, srv, peer, s := seatSetup(t)
206 _, kb := peer.ids()
207 surf := s.SurfaceObjectID()
208 seat := c.Seat()
209
210 var keys []neferclient.KeyEvent
211 var texts []string
212 var focus []bool
213 var changed []int
214 h := neferclientmocks.NewMockHandler(t)
215 h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
216 keys = append(keys, *ev)
217 texts = append(texts, string(ev.Text)) // the only place text is copied: the assertion of the test
218 }).Return()
219 h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
220 h.EXPECT().SecretChanged(mock.Anything).Run(func(n int) { changed = append(changed, n) }).Return()
221
222 peer.keymap(kb, 1) // French AZERTY: evdev 16 is "a"
223 srv.write(frame(kb, 5, append(appendU32(nil, 0), appendU32(nil, 0)...)))
224 peer.keyboardEnter(kb, surf)
225 srv.write(peer.key(kb, 16, 1))
226 srv.write(peer.key(kb, 16, 0))
227 require.NoError(t, c.Roundtrip())
228 dispatchUntil(t, c, h, func() bool { return len(keys) >= 2 })
229 require.Equal(t, []bool{true}, focus)
230 require.Equal(t, "a", texts[0])
231 require.True(t, keys[0].Pressed)
232 require.Equal(t, s.ID(), keys[0].Surface)
233 require.Equal(t, "", texts[1])
234 require.False(t, keys[1].Pressed)
235 require.Empty(t, changed)
236
237 // With a secret buffer: the text goes only into it.
238 buf := neferclient.NewSecretBuffer(8)
239 seat.SetSecret(buf)
240 srv.write(peer.key(kb, 16, 1))
241 srv.write(peer.key(kb, 16, 0))
242 srv.write(peer.key(kb, 30, 1)) // "q" on AZERTY
243 srv.write(peer.key(kb, 30, 0))
244 srv.write(peer.key(kb, 14, 1)) // BackSpace
245 srv.write(peer.key(kb, 14, 0))
246 srv.write(peer.key(kb, 28, 1)) // Return: delivered, no text
247 require.NoError(t, c.Roundtrip())
248 dispatchUntil(t, c, h, func() bool { return len(keys) >= 2+7 })
249 for _, text := range texts[2:] {
250 require.Empty(t, text)
251 }
252 require.Equal(t, "a", string(buf.Bytes()))
253 require.Equal(t, 1, buf.Len())
254 require.Equal(t, []int{1, 2, 1}, changed)
255
256 srv.write(frame(kb, 2, appendU32(appendU32(nil, 5), surf))) // leave
257 require.NoError(t, c.Roundtrip())
258 dispatchUntil(t, c, h, func() bool { return len(focus) >= 2 })
259 require.Equal(t, []bool{true, false}, focus)
260 buf.Wipe()
261}
262
263// TestSeatKeymapFDClosedOnClose: a keymap descriptor queued but never drained
264// is closed by Close. The descriptor count after Close must not depend on
265// whether a keymap was left in the queue.
266func TestSeatKeymapFDClosedOnClose(t *testing.T) {
267 run := func(withKeymap bool) (after int) {
268 t.Run("run", func(t *testing.T) {
269 c, _, peer, _ := seatSetup(t)
270 _, kb := peer.ids()
271 if withKeymap {
272 before := openFDs(t)
273 peer.keymap(kb, 1)
274 require.NoError(t, c.Roundtrip()) // queued, not dispatched
275 require.Greater(t, openFDs(t), before, "the queued event owns a descriptor")
276 }
277 require.NoError(t, c.Close())
278 after = openFDs(t)
279 })
280 return after
281 }
282 without := run(false)
283 require.Equal(t, without, run(true), "descriptor closed with the connection")
284}
285
286// TestSeatKeymapFDClosedOnBadFormat: an unknown keymap format is reported and
287// its descriptor closed; no_keymap (0) is closed silently.
288func TestSeatKeymapFDClosedOnBadFormat(t *testing.T) {
289 c, _, peer, _ := seatSetup(t)
290 _, kb := peer.ids()
291 before := openFDs(t)
292
293 peer.keymap(kb, 0) // no_keymap: no error, no leak
294 require.NoError(t, c.Roundtrip())
295 require.NoError(t, c.Dispatch(neferclientmocks.NewMockHandler(t))) // any Handler call fails
296 require.Equal(t, before, openFDs(t))
297
298 peer.keymap(kb, 7) // unknown format
299 require.NoError(t, c.Roundtrip())
300 var errs int
301 h := neferclientmocks.NewMockHandler(t)
302 h.EXPECT().Error(mock.Anything).Run(func(error) { errs++ }).Return()
303 dispatchUntil(t, c, h, func() bool { return errs == 1 })
304 require.Equal(t, before, openFDs(t))
305}
306
307func TestSeatCapabilityLossReleasesDevices(t *testing.T) {
308 c, srv, peer, s := seatSetup(t)
309 pointer, kb := peer.ids()
310 b := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
311 srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
312 peer.keyboardEnter(kb, s.SurfaceObjectID())
313 require.NoError(t, c.Roundtrip())
314
315 var got []neferclient.PointerEvent
316 var focus []bool
317 h := neferclientmocks.NewMockHandler(t)
318 h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
319 h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
320 dispatchUntil(t, c, h, func() bool { return len(got) == 1 && len(focus) == 1 })
321
322 peer.capabilities(0)
323 require.NoError(t, c.Roundtrip())
324 dispatchUntil(t, c, h, func() bool { return len(got) == 2 && len(focus) == 2 })
325 require.Equal(t, neferclient.PointerLeave, got[1].Kind, "a synthetic leave closes the focus")
326 require.False(t, focus[1])
327 require.NoError(t, c.Roundtrip())
328 peer.mu.Lock()
329 require.True(t, peer.pointerReleased)
330 peer.mu.Unlock()
331}
332
333// TestSeatRepeatThroughEpoll: the repeat timerfd is watched by the connection's
334// epoll loop and the repeat arrives through Dispatch.
335func TestSeatRepeatThroughEpoll(t *testing.T) {
336 c, srv, peer, s := seatSetup(t)
337 _, kb := peer.ids()
338 peer.keymap(kb, 1)
339 srv.write(frame(kb, 5, appendU32(appendU32(nil, 100), 1))) // 100/s after 1 ms
340 peer.keyboardEnter(kb, s.SurfaceObjectID())
341 srv.write(peer.key(kb, 16, 1))
342 require.NoError(t, c.Roundtrip())
343 var repeats int
344 var texts []string
345 h := neferclientmocks.NewMockHandler(t)
346 h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return()
347 h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
348 if ev.Repeat {
349 repeats++
350 texts = append(texts, string(ev.Text))
351 }
352 }).Return()
353 dispatchUntil(t, c, h, func() bool { return repeats >= 3 })
354 require.Equal(t, "a", texts[0])
355
356 srv.write(peer.key(kb, 16, 0))
357 require.NoError(t, c.Roundtrip())
358 require.NoError(t, c.Dispatch(h))
359 n := repeats
360 time.Sleep(50 * time.Millisecond) // five repeat intervals at 100/s
361 require.NoError(t, c.Dispatch(h))
362 require.Equal(t, n, repeats, "release stops the repeat")
363}
364
365// TestAllocKey measures the steady-state key path over a real socketpair, on
366// the real code path (no mocks): key frames in, reader decode, queue, Dispatch
367// with xkb state and translation, and the Handler call.
368//
369// Every path is zero-allocation: releases, text and secret presses, and
370// modifier masks (purego-xkbcommon v0.2.0 dispatches without allocating).
371func TestAllocKey(t *testing.T) {
372 skipUnderRace(t)
373 c, srv, peer, s := seatSetup(t)
374 _, kb := peer.ids()
375 peer.keymap(kb, 1)
376 srv.write(frame(kb, 5, appendU32(appendU32(nil, 0), 0))) // repeat disabled
377 peer.keyboardEnter(kb, s.SurfaceObjectID())
378 require.NoError(t, c.Roundtrip())
379 h := &countingHandler{}
380 require.NoError(t, c.Dispatch(h))
381 require.Zero(t, h.errs)
382
383 run := func(batch []byte, events int) func() {
384 return func() {
385 if _, err := srv.conn.Write(batch); err != nil {
386 t.Fatal(err)
387 }
388 for c.QueueLen() < events {
389 runtime.Gosched()
390 }
391 if err := c.Dispatch(h); err != nil {
392 t.Fatal(err)
393 }
394 }
395 }
396 measure := func(name string, step func()) float64 {
397 step()
398 step()
399 allocs := testing.AllocsPerRun(20, step)
400 t.Logf("%s: allocs per Dispatch: %v", name, allocs)
401 return allocs
402 }
403
404 const pairs = 50
405 var releases []byte // releases of keys that were never pressed: queue + decode + dispatch only
406 for range 2 * pairs {
407 releases = append(releases, peer.key(kb, 16, 0)...)
408 }
409 require.Zero(t, measure("pipeline, 100 releases", run(releases, 2*pairs)), "the library itself allocates nothing per key")
410 require.Zero(t, h.keys, "unheld releases are not delivered")
411
412 var batch []byte
413 for range pairs {
414 batch = append(batch, peer.key(kb, 16, 1)...)
415 batch = append(batch, peer.key(kb, 16, 0)...)
416 }
417 step := run(batch, 2*pairs)
418 text := measure("text, 50 presses + releases", step)
419 require.Zero(t, text, "text presses allocate nothing")
420 require.Equal(t, "a", h.lastText)
421
422 // Modifier masks: an identical mask is a no-op and allocates nothing; a
423 // changing one updates the xkb state, also without allocating.
424 mods := func(depressed uint32) []byte {
425 b := appendU32(appendU32(nil, 1), depressed)
426 return frame(kb, 4, appendU32(appendU32(appendU32(b, 0), 0), 0))
427 }
428 var same, changing []byte
429 for range 100 {
430 same = append(same, mods(0)...)
431 }
432 for i := range 100 {
433 changing = append(changing, mods(uint32(i%2))...)
434 }
435 require.Zero(t, measure("modifiers, 100 identical masks", run(same, 100)), "identical masks cost nothing")
436 changes := measure("modifiers, 100 alternating masks", run(changing, 100))
437 require.Zero(t, changes, "mask changes allocate nothing")
438
439 buf := neferclient.NewSecretBuffer(4096)
440 c.Seat().SetSecret(buf)
441 secret := measure("secret, 50 presses + releases", step)
442 require.Zero(t, secret, "secret presses allocate nothing")
443 require.Empty(t, h.lastText, "no text reaches the handler in secret mode")
444 require.Positive(t, buf.Len())
445 buf.Wipe()
446 require.Zero(t, h.errs)
447}
448
449// TestAllocPointer measures the pointer path over a real socketpair: a batch
450// of motion, button and axis frames in, reader decode, queue, Dispatch and the
451// Handler call. Pointer events need no xkb, so the whole path allocates
452// nothing.
453func TestAllocPointer(t *testing.T) {
454 skipUnderRace(t)
455 c, srv, peer, s := seatSetup(t)
456 pointer, _ := peer.ids()
457 enter := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
458 srv.write(frame(pointer, 0, appendU32(appendU32(enter, fixed(1)), fixed(1))))
459 require.NoError(t, c.Roundtrip())
460 h := &countingHandler{}
461 require.NoError(t, c.Dispatch(h))
462 require.Equal(t, 1, h.pointers)
463
464 const rounds = 40
465 var batch []byte
466 for i := range rounds {
467 batch = append(batch, frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(float64(i))), appendU32(nil, fixed(2))...))...)
468 batch = append(batch, frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 6), 0), 0x110), uint32(i%2)))...)
469 batch = append(batch, frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(2)))...)
470 batch = append(batch, frame(pointer, 9, appendU32(appendU32(nil, 0), 120))...)
471 }
472 step := func() {
473 if _, err := srv.conn.Write(batch); err != nil {
474 t.Fatal(err)
475 }
476 for c.QueueLen() < 4*rounds {
477 runtime.Gosched()
478 }
479 if err := c.Dispatch(h); err != nil {
480 t.Fatal(err)
481 }
482 }
483 step()
484 step()
485 before := h.pointers
486 allocs := testing.AllocsPerRun(20, step)
487 t.Logf("allocs per Dispatch of %d pointer events: %v", 4*rounds, allocs)
488 require.Zero(t, allocs)
489 require.Greater(t, h.pointers, before)
490 require.Zero(t, h.errs)
491}