v0.4.0 c0db34c

client_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"encoding/binary"
6	"io"
7	"net"
8	"os"
9	"slices"
10	"testing"
11	"time"
12
13	"github.com/bnema/neferafk/internal/ports"
14	"github.com/bnema/zerowrap"
15	"golang.org/x/sys/unix"
16)
17
18func sockets(t *testing.T) (net.Conn, *net.UnixConn) {
19	t.Helper()
20	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
21	if err != nil {
22		t.Fatal(err)
23	}
24	files := []*os.File{os.NewFile(uintptr(fds[0]), "client"), os.NewFile(uintptr(fds[1]), "peer")}
25	a, err := net.FileConn(files[0])
26	files[0].Close()
27	if err != nil {
28		t.Fatal(err)
29	}
30	b, err := net.FileConn(files[1])
31	files[1].Close()
32	if err != nil {
33		t.Fatal(err)
34	}
35	t.Cleanup(func() { a.Close(); b.Close() })
36	return a, b.(*net.UnixConn)
37}
38func packet(id uint32, op uint16, body []byte) []byte {
39	b := make([]byte, 8+len(body))
40	binary.NativeEndian.PutUint32(b, id)
41	binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
42	copy(b[8:], body)
43	return b
44}
45func words(v ...uint32) []byte {
46	b := make([]byte, 4*len(v))
47	for i, n := range v {
48		binary.NativeEndian.PutUint32(b[i*4:], n)
49	}
50	return b
51}
52func wireString(s string) []byte {
53	b := make([]byte, 4+(len(s)+4)&^3)
54	binary.NativeEndian.PutUint32(b, uint32(len(s)+1))
55	copy(b[4:], s)
56	return b
57}
58func readWire(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
59	t.Helper()
60	p.SetReadDeadline(time.Now().Add(2 * time.Second))
61	h := make([]byte, 8)
62	if _, err := io.ReadFull(p, h); err != nil {
63		t.Fatal(err)
64	}
65	size := binary.NativeEndian.Uint32(h[4:]) >> 16
66	b := make([]byte, size-8)
67	if _, err := io.ReadFull(p, b); err != nil {
68		t.Fatal(err)
69	}
70	return binary.NativeEndian.Uint32(h), uint16(binary.NativeEndian.Uint32(h[4:])), b
71}
72func sendWire(t *testing.T, p *net.UnixConn, id uint32, op uint16, b []byte) {
73	t.Helper()
74	if _, err := p.Write(packet(id, op, b)); err != nil {
75		t.Fatal(err)
76	}
77}
78func global(t *testing.T, p *net.UnixConn, registry, name uint32, iface string, v uint32) {
79	b := append(words(name), wireString(iface)...)
80	b = append(b, words(v)...)
81	sendWire(t, p, registry, 0, b)
82}
83func synchronize(t *testing.T, p *net.UnixConn, body []byte) {
84	id := binary.NativeEndian.Uint32(body)
85	sendWire(t, p, id, 0, words(1))
86	sendWire(t, p, 1, 1, words(id))
87}
88func capability(t *testing.T, out <-chan ports.WaylandEvent) ports.Capabilities {
89	t.Helper()
90	for {
91		select {
92		case e := <-out:
93			if c, ok := e.(ports.WaylandCapabilities); ok {
94				return c.Capabilities
95			}
96		case <-time.After(2 * time.Second):
97			t.Fatal("capability timeout")
98		}
99	}
100}
101func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent {
102	t.Helper()
103	select {
104	case e := <-out:
105		return e
106	case <-time.After(2 * time.Second):
107		t.Fatal("event timeout")
108		return nil
109	}
110}
111
112func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
113	conn, peer := sockets(t)
114	c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
115	if err != nil {
116		t.Fatal(err)
117	}
118	_, _, body := readWire(t, peer)
119	registry := binary.NativeEndian.Uint32(body)
120	ctx, cancel := context.WithCancel(context.Background())
121	defer cancel()
122	commands := make(chan ports.WaylandCommand, 8)
123	out := make(chan ports.WaylandEvent, 64)
124	done := make(chan error, 1)
125	go func() { done <- c.Run(ctx, commands, out) }()
126	_, _, body = readWire(t, peer)
127	global(t, peer, registry, 10, "wl_seat", 5)
128	global(t, peer, registry, 11, "wl_output", 4)
129	global(t, peer, registry, 12, "ext_idle_notifier_v1", 2)
130	global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1)
131	global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1)
132	synchronize(t, peer, body)
133	ids := map[string]uint32{}
134	notifications := map[uint32]uint32{}
135	var wake, power uint32
136	for {
137		id, op, b := readWire(t, peer)
138		if id == 1 {
139			synchronize(t, peer, b)
140			break
141		}
142		if id == registry {
143			n := int(binary.NativeEndian.Uint32(b[4:]))
144			iface := string(b[8 : 8+n-1])
145			ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
146			if iface == "wl_output" {
147				sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1"))
148			}
149			continue
150		}
151		if id == ids["ext_idle_notifier_v1"] {
152			child := binary.NativeEndian.Uint32(b)
153			ms := binary.NativeEndian.Uint32(b[4:])
154			if op == 2 {
155				wake = child
156			} else {
157				notifications[ms] = child
158			}
159		}
160		if id == ids["zwlr_output_power_manager_v1"] {
161			power = binary.NativeEndian.Uint32(b)
162			sendWire(t, peer, power, 0, words(1))
163		}
164	}
165	caps := capability(t, out)
166	if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {
167		t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)
168	}
169	if !slices.Contains(caps.Missing, "zwp_linux_dmabuf_v1 >= 4") || slices.Contains(caps.Missing, "ext_session_lock_manager_v1 >= 1") {
170		t.Fatal(caps.Missing)
171	}
172	lifetime := caps.Outputs[0].Lifetime
173	sendWire(t, peer, notifications[1000], 0, nil)
174	idle, ok := nextEvent(t, out).(ports.WaylandIdle)
175	if !ok || idle.Generation != 1 || idle.After != time.Second || idle.Cycle != 1 {
176		t.Fatal(idle, ok)
177	}
178	// Reload retains the one-second proxy and removes only the two-second one.
179	commands <- ports.SetDeadlines{Generation: 2, Deadlines: []time.Duration{time.Second, 3 * time.Second}}
180	var added uint32
181	for i := 0; i < 2; i++ {
182		id, op, b := readWire(t, peer)
183		if id == notifications[2000] && op == 0 {
184			continue
185		}
186		if id == ids["ext_idle_notifier_v1"] && op == 1 && binary.NativeEndian.Uint32(b[4:]) == 3000 {
187			added = binary.NativeEndian.Uint32(b)
188			continue
189		}
190		t.Fatalf("reload request %d/%d %x", id, op, b)
191	}
192	if added == 0 {
193		t.Fatal("new deadline missing")
194	}
195	capability(t, out)
196	// An in-flight event for the deleted notification must never become policy.
197	sendWire(t, peer, notifications[2000], 0, nil)
198	sendWire(t, peer, notifications[1000], 1, nil)
199	sendWire(t, peer, notifications[1000], 0, nil)
200	idle, ok = nextEvent(t, out).(ports.WaylandIdle)
201	if !ok || idle.Generation != 2 || idle.Proxy == 0 || idle.Cycle != 2 {
202		t.Fatal(idle, ok)
203	}
204	commands <- ports.OutputPower{On: false}
205	id, op, b := readWire(t, peer)
206	if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
207		t.Fatalf("off %d/%d %x", id, op, b)
208	}
209	sendWire(t, peer, wake, 0, nil)
210	sendWire(t, peer, wake, 1, nil)
211	id, op, b = readWire(t, peer)
212	if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 1 {
213		t.Fatalf("wake %d/%d %x", id, op, b)
214	}
215	if _, ok := nextEvent(t, out).(ports.WaylandActivity); !ok {
216		t.Fatal("missing activity")
217	}
218	// Removing/re-adding the same global number creates a new output lifetime.
219	sendWire(t, peer, registry, 1, words(11))
220	for i := 0; i < 2; i++ {
221		readWire(t, peer)
222	}
223	caps = capability(t, out)
224	if len(caps.Outputs) != 0 {
225		t.Fatal(caps)
226	}
227	global(t, peer, registry, 11, "wl_output", 4)
228	var newPower uint32
229	for i := 0; i < 2; i++ {
230		id, _, b = readWire(t, peer)
231		if id == registry {
232			ids["new-output"] = binary.NativeEndian.Uint32(b[len(b)-4:])
233		} else {
234			newPower = binary.NativeEndian.Uint32(b)
235		}
236	}
237	caps = capability(t, out)
238	if len(caps.Outputs) != 1 || caps.Outputs[0].Lifetime == lifetime {
239		t.Fatal(caps)
240	}
241	// A late failed event from the previous output cannot disable its successor.
242	sendWire(t, peer, power, 1, nil)
243	sendWire(t, peer, newPower, 1, nil)
244	id, op, _ = readWire(t, peer)
245	if id != newPower || op != 1 {
246		t.Fatal("failed control not destroyed", id, op)
247	}
248	if failure, ok := nextEvent(t, out).(ports.WaylandPowerFailed); !ok || failure.Output != caps.Outputs[0].Lifetime {
249		t.Fatal(failure, ok)
250	}
251	caps = capability(t, out)
252	if caps.Outputs[0].PowerSupported || len(caps.Missing) != 4 {
253		t.Fatal(caps)
254	}
255	cancel()
256	select {
257	case err := <-done:
258		if err != nil {
259			t.Fatal(err)
260		}
261	case <-time.After(2 * time.Second):
262		t.Fatal("Run failed to join reader")
263	}
264}
265
266func TestMissingAndDisabledCapabilities(t *testing.T) {
267	conn, peer := sockets(t)
268	c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
269	if err != nil {
270		t.Fatal(err)
271	}
272	_, _, b := readWire(t, peer)
273	registry := binary.NativeEndian.Uint32(b)
274	ctx, cancel := context.WithCancel(context.Background())
275	defer cancel()
276	out := make(chan ports.WaylandEvent, 8)
277	done := make(chan error, 1)
278	go func() { done <- c.Run(ctx, nil, out) }()
279	_, _, b = readWire(t, peer)
280	global(t, peer, registry, 10, "ext_idle_notifier_v1", 1)
281	global(t, peer, registry, 11, "zwlr_output_power_manager_v1", 1)
282	synchronize(t, peer, b)
283	id, _, b := readWire(t, peer)
284	if id != 1 {
285		t.Fatalf("disabled optional global bound: %d", id)
286	}
287	synchronize(t, peer, b)
288	caps := capability(t, out)
289	if len(caps.Missing) != 5 || slices.Contains(caps.Missing, "ext_idle_notifier_v1 >= 2") {
290		t.Fatal(caps)
291	}
292	cancel()
293	if err := <-done; err != nil {
294		t.Fatal(err)
295	}
296}
297
298func TestDeadlineValidation(t *testing.T) {
299	for _, d := range []time.Duration{-time.Second, time.Nanosecond, time.Duration(1<<32) * time.Millisecond} {
300		if _, err := normalize([]time.Duration{d}); err == nil {
301			t.Fatal(d)
302		}
303	}
304	ds, err := normalize([]time.Duration{0, time.Second, time.Second})
305	if err != nil || len(ds) != 1 {
306		t.Fatal(ds, err)
307	}
308}
309
310func TestCancelBlockedStartup(t *testing.T) {
311	conn, peer := sockets(t)
312	c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
313	if err != nil {
314		t.Fatal(err)
315	}
316	readWire(t, peer)
317	ctx, cancel := context.WithCancel(context.Background())
318	done := make(chan error, 1)
319	go func() { done <- c.Run(ctx, nil, nil) }()
320	readWire(t, peer) // sync request deliberately unanswered
321	cancel()
322	select {
323	case err := <-done:
324		if err != nil {
325			t.Fatal(err)
326		}
327	case <-time.After(time.Second):
328		t.Fatal("cancel did not close socket")
329	}
330}
331
332func TestBoundedCallbackQueueFailsClosed(t *testing.T) {
333	conn, peer := sockets(t)
334	c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
335	if err != nil {
336		t.Fatal(err)
337	}
338	readWire(t, peer)
339	for i := 0; i <= queueSize; i++ {
340		c.enqueue(notice{kind: 1})
341	}
342	select {
343	case <-c.overflow:
344	default:
345		t.Fatal("overflow not reported")
346	}
347}

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 2

Tags 9

↑↓ moveenter openesc close