feat/data-dnd e05cb15

inputtimestamps_test.go RawHistoryFolder

1package wayland
2
3import (
4	"fmt"
5	"slices"
6	"sync"
7	"testing"
8	"time"
9
10	"github.com/bnema/go-wayland-bindings/server/inputtimestamps"
11	"github.com/bnema/go-wayland-bindings/server/wayland"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/wlturbo"
14)
15
16// stampRecorder is the ordered log shared by the proxies of one client, so a
17// test sees whether a timestamp comes right before its event.
18type stampRecorder struct {
19	mu    sync.Mutex
20	lines []string
21}
22
23func (r *stampRecorder) add(format string, args ...any) {
24	r.mu.Lock()
25	defer r.mu.Unlock()
26	r.lines = append(r.lines, fmt.Sprintf(format, args...))
27}
28
29func (r *stampRecorder) snapshot() []string {
30	r.mu.Lock()
31	defer r.mu.Unlock()
32	return slices.Clone(r.lines)
33}
34
35// wait round-trips until the log holds n lines.
36func (r *stampRecorder) wait(t *testing.T, c *wlturbo.Display, n int) []string {
37	t.Helper()
38	deadline := time.Now().Add(2 * time.Second)
39	for {
40		if err := c.Roundtrip(); err != nil {
41			t.Fatal(err)
42		}
43		if got := r.snapshot(); len(got) >= n {
44			return got
45		}
46		if time.Now().After(deadline) {
47			t.Fatalf("log has %v, want %d lines", r.snapshot(), n)
48		}
49		time.Sleep(5 * time.Millisecond)
50	}
51}
52
53// stampedKeyboard logs the key events of one wl_keyboard under its tag.
54type stampedKeyboard struct {
55	wlturbo.BaseProxy
56	tag string
57	log *stampRecorder
58}
59
60func (p *stampedKeyboard) Dispatch(e *wlturbo.Event) {
61	if e.Opcode == uint16(wayland.KeyboardEventKey) {
62		_ = e.Uint32() // serial
63		p.log.add("%s key %d", p.tag, e.Uint32())
64	}
65}
66
67// stampedPointer logs the motion, button and axis events of a wl_pointer.
68type stampedPointer struct {
69	wlturbo.BaseProxy
70	log *stampRecorder
71}
72
73func (p *stampedPointer) Dispatch(e *wlturbo.Event) {
74	switch e.Opcode {
75	case uint16(wayland.PointerEventMotion):
76		p.log.add("motion %d", e.Uint32())
77	case uint16(wayland.PointerEventButton):
78		_ = e.Uint32() // serial
79		p.log.add("button %d", e.Uint32())
80	case uint16(wayland.PointerEventAxis):
81		p.log.add("axis %d", e.Uint32())
82	case uint16(wayland.PointerEventAxisStop):
83		p.log.add("axis_stop %d", e.Uint32())
84	}
85}
86
87// stampProxy logs zwp_input_timestamps_v1.timestamp events.
88type stampProxy struct {
89	wlturbo.BaseProxy
90	log *stampRecorder
91}
92
93func (p *stampProxy) Dispatch(e *wlturbo.Event) {
94	if e.Opcode == uint16(inputtimestamps.ZwpInputTimestampsV1EventTimestamp) {
95		hi, lo, ns := e.Uint32(), e.Uint32(), e.Uint32()
96		p.log.add("ts %d %d %d", hi, lo, ns)
97	}
98}
99
100func stampSubscription(t *testing.T, c *wlturbo.Display, log *stampRecorder, manager uint32, request uint32, device uint32) uint32 {
101	t.Helper()
102	id := c.AllocateID()
103	p := &stampProxy{log: log}
104	p.SetID(id)
105	registerWireProxy(c, p)
106	requestProtocol(t, c, manager, request, id, device)
107	return id
108}
109
110func TestInputTimestampsGlobal(t *testing.T) {
111	s, _, _, dir := keyboardServer(t)
112	c := protocolClient(t, s, dir)
113	g, ok := c.Registry().FindGlobal("zwp_input_timestamps_manager_v1")
114	if !ok || g.Version != 1 {
115		t.Fatalf("zwp_input_timestamps_manager_v1: %+v, %v", g, ok)
116	}
117}
118
119func TestInputTimestampsKeyboard(t *testing.T) {
120	s, events, commands, dir := keyboardServer(t)
121	c := protocolClient(t, s, dir)
122	log := &stampRecorder{}
123	seat := bindProtocol(t, c, "wl_seat")
124	registerProtocol(t, c, seat)
125	var keyboards [2]uint32
126	for i, tag := range []string{"subscribed", "plain"} {
127		keyboards[i] = c.AllocateID()
128		kp := &stampedKeyboard{tag: tag, log: log}
129		kp.SetID(keyboards[i])
130		registerWireProxy(c, kp)
131		requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboards[i])
132	}
133	manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
134	ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetKeyboardTimestamps, keyboards[0])
135	w := toplevelMapper(t, c, events)()
136	commands <- ports.FocusWindow{ID: w.ID}
137
138	commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 5*time.Second + 123456789}}
139	got := log.wait(t, c, 3)
140	want := []string{"ts 0 5 123456789", "subscribed key 5123", "plain key 5123"}
141	if !slices.Equal(got, want) {
142		t.Fatalf("log %v, want %v", got, want)
143	}
144
145	// Seconds past 32 bits go to sec_hi.
146	big := time.Duration(1<<32+7) * time.Second
147	commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Time: big}}
148	ms := uint64(big / time.Millisecond)
149	got = log.wait(t, c, 6)
150	want = append(want, "ts 1 7 0", fmt.Sprint("subscribed key ", uint32(ms)), fmt.Sprint("plain key ", uint32(ms)))
151	if !slices.Equal(got, want) {
152		t.Fatalf("log %v, want %v", got, want)
153	}
154
155	// A destroyed timestamps object sends nothing more.
156	requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
157	if err := c.Roundtrip(); err != nil {
158		t.Fatal(err)
159	}
160	commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 31, Pressed: true, Time: 9 * time.Second}}
161	got = log.wait(t, c, 8)
162	want = append(want, "subscribed key 9000", "plain key 9000")
163	if !slices.Equal(got, want) {
164		t.Fatalf("log %v, want %v", got, want)
165	}
166}
167
168func TestInputTimestampsPointer(t *testing.T) {
169	s, events, commands, dir := keyboardServer(t)
170	c := protocolClient(t, s, dir)
171	log := &stampRecorder{}
172	seat := bindVersion(t, c, "wl_seat", 8)
173	registerProtocol(t, c, seat)
174	pointer := c.AllocateID()
175	pp := &stampedPointer{log: log}
176	pp.SetID(pointer)
177	registerWireProxy(c, pp)
178	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
179	manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
180	ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
181	w := toplevelMapper(t, c, events)()
182	commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
183
184	commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0, Time: 2*time.Second + time.Millisecond}
185	commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 3 * time.Second}
186	commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: 5}, Time: 4 * time.Second}}
187	commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 5 * time.Second}}
188	got := log.wait(t, c, 8)
189	want := []string{
190		"ts 0 2 1000000", "motion 2001",
191		"ts 0 3 0", "button 3000",
192		"ts 0 4 0", "axis 4000",
193		"ts 0 5 0", "axis_stop 5000",
194	}
195	if !slices.Equal(got, want) {
196		t.Fatalf("log %v, want %v", got, want)
197	}
198
199	// The stamped input path allocates nothing, with a live subscriber.
200	var allocs float64
201	s.display.Do(func() {
202		allocs = testing.AllocsPerRun(100, func() {
203			s.applyInput(ports.PointerMotionTo{ID: w.ID, Time: 6 * time.Second})
204		})
205	})
206	if allocs != 0 {
207		t.Fatalf("stamped motion allocates %v", allocs)
208	}
209
210	// Releasing the pointer makes the object inert and drops its entry;
211	// destroying it afterwards is still fine.
212	requestProtocol(t, c, pointer, wayland.PointerRequestRelease)
213	if err := c.Roundtrip(); err != nil {
214		t.Fatal(err)
215	}
216	if n := stampEntries(s); n != 0 {
217		t.Fatalf("%d stamp entries after the pointer died", n)
218	}
219	requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
220	if err := c.Roundtrip(); err != nil {
221		t.Fatal(err)
222	}
223}
224
225// Destroying the timestamps object before its device drops the entry too.
226func TestInputTimestampsDestroyBeforeDevice(t *testing.T) {
227	s, _, _, dir := keyboardServer(t)
228	c := protocolClient(t, s, dir)
229	log := &stampRecorder{}
230	seat := bindProtocol(t, c, "wl_seat")
231	registerProtocol(t, c, seat)
232	pointer := c.AllocateID()
233	registerProtocol(t, c, pointer)
234	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
235	manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
236	ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
237	if err := c.Roundtrip(); err != nil {
238		t.Fatal(err)
239	}
240	if n := stampEntries(s); n != 1 {
241		t.Fatalf("%d stamp entries, want 1", n)
242	}
243	requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
244	if err := c.Roundtrip(); err != nil {
245		t.Fatal(err)
246	}
247	if n := stampEntries(s); n != 0 {
248		t.Fatalf("%d stamp entries after destroy", n)
249	}
250}
251
252// stampEntries counts the devices with subscriptions, on the display goroutine.
253func stampEntries(s *Server) int {
254	var n int
255	s.display.Do(func() { n = len(s.seat.keyStamps) + len(s.seat.pointerStamps) })
256	return n
257}

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 23

Tags 10

↑↓ moveenter openesc close