v0.5.1 31f302d

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
112// started is a client running against a fake compositor that announced
113// its globals and answered the initial bindings (one output, HEADLESS-1).
114type started struct {
115	peer                  *net.UnixConn
116	registry, wake, power uint32
117	ids                   map[string]uint32 // bound ID by interface
118	notifications         map[uint32]uint32 // idle notification by timeout (ms)
119	commands              chan ports.WaylandCommand
120	out                   chan ports.WaylandEvent
121	done                  chan error
122}
123
124// extraGlobal is one more global announced at start.
125type extraGlobal struct {
126	name    uint32
127	iface   string
128	version uint32
129}
130
131// start runs a client and the initial discovery; extra globals follow
132// wl_seat, wl_output, ext_idle_notifier_v1 and zwlr_output_power_manager_v1.
133func start(t *testing.T, ctx context.Context, options Options, extra ...extraGlobal) *started {
134	t.Helper()
135	conn, peer := sockets(t)
136	c, err := New(conn, options, zerowrap.New(zerowrap.Config{Output: io.Discard}))
137	if err != nil {
138		t.Fatal(err)
139	}
140	_, _, body := readWire(t, peer)
141	s := &started{peer: peer, registry: binary.NativeEndian.Uint32(body), ids: map[string]uint32{}, notifications: map[uint32]uint32{}, commands: make(chan ports.WaylandCommand, 8), out: make(chan ports.WaylandEvent, 64), done: make(chan error, 1)}
142	go func() { s.done <- c.Run(ctx, s.commands, s.out) }()
143	_, _, body = readWire(t, peer)
144	global(t, peer, s.registry, 10, "wl_seat", 5)
145	global(t, peer, s.registry, 11, "wl_output", 4)
146	global(t, peer, s.registry, 12, "ext_idle_notifier_v1", 2)
147	global(t, peer, s.registry, 13, "zwlr_output_power_manager_v1", 1)
148	for _, g := range extra {
149		global(t, peer, s.registry, g.name, g.iface, g.version)
150	}
151	synchronize(t, peer, body)
152	for {
153		id, op, b := readWire(t, peer)
154		if id == 1 {
155			synchronize(t, peer, b)
156			break
157		}
158		if id == s.registry {
159			n := int(binary.NativeEndian.Uint32(b[4:]))
160			iface := string(b[8 : 8+n-1])
161			s.ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
162			if iface == "wl_output" {
163				sendWire(t, peer, s.ids[iface], 4, wireString("HEADLESS-1"))
164			}
165			continue
166		}
167		if id == s.ids["ext_idle_notifier_v1"] {
168			child := binary.NativeEndian.Uint32(b)
169			if op == 2 {
170				s.wake = child
171			} else {
172				s.notifications[binary.NativeEndian.Uint32(b[4:])] = child
173			}
174		}
175		if id == s.ids["zwlr_output_power_manager_v1"] {
176			s.power = binary.NativeEndian.Uint32(b)
177			sendWire(t, peer, s.power, 0, words(1))
178		}
179	}
180	return s
181}
182
183func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
184	ctx, cancel := context.WithCancel(context.Background())
185	defer cancel()
186	s := start(t, ctx, 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}}, extraGlobal{14, "ext_session_lock_manager_v1", 1})
187	peer, registry, ids, notifications, wake, power := s.peer, s.registry, s.ids, s.notifications, s.wake, s.power
188	commands, out, done := s.commands, s.out, s.done
189	caps := capability(t, out)
190	if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {
191		t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)
192	}
193	if !slices.Contains(caps.Missing, "zwp_linux_dmabuf_v1 >= 4") || slices.Contains(caps.Missing, "ext_session_lock_manager_v1 >= 1") {
194		t.Fatal(caps.Missing)
195	}
196	lifetime := caps.Outputs[0].Lifetime
197	sendWire(t, peer, notifications[1000], 0, nil)
198	idle, ok := nextEvent(t, out).(ports.WaylandIdle)
199	if !ok || idle.Generation != 1 || idle.After != time.Second || idle.Cycle != 1 {
200		t.Fatal(idle, ok)
201	}
202	// Reload retains the one-second proxy and removes only the two-second one.
203	commands <- ports.SetDeadlines{Generation: 2, Deadlines: []time.Duration{time.Second, 3 * time.Second}}
204	var added uint32
205	for i := 0; i < 2; i++ {
206		id, op, b := readWire(t, peer)
207		if id == notifications[2000] && op == 0 {
208			continue
209		}
210		if id == ids["ext_idle_notifier_v1"] && op == 1 && binary.NativeEndian.Uint32(b[4:]) == 3000 {
211			added = binary.NativeEndian.Uint32(b)
212			continue
213		}
214		t.Fatalf("reload request %d/%d %x", id, op, b)
215	}
216	if added == 0 {
217		t.Fatal("new deadline missing")
218	}
219	capability(t, out)
220	// An in-flight event for the deleted notification must never become policy.
221	sendWire(t, peer, notifications[2000], 0, nil)
222	sendWire(t, peer, notifications[1000], 1, nil)
223	sendWire(t, peer, notifications[1000], 0, nil)
224	idle, ok = nextEvent(t, out).(ports.WaylandIdle)
225	if !ok || idle.Generation != 2 || idle.Proxy == 0 || idle.Cycle != 2 {
226		t.Fatal(idle, ok)
227	}
228	commands <- ports.OutputPower{On: false}
229	id, op, b := readWire(t, peer)
230	if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
231		t.Fatalf("off %d/%d %x", id, op, b)
232	}
233	sendWire(t, peer, wake, 0, nil)
234	sendWire(t, peer, wake, 1, nil)
235	id, op, b = readWire(t, peer)
236	if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 1 {
237		t.Fatalf("wake %d/%d %x", id, op, b)
238	}
239	if _, ok := nextEvent(t, out).(ports.WaylandActivity); !ok {
240		t.Fatal("missing activity")
241	}
242	// Removing/re-adding the same global number creates a new output lifetime.
243	sendWire(t, peer, registry, 1, words(11))
244	for i := 0; i < 2; i++ {
245		readWire(t, peer)
246	}
247	caps = capability(t, out)
248	if len(caps.Outputs) != 0 {
249		t.Fatal(caps)
250	}
251	global(t, peer, registry, 11, "wl_output", 4)
252	var newPower uint32
253	for i := 0; i < 2; i++ {
254		id, _, b = readWire(t, peer)
255		if id == registry {
256			ids["new-output"] = binary.NativeEndian.Uint32(b[len(b)-4:])
257		} else {
258			newPower = binary.NativeEndian.Uint32(b)
259		}
260	}
261	caps = capability(t, out)
262	if len(caps.Outputs) != 1 || caps.Outputs[0].Lifetime == lifetime {
263		t.Fatal(caps)
264	}
265	// A late failed event from the previous output cannot disable its successor.
266	sendWire(t, peer, power, 1, nil)
267	sendWire(t, peer, newPower, 1, nil)
268	id, op, _ = readWire(t, peer)
269	if id != newPower || op != 1 {
270		t.Fatal("failed control not destroyed", id, op)
271	}
272	if failure, ok := nextEvent(t, out).(ports.WaylandPowerFailed); !ok || failure.Output != caps.Outputs[0].Lifetime {
273		t.Fatal(failure, ok)
274	}
275	caps = capability(t, out)
276	if caps.Outputs[0].PowerSupported || len(caps.Missing) != 4 {
277		t.Fatal(caps)
278	}
279	cancel()
280	select {
281	case err := <-done:
282		if err != nil {
283			t.Fatal(err)
284		}
285	case <-time.After(2 * time.Second):
286		t.Fatal("Run failed to join reader")
287	}
288}
289
290func TestOutputAddedWhileOffIsTurnedOffAndWakesWithTheOthers(t *testing.T) {
291	ctx, cancel := context.WithCancel(context.Background())
292	defer cancel()
293	s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true}, Generation: 1, Deadlines: []time.Duration{time.Second}})
294	peer, registry, wake, power := s.peer, s.registry, s.wake, s.power
295	commands, out, done := s.commands, s.out, s.done
296	manager := s.ids["zwlr_output_power_manager_v1"]
297	capability(t, out)
298	if manager == 0 || wake == 0 || power == 0 {
299		t.Fatal(manager, wake, power)
300	}
301	commands <- ports.OutputPower{On: false}
302	if id, op, b := readWire(t, peer); id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
303		t.Fatalf("off %d/%d %x", id, op, b)
304	}
305	// addOutput announces an output and answers its power control's initial
306	// mode; it returns that control.
307	addOutput := func(name, mode uint32) uint32 {
308		global(t, peer, registry, name, "wl_output", 4)
309		var p uint32
310		for i := 0; i < 2; i++ {
311			if id, _, b := readWire(t, peer); id == manager {
312				p = binary.NativeEndian.Uint32(b)
313			}
314		}
315		sendWire(t, peer, p, 0, words(mode))
316		return p
317	}
318	// A display reconnecting from deep sleep comes back on: turned off.
319	on := addOutput(14, 1)
320	if id, op, b := readWire(t, peer); id != on || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
321		t.Fatalf("new output off %d/%d %x", id, op, b)
322	}
323	// One that comes back off needs no request, but is woken with the rest.
324	off := addOutput(15, 0)
325	sendWire(t, peer, wake, 0, nil)
326	sendWire(t, peer, wake, 1, nil)
327	woken := map[uint32]bool{}
328	for range 3 {
329		id, op, b := readWire(t, peer)
330		if op != 0 || binary.NativeEndian.Uint32(b) != 1 {
331			t.Fatalf("wake %d/%d %x", id, op, b)
332		}
333		woken[id] = true
334	}
335	if !woken[power] || !woken[on] || !woken[off] {
336		t.Fatal(woken)
337	}
338	cancel()
339	if err := <-done; err != nil {
340		t.Fatal(err)
341	}
342}
343
344func TestMissingAndDisabledCapabilities(t *testing.T) {
345	conn, peer := sockets(t)
346	c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
347	if err != nil {
348		t.Fatal(err)
349	}
350	_, _, b := readWire(t, peer)
351	registry := binary.NativeEndian.Uint32(b)
352	ctx, cancel := context.WithCancel(context.Background())
353	defer cancel()
354	out := make(chan ports.WaylandEvent, 8)
355	done := make(chan error, 1)
356	go func() { done <- c.Run(ctx, nil, out) }()
357	_, _, b = readWire(t, peer)
358	global(t, peer, registry, 10, "ext_idle_notifier_v1", 1)
359	global(t, peer, registry, 11, "zwlr_output_power_manager_v1", 1)
360	synchronize(t, peer, b)
361	id, _, b := readWire(t, peer)
362	if id != 1 {
363		t.Fatalf("disabled optional global bound: %d", id)
364	}
365	synchronize(t, peer, b)
366	caps := capability(t, out)
367	if len(caps.Missing) != 5 || slices.Contains(caps.Missing, "ext_idle_notifier_v1 >= 2") {
368		t.Fatal(caps)
369	}
370	cancel()
371	if err := <-done; err != nil {
372		t.Fatal(err)
373	}
374}
375
376func TestDeadlineValidation(t *testing.T) {
377	for _, d := range []time.Duration{-time.Second, time.Nanosecond, time.Duration(1<<32) * time.Millisecond} {
378		if _, err := normalize([]time.Duration{d}); err == nil {
379			t.Fatal(d)
380		}
381	}
382	ds, err := normalize([]time.Duration{0, time.Second, time.Second})
383	if err != nil || len(ds) != 1 {
384		t.Fatal(ds, err)
385	}
386}
387
388func TestCancelBlockedStartup(t *testing.T) {
389	conn, peer := sockets(t)
390	c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
391	if err != nil {
392		t.Fatal(err)
393	}
394	readWire(t, peer)
395	ctx, cancel := context.WithCancel(context.Background())
396	done := make(chan error, 1)
397	go func() { done <- c.Run(ctx, nil, nil) }()
398	readWire(t, peer) // sync request deliberately unanswered
399	cancel()
400	select {
401	case err := <-done:
402		if err != nil {
403			t.Fatal(err)
404		}
405	case <-time.After(time.Second):
406		t.Fatal("cancel did not close socket")
407	}
408}
409
410func TestBoundedCallbackQueueFailsClosed(t *testing.T) {
411	conn, peer := sockets(t)
412	c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
413	if err != nil {
414		t.Fatal(err)
415	}
416	readWire(t, peer)
417	for i := 0; i <= queueSize; i++ {
418		c.enqueue(notice{kind: 1})
419	}
420	select {
421	case <-c.overflow:
422	default:
423		t.Fatal("overflow not reported")
424	}
425}

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