detached ff9aa47

test: cover seat pointer, keyboard, secret, repeat, keymap fd ownership and key allocations

commit
ff9aa4761dbabf8eaa4586a8176a5680a2b35a78
author
brice · · 2026-10-01 18:37 UTC
parent
7219ece
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +478 -0
M alloc_test.go +11 -0
@@ -17,12 +17,23 @@ import (
1717 type countingHandler struct {
1818 neferclient.NopHandler
1919 fdReady, errs, frames int
20+ keys int
21+ lastText string // set only by the allocation-free comparison below
2022 }
2123
2224 func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
2325 func (h *countingHandler) Error(error) { h.errs++ }
2426 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
2527
28+// Key records the text through a comparison that does not allocate: the
29+// string is only built the first time a different text shows up.
30+func (h *countingHandler) Key(ev *neferclient.KeyEvent) {
31+ h.keys++
32+ if string(ev.Text) != h.lastText && ev.Pressed {
33+ h.lastText = string(ev.Text)
34+ }
35+}
36+
2637 // TestAllocDispatch measures the whole steady-state pipeline over a real
2738 // socketpair: wire frames in, reader decode, queue, Dispatch (output state
2839 // updates, an output commit and a watched-fd notification with epoll re-arm). testing's
@@ -17,12 +17,23 @@ import (
17 type countingHandler struct {17 type countingHandler struct {
18 neferclient.NopHandler18 neferclient.NopHandler
19 fdReady, errs, frames int19 fdReady, errs, frames int
20+ keys int
21+ lastText string // set only by the allocation-free comparison below
20 }22 }
21 23
22 func (h *countingHandler) FDReady(uint64) { h.fdReady++ }24 func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
23 func (h *countingHandler) Error(error) { h.errs++ }25 func (h *countingHandler) Error(error) { h.errs++ }
24 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }26 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
25 27
28+// Key records the text through a comparison that does not allocate: the
29+// string is only built the first time a different text shows up.
30+func (h *countingHandler) Key(ev *neferclient.KeyEvent) {
31+ h.keys++
32+ if string(ev.Text) != h.lastText && ev.Pressed {
33+ h.lastText = string(ev.Text)
34+ }
35+}
36+
26 // TestAllocDispatch measures the whole steady-state pipeline over a real37 // TestAllocDispatch measures the whole steady-state pipeline over a real
27 // socketpair: wire frames in, reader decode, queue, Dispatch (output state38 // socketpair: wire frames in, reader decode, queue, Dispatch (output state
28 // updates, an output commit and a watched-fd notification with epoll re-arm). testing's39 // updates, an output commit and a watched-fd notification with epoll re-arm). testing's
M headless_surface_test.go +31 -0
@@ -104,3 +104,34 @@ func TestHeadlessLockUnlock(t *testing.T) {
104104 }
105105 require.NoError(t, c.Roundtrip())
106106 }
107+
108+// TestHeadlessSeat binds the seat against a real compositor and drives it
109+// through a surface: the seat bind, capability handling and cursor request
110+// must not fail, whatever devices the headless backend offers.
111+func TestHeadlessSeat(t *testing.T) {
112+ c := connectHeadless(t)
113+ s, err := c.NewLayerSurface(neferclient.LayerConfig{
114+ Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
115+ Width: 100, Height: 40, Keyboard: neferclient.KeyboardOnDemand,
116+ })
117+ require.NoError(t, err)
118+ seat := c.Seat()
119+ require.NotNil(t, seat)
120+ seat.SetSecret(neferclient.NewSecretBuffer(16))
121+ seat.SetSecret(nil)
122+
123+ var configured bool
124+ h := neferclientmocks.NewMockHandler(t)
125+ h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
126+ h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
127+ h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
128+ h.EXPECT().Pointer(mock.Anything).Return().Maybe()
129+ h.EXPECT().Key(mock.Anything).Return().Maybe()
130+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return().Maybe()
131+ h.EXPECT().SecretChanged(mock.Anything).Return().Maybe()
132+ dispatchUntil(t, c, h, func() bool { return configured })
133+ require.NoError(t, c.Roundtrip())
134+ require.NoError(t, c.Dispatch(h))
135+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer))
136+ require.NoError(t, s.Close())
137+}
@@ -104,3 +104,34 @@ func TestHeadlessLockUnlock(t *testing.T) {
104 }104 }
105 require.NoError(t, c.Roundtrip())105 require.NoError(t, c.Roundtrip())
106 }106 }
107+
108+// TestHeadlessSeat binds the seat against a real compositor and drives it
109+// through a surface: the seat bind, capability handling and cursor request
110+// must not fail, whatever devices the headless backend offers.
111+func TestHeadlessSeat(t *testing.T) {
112+ c := connectHeadless(t)
113+ s, err := c.NewLayerSurface(neferclient.LayerConfig{
114+ Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
115+ Width: 100, Height: 40, Keyboard: neferclient.KeyboardOnDemand,
116+ })
117+ require.NoError(t, err)
118+ seat := c.Seat()
119+ require.NotNil(t, seat)
120+ seat.SetSecret(neferclient.NewSecretBuffer(16))
121+ seat.SetSecret(nil)
122+
123+ var configured bool
124+ h := neferclientmocks.NewMockHandler(t)
125+ h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
126+ h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
127+ h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
128+ h.EXPECT().Pointer(mock.Anything).Return().Maybe()
129+ h.EXPECT().Key(mock.Anything).Return().Maybe()
130+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return().Maybe()
131+ h.EXPECT().SecretChanged(mock.Anything).Return().Maybe()
132+ dispatchUntil(t, c, h, func() bool { return configured })
133+ require.NoError(t, c.Roundtrip())
134+ require.NoError(t, c.Dispatch(h))
135+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer))
136+ require.NoError(t, s.Close())
137+}
A seat_wire_test.go +436 -0
@@ -0,0 +1,436 @@
1+package neferclient_test
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "runtime"
7+ "sync"
8+ "testing"
9+
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+ "golang.org/x/sys/unix"
13+
14+ "github.com/bnema/neferclient"
15+ neferclientmocks "github.com/bnema/neferclient/mocks"
16+)
17+
18+const (
19+ seatGlobal = 8
20+ shapesGlobal = 9
21+)
22+
23+var seatGlobals = append(append([]wireGlobal{}, surfaceGlobals...),
24+ wireGlobal{seatGlobal, "wl_seat", 9}, wireGlobal{shapesGlobal, "wp_cursor_shape_manager_v1", 2})
25+
26+// seatPeer is the compositor side of a seat: it learns the object ids the
27+// client creates and scripts input events.
28+type seatPeer struct {
29+ t *testing.T
30+ srv *wireServer
31+
32+ mu sync.Mutex
33+ pointer, keyboard, shape uint32
34+ shapeSerial, shapeValue uint32
35+ shapeSets int
36+ pointerReleased bool
37+}
38+
39+func newSeatPeer(t *testing.T, srv *wireServer) *seatPeer {
40+ p := &seatPeer{t: t, srv: srv}
41+ srv.mu.Lock()
42+ srv.hook = func(obj uint32, op uint16, body []byte) {
43+ srv.mu.Lock()
44+ seat, shapes := srv.bound[seatGlobal].id, srv.bound[shapesGlobal].id
45+ srv.mu.Unlock()
46+ p.mu.Lock()
47+ defer p.mu.Unlock()
48+ switch {
49+ case obj == seat && seat != 0 && op == 0 && len(body) == 4:
50+ p.pointer = binary.LittleEndian.Uint32(body)
51+ case obj == seat && seat != 0 && op == 1 && len(body) == 4:
52+ p.keyboard = binary.LittleEndian.Uint32(body)
53+ case obj == shapes && shapes != 0 && op == 1 && len(body) == 8:
54+ p.shape = binary.LittleEndian.Uint32(body)
55+ case obj == p.shape && p.shape != 0 && op == 1 && len(body) == 8:
56+ p.shapeSerial = binary.LittleEndian.Uint32(body)
57+ p.shapeValue = binary.LittleEndian.Uint32(body[4:])
58+ p.shapeSets++
59+ case obj == p.pointer && p.pointer != 0 && op == 1:
60+ p.pointerReleased = true
61+ }
62+ }
63+ srv.mu.Unlock()
64+ return p
65+}
66+
67+func (p *seatPeer) ids() (pointer, keyboard uint32) {
68+ p.mu.Lock()
69+ defer p.mu.Unlock()
70+ return p.pointer, p.keyboard
71+}
72+
73+func (p *seatPeer) seatID() uint32 {
74+ p.srv.mu.Lock()
75+ defer p.srv.mu.Unlock()
76+ return p.srv.bound[seatGlobal].id
77+}
78+
79+func fixed(v float64) uint32 { return uint32(int32(v * 256)) }
80+
81+func negU32(v int32) uint32 { return uint32(-v) }
82+
83+func (p *seatPeer) capabilities(caps uint32) {
84+ p.srv.write(frame(p.seatID(), 0, appendU32(nil, caps)))
85+}
86+
87+// keymap sends wl_keyboard.keymap with testdata/fr.xkb in a descriptor.
88+func (p *seatPeer) keymap(keyboard uint32, format uint32) {
89+ p.t.Helper()
90+ data, err := os.ReadFile("testdata/fr.xkb")
91+ require.NoError(p.t, err)
92+ data = append(data, 0)
93+ fd, err := unix.MemfdCreate("keymap", unix.MFD_CLOEXEC)
94+ require.NoError(p.t, err)
95+ defer unix.Close(fd)
96+ _, err = unix.Write(fd, data)
97+ require.NoError(p.t, err)
98+ body := appendU32(nil, format)
99+ body = appendU32(body, uint32(len(data)))
100+ _, _, err = p.srv.conn.WriteMsgUnix(frame(keyboard, 0, body), unix.UnixRights(fd), nil)
101+ require.NoError(p.t, err)
102+}
103+
104+func (p *seatPeer) keyboardEnter(kb, surface uint32) {
105+ body := appendU32(appendU32(nil, 1), surface)
106+ body = appendU32(body, 0) // empty keys array
107+ p.srv.write(frame(kb, 1, body))
108+}
109+
110+func (p *seatPeer) key(kb, code, state uint32) []byte {
111+ b := appendU32(appendU32(nil, 1), 0)
112+ b = appendU32(appendU32(b, code), state)
113+ return frame(kb, 3, b)
114+}
115+
116+func seatSetup(t *testing.T) (*neferclient.Conn, *wireServer, *seatPeer, *neferclient.Surface) {
117+ t.Helper()
118+ c, srv := connectWire(t, seatGlobals, surfaceOutputs)
119+ peer := newSeatPeer(t, srv)
120+ s, err := c.NewToplevel("seat", 64, 48)
121+ require.NoError(t, err)
122+ require.NotNil(t, c.Seat())
123+ require.NoError(t, c.Roundtrip())
124+ peer.capabilities(3)
125+ require.NoError(t, c.Roundtrip())
126+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
127+ require.NoError(t, c.Roundtrip())
128+ return c, srv, peer, s
129+}
130+
131+func TestSeatPointerEvents(t *testing.T) {
132+ c, srv, peer, s := seatSetup(t)
133+ pointer, keyboard := peer.ids()
134+ require.NotZero(t, pointer)
135+ require.NotZero(t, keyboard)
136+ surf := s.SurfaceObjectID()
137+
138+ var got []neferclient.PointerEvent
139+ h := neferclientmocks.NewMockHandler(t)
140+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
141+
142+ // A motion outside any surface is ignored; the enter binds the position.
143+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(1)), appendU32(nil, fixed(2))...)))
144+ b := appendU32(appendU32(nil, 77), surf)
145+ b = appendU32(appendU32(b, fixed(10.5)), fixed(20.25))
146+ srv.write(frame(pointer, 0, b)) // enter
147+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(11)), appendU32(nil, fixed(21))...)))
148+ srv.write(frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 78), 0), 0x110), 1))) // button press
149+ srv.write(frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(15)))) // axis vertical
150+ srv.write(frame(pointer, 9, appendU32(appendU32(nil, 1), negU32(120)))) // value120 horizontal
151+ srv.write(frame(pointer, 7, appendU32(appendU32(nil, 0), 0))) // axis stop
152+ srv.write(frame(pointer, 1, appendU32(appendU32(nil, 79), surf))) // leave
153+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(3)), appendU32(nil, fixed(4))...)))
154+ require.NoError(t, c.Roundtrip())
155+ dispatchUntil(t, c, h, func() bool { return len(got) >= 7 })
156+
157+ require.Len(t, got, 7)
158+ id := s.ID()
159+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerEnter, X: 10.5, Y: 20.25}, got[0])
160+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerMotion, X: 11, Y: 21}, got[1])
161+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerButton, X: 11, Y: 21, Button: 0x110, Pressed: true}, got[2])
162+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 0, DY: 15}, got[3])
163+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 1, Value120: -120}, got[4])
164+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxisStop, X: 11, Y: 21}, got[5])
165+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerLeave, X: 11, Y: 21}, got[6])
166+}
167+
168+func TestSeatCursorShape(t *testing.T) {
169+ c, srv, peer, s := seatSetup(t)
170+ pointer, _ := peer.ids()
171+ seat := c.Seat()
172+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer), "outside: no-op")
173+ require.NoError(t, c.Roundtrip())
174+ require.Zero(t, peer.shapeSets)
175+
176+ enter := func(serial uint32) {
177+ b := appendU32(appendU32(nil, serial), s.SurfaceObjectID())
178+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
179+ require.NoError(t, c.Roundtrip())
180+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
181+ }
182+ enter(40)
183+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
184+ require.NoError(t, seat.SetCursor(neferclient.CursorText), "unchanged: nothing sent")
185+ require.NoError(t, c.Roundtrip())
186+ peer.mu.Lock()
187+ require.Equal(t, 1, peer.shapeSets)
188+ require.Equal(t, uint32(40), peer.shapeSerial)
189+ require.Equal(t, uint32(neferclient.CursorText), peer.shapeValue)
190+ peer.mu.Unlock()
191+
192+ enter(41) // a new enter resets the shape and carries a new serial
193+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
194+ require.NoError(t, c.Roundtrip())
195+ peer.mu.Lock()
196+ require.Equal(t, 2, peer.shapeSets)
197+ require.Equal(t, uint32(41), peer.shapeSerial)
198+ peer.mu.Unlock()
199+ require.Error(t, seat.SetCursor(0))
200+ require.Error(t, seat.SetCursor(neferclient.CursorAllResize+1))
201+}
202+
203+func TestSeatKeyboardFocusTextAndSecret(t *testing.T) {
204+ c, srv, peer, s := seatSetup(t)
205+ _, kb := peer.ids()
206+ surf := s.SurfaceObjectID()
207+ seat := c.Seat()
208+
209+ var keys []neferclient.KeyEvent
210+ var texts []string
211+ var focus []bool
212+ var changed []int
213+ h := neferclientmocks.NewMockHandler(t)
214+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
215+ keys = append(keys, *ev)
216+ texts = append(texts, string(ev.Text)) // the only place text is copied: the assertion of the test
217+ }).Return()
218+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
219+ h.EXPECT().SecretChanged(mock.Anything).Run(func(n int) { changed = append(changed, n) }).Return()
220+
221+ peer.keymap(kb, 1) // French AZERTY: evdev 16 is "a"
222+ srv.write(frame(kb, 5, append(appendU32(nil, 0), appendU32(nil, 0)...)))
223+ peer.keyboardEnter(kb, surf)
224+ srv.write(peer.key(kb, 16, 1))
225+ srv.write(peer.key(kb, 16, 0))
226+ require.NoError(t, c.Roundtrip())
227+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2 })
228+ require.Equal(t, []bool{true}, focus)
229+ require.Equal(t, "a", texts[0])
230+ require.True(t, keys[0].Pressed)
231+ require.Equal(t, s.ID(), keys[0].Surface)
232+ require.Equal(t, "", texts[1])
233+ require.False(t, keys[1].Pressed)
234+ require.Empty(t, changed)
235+
236+ // With a secret buffer: the text goes only into it.
237+ buf := neferclient.NewSecretBuffer(8)
238+ seat.SetSecret(buf)
239+ srv.write(peer.key(kb, 16, 1))
240+ srv.write(peer.key(kb, 16, 0))
241+ srv.write(peer.key(kb, 30, 1)) // "q" on AZERTY
242+ srv.write(peer.key(kb, 30, 0))
243+ srv.write(peer.key(kb, 14, 1)) // BackSpace
244+ srv.write(peer.key(kb, 14, 0))
245+ srv.write(peer.key(kb, 28, 1)) // Return: delivered, no text
246+ require.NoError(t, c.Roundtrip())
247+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2+7 })
248+ for _, text := range texts[2:] {
249+ require.Empty(t, text)
250+ }
251+ require.Equal(t, "a", string(buf.Bytes()))
252+ require.Equal(t, 1, buf.Len())
253+ require.Equal(t, []int{1, 2, 1}, changed)
254+
255+ srv.write(frame(kb, 2, appendU32(appendU32(nil, 5), surf))) // leave
256+ require.NoError(t, c.Roundtrip())
257+ dispatchUntil(t, c, h, func() bool { return len(focus) >= 2 })
258+ require.Equal(t, []bool{true, false}, focus)
259+ buf.Wipe()
260+}
261+
262+// TestSeatKeymapFDClosedOnClose: a keymap descriptor queued but never drained
263+// is closed by Close. The descriptor count after Close must not depend on
264+// whether a keymap was left in the queue.
265+func TestSeatKeymapFDClosedOnClose(t *testing.T) {
266+ run := func(withKeymap bool) (after int) {
267+ t.Run("run", func(t *testing.T) {
268+ c, _, peer, _ := seatSetup(t)
269+ _, kb := peer.ids()
270+ if withKeymap {
271+ before := openFDs(t)
272+ peer.keymap(kb, 1)
273+ require.NoError(t, c.Roundtrip()) // queued, not dispatched
274+ require.Greater(t, openFDs(t), before, "the queued event owns a descriptor")
275+ }
276+ require.NoError(t, c.Close())
277+ after = openFDs(t)
278+ })
279+ return after
280+ }
281+ without := run(false)
282+ require.Equal(t, without, run(true), "descriptor closed with the connection")
283+}
284+
285+// TestSeatKeymapFDClosedOnBadFormat: an unsupported keymap is closed, not leaked.
286+func TestSeatKeymapFDClosedOnBadFormat(t *testing.T) {
287+ c, _, peer, _ := seatSetup(t)
288+ _, kb := peer.ids()
289+ before := openFDs(t)
290+ peer.keymap(kb, 0) // XKB_V1 is 1; 0 is no_keymap
291+ require.NoError(t, c.Roundtrip())
292+ var errs int
293+ h := neferclientmocks.NewMockHandler(t)
294+ h.EXPECT().Error(mock.Anything).Run(func(error) { errs++ }).Return()
295+ dispatchUntil(t, c, h, func() bool { return errs == 1 })
296+ require.Equal(t, before, openFDs(t))
297+}
298+
299+func TestSeatCapabilityLossReleasesDevices(t *testing.T) {
300+ c, srv, peer, s := seatSetup(t)
301+ pointer, kb := peer.ids()
302+ b := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
303+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
304+ peer.keyboardEnter(kb, s.SurfaceObjectID())
305+ require.NoError(t, c.Roundtrip())
306+
307+ var got []neferclient.PointerEvent
308+ var focus []bool
309+ h := neferclientmocks.NewMockHandler(t)
310+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
311+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
312+ dispatchUntil(t, c, h, func() bool { return len(got) == 1 && len(focus) == 1 })
313+
314+ peer.capabilities(0)
315+ require.NoError(t, c.Roundtrip())
316+ dispatchUntil(t, c, h, func() bool { return len(got) == 2 && len(focus) == 2 })
317+ require.Equal(t, neferclient.PointerLeave, got[1].Kind, "a synthetic leave closes the focus")
318+ require.False(t, focus[1])
319+ require.NoError(t, c.Roundtrip())
320+ peer.mu.Lock()
321+ require.True(t, peer.pointerReleased)
322+ peer.mu.Unlock()
323+ _ = srv
324+}
325+
326+// TestSeatRepeatThroughEpoll: the repeat timerfd is watched by the connection's
327+// epoll loop and the repeat arrives through Dispatch.
328+func TestSeatRepeatThroughEpoll(t *testing.T) {
329+ c, srv, peer, s := seatSetup(t)
330+ _, kb := peer.ids()
331+ peer.keymap(kb, 1)
332+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 100), 1))) // 100/s after 1 ms
333+ peer.keyboardEnter(kb, s.SurfaceObjectID())
334+ srv.write(peer.key(kb, 16, 1))
335+ require.NoError(t, c.Roundtrip())
336+ var repeats int
337+ var texts []string
338+ h := neferclientmocks.NewMockHandler(t)
339+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return()
340+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
341+ if ev.Repeat {
342+ repeats++
343+ texts = append(texts, string(ev.Text))
344+ }
345+ }).Return()
346+ dispatchUntil(t, c, h, func() bool { return repeats >= 3 })
347+ require.Equal(t, "a", texts[0])
348+
349+ srv.write(peer.key(kb, 16, 0))
350+ require.NoError(t, c.Roundtrip())
351+ require.NoError(t, c.Dispatch(h))
352+ n := repeats
353+ for range 5 {
354+ runtime.Gosched()
355+ }
356+ require.NoError(t, c.Dispatch(h))
357+ require.Equal(t, n, repeats, "release stops the repeat")
358+}
359+
360+// xkbAllocsPerPress bounds the allocations of one key press. They all come
361+// from purego-xkbcommon: its functions are registered through purego's
362+// reflection-based RegisterFunc, which allocates the boxed return value (and
363+// more) on every native call, and a press makes four (key_get_one_sym,
364+// compose feed, compose status, key_get_utf8). The library itself adds none;
365+// the zero-allocation guard below proves that. The bound pins the binding's
366+// cost so a regression above it shows.
367+const xkbAllocsPerPress = 20
368+
369+// TestAllocKey measures the steady-state key path over a real socketpair, on
370+// the real code path (no mocks): key frames in, reader decode, queue, Dispatch
371+// with xkb state and translation, and the Handler call.
372+//
373+// Releases make no native call, so a batch of them measures the library's own
374+// cost, which must be zero. Presses add the purego-xkbcommon cost (see
375+// xkbAllocsPerPress); that is measured for ordinary text and for the secret
376+// buffer.
377+func TestAllocKey(t *testing.T) {
378+ skipUnderRace(t)
379+ c, srv, peer, s := seatSetup(t)
380+ _, kb := peer.ids()
381+ peer.keymap(kb, 1)
382+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 0), 0))) // repeat disabled
383+ peer.keyboardEnter(kb, s.SurfaceObjectID())
384+ require.NoError(t, c.Roundtrip())
385+ h := &countingHandler{}
386+ require.NoError(t, c.Dispatch(h))
387+ require.Zero(t, h.errs)
388+
389+ run := func(batch []byte, events int) func() {
390+ return func() {
391+ if _, err := srv.conn.Write(batch); err != nil {
392+ t.Fatal(err)
393+ }
394+ for c.QueueLen() < events {
395+ runtime.Gosched()
396+ }
397+ if err := c.Dispatch(h); err != nil {
398+ t.Fatal(err)
399+ }
400+ }
401+ }
402+ measure := func(name string, step func()) float64 {
403+ step()
404+ step()
405+ allocs := testing.AllocsPerRun(20, step)
406+ t.Logf("%s: allocs per Dispatch: %v", name, allocs)
407+ return allocs
408+ }
409+
410+ const pairs = 50
411+ var releases []byte // releases of keys that were never pressed: queue + decode + dispatch only
412+ for range 2 * pairs {
413+ releases = append(releases, peer.key(kb, 16, 0)...)
414+ }
415+ require.Zero(t, measure("pipeline, 100 releases", run(releases, 2*pairs)), "the library itself allocates nothing per key")
416+ require.Zero(t, h.keys, "unheld releases are not delivered")
417+
418+ var batch []byte
419+ for range pairs {
420+ batch = append(batch, peer.key(kb, 16, 1)...)
421+ batch = append(batch, peer.key(kb, 16, 0)...)
422+ }
423+ step := run(batch, 2*pairs)
424+ text := measure("text, 50 presses + releases", step)
425+ require.LessOrEqual(t, text, float64(pairs*xkbAllocsPerPress))
426+ require.Equal(t, "a", h.lastText)
427+
428+ buf := neferclient.NewSecretBuffer(4096)
429+ c.Seat().SetSecret(buf)
430+ secret := measure("secret, 50 presses + releases", step)
431+ require.LessOrEqual(t, secret, float64(pairs*xkbAllocsPerPress))
432+ require.Empty(t, h.lastText, "no text reaches the handler in secret mode")
433+ require.Positive(t, buf.Len())
434+ buf.Wipe()
435+ require.Zero(t, h.errs)
436+}
@@ -0,0 +1,436 @@
1+package neferclient_test
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "runtime"
7+ "sync"
8+ "testing"
9+
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+ "golang.org/x/sys/unix"
13+
14+ "github.com/bnema/neferclient"
15+ neferclientmocks "github.com/bnema/neferclient/mocks"
16+)
17+
18+const (
19+ seatGlobal = 8
20+ shapesGlobal = 9
21+)
22+
23+var seatGlobals = append(append([]wireGlobal{}, surfaceGlobals...),
24+ wireGlobal{seatGlobal, "wl_seat", 9}, wireGlobal{shapesGlobal, "wp_cursor_shape_manager_v1", 2})
25+
26+// seatPeer is the compositor side of a seat: it learns the object ids the
27+// client creates and scripts input events.
28+type seatPeer struct {
29+ t *testing.T
30+ srv *wireServer
31+
32+ mu sync.Mutex
33+ pointer, keyboard, shape uint32
34+ shapeSerial, shapeValue uint32
35+ shapeSets int
36+ pointerReleased bool
37+}
38+
39+func newSeatPeer(t *testing.T, srv *wireServer) *seatPeer {
40+ p := &seatPeer{t: t, srv: srv}
41+ srv.mu.Lock()
42+ srv.hook = func(obj uint32, op uint16, body []byte) {
43+ srv.mu.Lock()
44+ seat, shapes := srv.bound[seatGlobal].id, srv.bound[shapesGlobal].id
45+ srv.mu.Unlock()
46+ p.mu.Lock()
47+ defer p.mu.Unlock()
48+ switch {
49+ case obj == seat && seat != 0 && op == 0 && len(body) == 4:
50+ p.pointer = binary.LittleEndian.Uint32(body)
51+ case obj == seat && seat != 0 && op == 1 && len(body) == 4:
52+ p.keyboard = binary.LittleEndian.Uint32(body)
53+ case obj == shapes && shapes != 0 && op == 1 && len(body) == 8:
54+ p.shape = binary.LittleEndian.Uint32(body)
55+ case obj == p.shape && p.shape != 0 && op == 1 && len(body) == 8:
56+ p.shapeSerial = binary.LittleEndian.Uint32(body)
57+ p.shapeValue = binary.LittleEndian.Uint32(body[4:])
58+ p.shapeSets++
59+ case obj == p.pointer && p.pointer != 0 && op == 1:
60+ p.pointerReleased = true
61+ }
62+ }
63+ srv.mu.Unlock()
64+ return p
65+}
66+
67+func (p *seatPeer) ids() (pointer, keyboard uint32) {
68+ p.mu.Lock()
69+ defer p.mu.Unlock()
70+ return p.pointer, p.keyboard
71+}
72+
73+func (p *seatPeer) seatID() uint32 {
74+ p.srv.mu.Lock()
75+ defer p.srv.mu.Unlock()
76+ return p.srv.bound[seatGlobal].id
77+}
78+
79+func fixed(v float64) uint32 { return uint32(int32(v * 256)) }
80+
81+func negU32(v int32) uint32 { return uint32(-v) }
82+
83+func (p *seatPeer) capabilities(caps uint32) {
84+ p.srv.write(frame(p.seatID(), 0, appendU32(nil, caps)))
85+}
86+
87+// keymap sends wl_keyboard.keymap with testdata/fr.xkb in a descriptor.
88+func (p *seatPeer) keymap(keyboard uint32, format uint32) {
89+ p.t.Helper()
90+ data, err := os.ReadFile("testdata/fr.xkb")
91+ require.NoError(p.t, err)
92+ data = append(data, 0)
93+ fd, err := unix.MemfdCreate("keymap", unix.MFD_CLOEXEC)
94+ require.NoError(p.t, err)
95+ defer unix.Close(fd)
96+ _, err = unix.Write(fd, data)
97+ require.NoError(p.t, err)
98+ body := appendU32(nil, format)
99+ body = appendU32(body, uint32(len(data)))
100+ _, _, err = p.srv.conn.WriteMsgUnix(frame(keyboard, 0, body), unix.UnixRights(fd), nil)
101+ require.NoError(p.t, err)
102+}
103+
104+func (p *seatPeer) keyboardEnter(kb, surface uint32) {
105+ body := appendU32(appendU32(nil, 1), surface)
106+ body = appendU32(body, 0) // empty keys array
107+ p.srv.write(frame(kb, 1, body))
108+}
109+
110+func (p *seatPeer) key(kb, code, state uint32) []byte {
111+ b := appendU32(appendU32(nil, 1), 0)
112+ b = appendU32(appendU32(b, code), state)
113+ return frame(kb, 3, b)
114+}
115+
116+func seatSetup(t *testing.T) (*neferclient.Conn, *wireServer, *seatPeer, *neferclient.Surface) {
117+ t.Helper()
118+ c, srv := connectWire(t, seatGlobals, surfaceOutputs)
119+ peer := newSeatPeer(t, srv)
120+ s, err := c.NewToplevel("seat", 64, 48)
121+ require.NoError(t, err)
122+ require.NotNil(t, c.Seat())
123+ require.NoError(t, c.Roundtrip())
124+ peer.capabilities(3)
125+ require.NoError(t, c.Roundtrip())
126+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
127+ require.NoError(t, c.Roundtrip())
128+ return c, srv, peer, s
129+}
130+
131+func TestSeatPointerEvents(t *testing.T) {
132+ c, srv, peer, s := seatSetup(t)
133+ pointer, keyboard := peer.ids()
134+ require.NotZero(t, pointer)
135+ require.NotZero(t, keyboard)
136+ surf := s.SurfaceObjectID()
137+
138+ var got []neferclient.PointerEvent
139+ h := neferclientmocks.NewMockHandler(t)
140+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
141+
142+ // A motion outside any surface is ignored; the enter binds the position.
143+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(1)), appendU32(nil, fixed(2))...)))
144+ b := appendU32(appendU32(nil, 77), surf)
145+ b = appendU32(appendU32(b, fixed(10.5)), fixed(20.25))
146+ srv.write(frame(pointer, 0, b)) // enter
147+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(11)), appendU32(nil, fixed(21))...)))
148+ srv.write(frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 78), 0), 0x110), 1))) // button press
149+ srv.write(frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(15)))) // axis vertical
150+ srv.write(frame(pointer, 9, appendU32(appendU32(nil, 1), negU32(120)))) // value120 horizontal
151+ srv.write(frame(pointer, 7, appendU32(appendU32(nil, 0), 0))) // axis stop
152+ srv.write(frame(pointer, 1, appendU32(appendU32(nil, 79), surf))) // leave
153+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(3)), appendU32(nil, fixed(4))...)))
154+ require.NoError(t, c.Roundtrip())
155+ dispatchUntil(t, c, h, func() bool { return len(got) >= 7 })
156+
157+ require.Len(t, got, 7)
158+ id := s.ID()
159+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerEnter, X: 10.5, Y: 20.25}, got[0])
160+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerMotion, X: 11, Y: 21}, got[1])
161+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerButton, X: 11, Y: 21, Button: 0x110, Pressed: true}, got[2])
162+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 0, DY: 15}, got[3])
163+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 1, Value120: -120}, got[4])
164+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxisStop, X: 11, Y: 21}, got[5])
165+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerLeave, X: 11, Y: 21}, got[6])
166+}
167+
168+func TestSeatCursorShape(t *testing.T) {
169+ c, srv, peer, s := seatSetup(t)
170+ pointer, _ := peer.ids()
171+ seat := c.Seat()
172+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer), "outside: no-op")
173+ require.NoError(t, c.Roundtrip())
174+ require.Zero(t, peer.shapeSets)
175+
176+ enter := func(serial uint32) {
177+ b := appendU32(appendU32(nil, serial), s.SurfaceObjectID())
178+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
179+ require.NoError(t, c.Roundtrip())
180+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
181+ }
182+ enter(40)
183+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
184+ require.NoError(t, seat.SetCursor(neferclient.CursorText), "unchanged: nothing sent")
185+ require.NoError(t, c.Roundtrip())
186+ peer.mu.Lock()
187+ require.Equal(t, 1, peer.shapeSets)
188+ require.Equal(t, uint32(40), peer.shapeSerial)
189+ require.Equal(t, uint32(neferclient.CursorText), peer.shapeValue)
190+ peer.mu.Unlock()
191+
192+ enter(41) // a new enter resets the shape and carries a new serial
193+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
194+ require.NoError(t, c.Roundtrip())
195+ peer.mu.Lock()
196+ require.Equal(t, 2, peer.shapeSets)
197+ require.Equal(t, uint32(41), peer.shapeSerial)
198+ peer.mu.Unlock()
199+ require.Error(t, seat.SetCursor(0))
200+ require.Error(t, seat.SetCursor(neferclient.CursorAllResize+1))
201+}
202+
203+func TestSeatKeyboardFocusTextAndSecret(t *testing.T) {
204+ c, srv, peer, s := seatSetup(t)
205+ _, kb := peer.ids()
206+ surf := s.SurfaceObjectID()
207+ seat := c.Seat()
208+
209+ var keys []neferclient.KeyEvent
210+ var texts []string
211+ var focus []bool
212+ var changed []int
213+ h := neferclientmocks.NewMockHandler(t)
214+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
215+ keys = append(keys, *ev)
216+ texts = append(texts, string(ev.Text)) // the only place text is copied: the assertion of the test
217+ }).Return()
218+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
219+ h.EXPECT().SecretChanged(mock.Anything).Run(func(n int) { changed = append(changed, n) }).Return()
220+
221+ peer.keymap(kb, 1) // French AZERTY: evdev 16 is "a"
222+ srv.write(frame(kb, 5, append(appendU32(nil, 0), appendU32(nil, 0)...)))
223+ peer.keyboardEnter(kb, surf)
224+ srv.write(peer.key(kb, 16, 1))
225+ srv.write(peer.key(kb, 16, 0))
226+ require.NoError(t, c.Roundtrip())
227+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2 })
228+ require.Equal(t, []bool{true}, focus)
229+ require.Equal(t, "a", texts[0])
230+ require.True(t, keys[0].Pressed)
231+ require.Equal(t, s.ID(), keys[0].Surface)
232+ require.Equal(t, "", texts[1])
233+ require.False(t, keys[1].Pressed)
234+ require.Empty(t, changed)
235+
236+ // With a secret buffer: the text goes only into it.
237+ buf := neferclient.NewSecretBuffer(8)
238+ seat.SetSecret(buf)
239+ srv.write(peer.key(kb, 16, 1))
240+ srv.write(peer.key(kb, 16, 0))
241+ srv.write(peer.key(kb, 30, 1)) // "q" on AZERTY
242+ srv.write(peer.key(kb, 30, 0))
243+ srv.write(peer.key(kb, 14, 1)) // BackSpace
244+ srv.write(peer.key(kb, 14, 0))
245+ srv.write(peer.key(kb, 28, 1)) // Return: delivered, no text
246+ require.NoError(t, c.Roundtrip())
247+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2+7 })
248+ for _, text := range texts[2:] {
249+ require.Empty(t, text)
250+ }
251+ require.Equal(t, "a", string(buf.Bytes()))
252+ require.Equal(t, 1, buf.Len())
253+ require.Equal(t, []int{1, 2, 1}, changed)
254+
255+ srv.write(frame(kb, 2, appendU32(appendU32(nil, 5), surf))) // leave
256+ require.NoError(t, c.Roundtrip())
257+ dispatchUntil(t, c, h, func() bool { return len(focus) >= 2 })
258+ require.Equal(t, []bool{true, false}, focus)
259+ buf.Wipe()
260+}
261+
262+// TestSeatKeymapFDClosedOnClose: a keymap descriptor queued but never drained
263+// is closed by Close. The descriptor count after Close must not depend on
264+// whether a keymap was left in the queue.
265+func TestSeatKeymapFDClosedOnClose(t *testing.T) {
266+ run := func(withKeymap bool) (after int) {
267+ t.Run("run", func(t *testing.T) {
268+ c, _, peer, _ := seatSetup(t)
269+ _, kb := peer.ids()
270+ if withKeymap {
271+ before := openFDs(t)
272+ peer.keymap(kb, 1)
273+ require.NoError(t, c.Roundtrip()) // queued, not dispatched
274+ require.Greater(t, openFDs(t), before, "the queued event owns a descriptor")
275+ }
276+ require.NoError(t, c.Close())
277+ after = openFDs(t)
278+ })
279+ return after
280+ }
281+ without := run(false)
282+ require.Equal(t, without, run(true), "descriptor closed with the connection")
283+}
284+
285+// TestSeatKeymapFDClosedOnBadFormat: an unsupported keymap is closed, not leaked.
286+func TestSeatKeymapFDClosedOnBadFormat(t *testing.T) {
287+ c, _, peer, _ := seatSetup(t)
288+ _, kb := peer.ids()
289+ before := openFDs(t)
290+ peer.keymap(kb, 0) // XKB_V1 is 1; 0 is no_keymap
291+ require.NoError(t, c.Roundtrip())
292+ var errs int
293+ h := neferclientmocks.NewMockHandler(t)
294+ h.EXPECT().Error(mock.Anything).Run(func(error) { errs++ }).Return()
295+ dispatchUntil(t, c, h, func() bool { return errs == 1 })
296+ require.Equal(t, before, openFDs(t))
297+}
298+
299+func TestSeatCapabilityLossReleasesDevices(t *testing.T) {
300+ c, srv, peer, s := seatSetup(t)
301+ pointer, kb := peer.ids()
302+ b := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
303+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
304+ peer.keyboardEnter(kb, s.SurfaceObjectID())
305+ require.NoError(t, c.Roundtrip())
306+
307+ var got []neferclient.PointerEvent
308+ var focus []bool
309+ h := neferclientmocks.NewMockHandler(t)
310+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
311+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
312+ dispatchUntil(t, c, h, func() bool { return len(got) == 1 && len(focus) == 1 })
313+
314+ peer.capabilities(0)
315+ require.NoError(t, c.Roundtrip())
316+ dispatchUntil(t, c, h, func() bool { return len(got) == 2 && len(focus) == 2 })
317+ require.Equal(t, neferclient.PointerLeave, got[1].Kind, "a synthetic leave closes the focus")
318+ require.False(t, focus[1])
319+ require.NoError(t, c.Roundtrip())
320+ peer.mu.Lock()
321+ require.True(t, peer.pointerReleased)
322+ peer.mu.Unlock()
323+ _ = srv
324+}
325+
326+// TestSeatRepeatThroughEpoll: the repeat timerfd is watched by the connection's
327+// epoll loop and the repeat arrives through Dispatch.
328+func TestSeatRepeatThroughEpoll(t *testing.T) {
329+ c, srv, peer, s := seatSetup(t)
330+ _, kb := peer.ids()
331+ peer.keymap(kb, 1)
332+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 100), 1))) // 100/s after 1 ms
333+ peer.keyboardEnter(kb, s.SurfaceObjectID())
334+ srv.write(peer.key(kb, 16, 1))
335+ require.NoError(t, c.Roundtrip())
336+ var repeats int
337+ var texts []string
338+ h := neferclientmocks.NewMockHandler(t)
339+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return()
340+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
341+ if ev.Repeat {
342+ repeats++
343+ texts = append(texts, string(ev.Text))
344+ }
345+ }).Return()
346+ dispatchUntil(t, c, h, func() bool { return repeats >= 3 })
347+ require.Equal(t, "a", texts[0])
348+
349+ srv.write(peer.key(kb, 16, 0))
350+ require.NoError(t, c.Roundtrip())
351+ require.NoError(t, c.Dispatch(h))
352+ n := repeats
353+ for range 5 {
354+ runtime.Gosched()
355+ }
356+ require.NoError(t, c.Dispatch(h))
357+ require.Equal(t, n, repeats, "release stops the repeat")
358+}
359+
360+// xkbAllocsPerPress bounds the allocations of one key press. They all come
361+// from purego-xkbcommon: its functions are registered through purego's
362+// reflection-based RegisterFunc, which allocates the boxed return value (and
363+// more) on every native call, and a press makes four (key_get_one_sym,
364+// compose feed, compose status, key_get_utf8). The library itself adds none;
365+// the zero-allocation guard below proves that. The bound pins the binding's
366+// cost so a regression above it shows.
367+const xkbAllocsPerPress = 20
368+
369+// TestAllocKey measures the steady-state key path over a real socketpair, on
370+// the real code path (no mocks): key frames in, reader decode, queue, Dispatch
371+// with xkb state and translation, and the Handler call.
372+//
373+// Releases make no native call, so a batch of them measures the library's own
374+// cost, which must be zero. Presses add the purego-xkbcommon cost (see
375+// xkbAllocsPerPress); that is measured for ordinary text and for the secret
376+// buffer.
377+func TestAllocKey(t *testing.T) {
378+ skipUnderRace(t)
379+ c, srv, peer, s := seatSetup(t)
380+ _, kb := peer.ids()
381+ peer.keymap(kb, 1)
382+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 0), 0))) // repeat disabled
383+ peer.keyboardEnter(kb, s.SurfaceObjectID())
384+ require.NoError(t, c.Roundtrip())
385+ h := &countingHandler{}
386+ require.NoError(t, c.Dispatch(h))
387+ require.Zero(t, h.errs)
388+
389+ run := func(batch []byte, events int) func() {
390+ return func() {
391+ if _, err := srv.conn.Write(batch); err != nil {
392+ t.Fatal(err)
393+ }
394+ for c.QueueLen() < events {
395+ runtime.Gosched()
396+ }
397+ if err := c.Dispatch(h); err != nil {
398+ t.Fatal(err)
399+ }
400+ }
401+ }
402+ measure := func(name string, step func()) float64 {
403+ step()
404+ step()
405+ allocs := testing.AllocsPerRun(20, step)
406+ t.Logf("%s: allocs per Dispatch: %v", name, allocs)
407+ return allocs
408+ }
409+
410+ const pairs = 50
411+ var releases []byte // releases of keys that were never pressed: queue + decode + dispatch only
412+ for range 2 * pairs {
413+ releases = append(releases, peer.key(kb, 16, 0)...)
414+ }
415+ require.Zero(t, measure("pipeline, 100 releases", run(releases, 2*pairs)), "the library itself allocates nothing per key")
416+ require.Zero(t, h.keys, "unheld releases are not delivered")
417+
418+ var batch []byte
419+ for range pairs {
420+ batch = append(batch, peer.key(kb, 16, 1)...)
421+ batch = append(batch, peer.key(kb, 16, 0)...)
422+ }
423+ step := run(batch, 2*pairs)
424+ text := measure("text, 50 presses + releases", step)
425+ require.LessOrEqual(t, text, float64(pairs*xkbAllocsPerPress))
426+ require.Equal(t, "a", h.lastText)
427+
428+ buf := neferclient.NewSecretBuffer(4096)
429+ c.Seat().SetSecret(buf)
430+ secret := measure("secret, 50 presses + releases", step)
431+ require.LessOrEqual(t, secret, float64(pairs*xkbAllocsPerPress))
432+ require.Empty(t, h.lastText, "no text reaches the handler in secret mode")
433+ require.Positive(t, buf.Len())
434+ buf.Wipe()
435+ require.Zero(t, h.errs)
436+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 4

↑↓ moveenter openesc close