v0.8.0 5f793eb

client_test.go RawHistoryFolder

1package server
2
3import (
4	"context"
5	"encoding/binary"
6	"errors"
7	"sync"
8	"testing"
9	"time"
10
11	"golang.org/x/sys/unix"
12)
13
14// clientDisplay runs a real display and returns it with a stop function that
15// waits for Run to return (and so for display destruction).
16func clientDisplay(t *testing.T) (*Display, func()) {
17	t.Helper()
18	d, err := NewDisplay()
19	if err != nil {
20		t.Fatal(err)
21	}
22	ctx, cancel := context.WithCancel(context.Background())
23	done := make(chan error, 1)
24	go func() { done <- d.Run(ctx) }()
25	var once bool
26	stop := func() {
27		if once {
28			return
29		}
30		once = true
31		cancel()
32		select {
33		case err := <-done:
34			if err != nil {
35				t.Error(err)
36			}
37		case <-time.After(3 * time.Second):
38			t.Fatal("Run did not return")
39		}
40	}
41	t.Cleanup(stop)
42	return d, stop
43}
44
45// sockPair is a connected socketpair. srv is meant to be adopted by the
46// display; peer is the test's end, closed at most once.
47type sockPair struct {
48	srv, peer int
49	once      sync.Once
50}
51
52func socketPair(t *testing.T) *sockPair {
53	t.Helper()
54	raw, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
55	if err != nil {
56		t.Fatal(err)
57	}
58	p := &sockPair{srv: raw[0], peer: raw[1]}
59	t.Cleanup(p.closePeer)
60	return p
61}
62
63// closePeer closes the peer end; it is idempotent, so a test may call it and
64// the cleanup still will not close a reused descriptor number.
65func (p *sockPair) closePeer() { p.once.Do(func() { _ = unix.Close(p.peer) }) }
66
67// fileID identifies the open file behind fd, immune to fd number reuse.
68type fileID struct{ dev, ino uint64 }
69
70func identify(t *testing.T, fd int) fileID {
71	t.Helper()
72	var st unix.Stat_t
73	if err := unix.Fstat(fd, &st); err != nil {
74		t.Fatal(err)
75	}
76	return fileID{uint64(st.Dev), st.Ino}
77}
78
79// stillOpen reports whether fd still refers to the file id.
80func stillOpen(fd int, id fileID) bool {
81	var st unix.Stat_t
82	if unix.Fstat(fd, &st) != nil {
83		return false
84	}
85	return fileID{uint64(st.Dev), st.Ino} == id
86}
87
88func adopt(t *testing.T, d *Display, fd int) Client {
89	t.Helper()
90	var c Client
91	var err error
92	if !d.Do(func() { c, err = d.CreateClient(fd) }) {
93		t.Fatal("display stopped")
94	}
95	if err != nil {
96		t.Fatal(err)
97	}
98	return c
99}
100
101func TestClientFD(t *testing.T) {
102	d, _ := clientDisplay(t)
103	fds := socketPair(t)
104	c := adopt(t, d, fds.srv)
105	var (
106		got, zero int
107		err, zerr error
108		allocs    float64
109	)
110	if !d.Do(func() {
111		got, err = c.FD()
112		zero, zerr = Client{}.FD()
113		allocs = testing.AllocsPerRun(100, func() { _, _ = c.FD() })
114	}) {
115		t.Fatal("display stopped")
116	}
117	if err != nil || got != fds.srv {
118		t.Fatalf("FD = %d, %v; want %d", got, err, fds.srv)
119	}
120	if zerr == nil || zero != -1 {
121		t.Fatalf("invalid client FD = %d, %v", zero, zerr)
122	}
123	if allocs != 0 {
124		t.Fatalf("FD allocs = %g, want 0", allocs)
125	}
126}
127
128func TestCreateClientFailureKeepsFD(t *testing.T) {
129	d, _ := clientDisplay(t)
130	var p [2]int
131	if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil {
132		t.Fatal(err)
133	}
134	defer unix.Close(p[0])
135	defer unix.Close(p[1])
136	pipeID := identify(t, p[0])
137	var err, errNeg error
138	var c, cn Client
139	if !d.Do(func() {
140		c, err = d.CreateClient(p[0]) // not a socket: SO_PEERCRED fails
141		cn, errNeg = d.CreateClient(-1)
142	}) {
143		t.Fatal("display stopped")
144	}
145	if !errors.Is(err, unix.ENOTSOCK) {
146		t.Fatalf("error %v does not wrap ENOTSOCK", err)
147	}
148	if err == nil || c != (Client{}) || errNeg == nil || cn != (Client{}) {
149		t.Fatalf("got %v %v, %v %v; want errors", c, err, cn, errNeg)
150	}
151	if !stillOpen(p[0], pipeID) {
152		t.Fatal("failed CreateClient closed the fd")
153	}
154}
155
156func TestCreateClientOwnsFD(t *testing.T) {
157	d, _ := clientDisplay(t)
158	fds := socketPair(t)
159	c := adopt(t, d, fds.srv)
160	gone := make(chan struct{})
161	d.Do(func() { c.OnDestroy(func() { close(gone) }) })
162	id := identify(t, fds.srv)
163	fds.closePeer()
164	select {
165	case <-gone:
166	case <-time.After(3 * time.Second):
167		t.Fatal("client not destroyed")
168	}
169	// The listener runs before libwayland closes the fd; Do returns only once
170	// the destruction that ran it has finished.
171	d.Do(func() {})
172	if stillOpen(fds.srv, id) {
173		t.Fatal("libwayland did not close the adopted fd")
174	}
175}
176
177// A raw client speaks the wire protocol over the other socketpair end.
178func wireMsg(id uint32, opcode uint16, args ...[]byte) []byte {
179	var body []byte
180	for _, a := range args {
181		body = append(body, a...)
182	}
183	b := make([]byte, 8, 8+len(body))
184	binary.LittleEndian.PutUint32(b, id)
185	binary.LittleEndian.PutUint32(b[4:], uint32(8+len(body))<<16|uint32(opcode))
186	return append(b, body...)
187}
188
189func u32(v uint32) []byte { return binary.LittleEndian.AppendUint32(nil, v) }
190
191func wireString(s string) []byte {
192	b := u32(uint32(len(s) + 1))
193	b = append(b, s...)
194	b = append(b, 0)
195	for len(b)%4 != 0 {
196		b = append(b, 0)
197	}
198	return b
199}
200
201func TestCreateClientBindsRegistry(t *testing.T) {
202	iface := &Interface{Name: "adopted_client_test", Version: 1}
203	if err := NewInterfaces(iface); err != nil {
204		t.Fatal(err)
205	}
206	d, err := NewDisplay()
207	if err != nil {
208		t.Fatal(err)
209	}
210	bound := make(chan Client, 1)
211	if err := d.CreateGlobal(iface, 1, func(c Client, version, id uint32) { bound <- c }); err != nil {
212		t.Fatal(err)
213	}
214	ctx, cancel := context.WithCancel(context.Background())
215	done := make(chan error, 1)
216	go func() { done <- d.Run(ctx) }()
217	defer func() { cancel(); <-done }()
218
219	fds := socketPair(t)
220	c := adopt(t, d, fds.srv)
221	if err := unix.SetsockoptTimeval(fds.peer, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {
222		t.Fatal(err)
223	}
224	// wl_display.get_registry(new_id 2), then wl_display.sync(new_id 3).
225	req := append(wireMsg(1, 1, u32(2)), wireMsg(1, 0, u32(3))...)
226	if _, err := unix.Write(fds.peer, req); err != nil {
227		t.Fatal(err)
228	}
229	// Read until the sync callback's done event (object 3), collecting globals.
230	var stream []byte
231	var name uint32
232	buf := make([]byte, 4096)
233	for sawDone := false; !sawDone; {
234		n, err := unix.Read(fds.peer, buf)
235		if err != nil || n == 0 {
236			t.Fatalf("read: %d, %v", n, err)
237		}
238		stream = append(stream, buf[:n]...)
239		for len(stream) >= 8 {
240			size := int(binary.LittleEndian.Uint32(stream[4:]) >> 16)
241			if size < 8 || len(stream) < size {
242				break
243			}
244			id, opcode := binary.LittleEndian.Uint32(stream), uint16(binary.LittleEndian.Uint32(stream[4:]))
245			body := stream[8:size]
246			if id == 2 && opcode == 0 { // wl_registry.global(name, interface, version)
247				strLen := int(binary.LittleEndian.Uint32(body[4:]))
248				if string(body[8:8+strLen-1]) == iface.Name {
249					name = binary.LittleEndian.Uint32(body)
250				}
251			}
252			if id == 3 {
253				sawDone = true
254			}
255			stream = stream[size:]
256		}
257	}
258	if name == 0 {
259		t.Fatal("global not advertised to the adopted client")
260	}
261	// wl_registry.bind(name, interface, version, new_id 4)
262	if _, err := unix.Write(fds.peer, wireMsg(2, 0, u32(name), wireString(iface.Name), u32(1), u32(4))); err != nil {
263		t.Fatal(err)
264	}
265	select {
266	case got := <-bound:
267		if got != c {
268			t.Fatalf("bind client %v, want adopted %v", got, c)
269		}
270	case <-time.After(3 * time.Second):
271		t.Fatal("bind never reached the global")
272	}
273}
274
275func TestOnDestroyOnDisconnect(t *testing.T) {
276	iface := &Interface{Name: "on_destroy_test", Version: 1}
277	if err := NewInterfaces(iface); err != nil {
278		t.Fatal(err)
279	}
280	d, _ := clientDisplay(t)
281	fds := socketPair(t)
282	c := adopt(t, d, fds.srv)
283	var (
284		order              []int
285		resAlive, fdOK     bool
286		other              = map[Client]int{c: 1}
287		freeBefore, free   int
288		fired              = make(chan struct{})
289		res                *Resource
290		createErr, fdError error
291	)
292	if !d.Do(func() {
293		res, createErr = c.CreateResource(iface, 1, 2, nil)
294		for i := 1; i <= 3; i++ {
295			c.OnDestroy(func() {
296				order = append(order, i)
297				if i == 3 {
298					var fd int
299					fd, fdError = c.FD()
300					fdOK = fd == fds.srv
301					resAlive = res.Alive()
302					delete(other, c)
303					close(fired)
304				}
305			})
306		}
307		freeBefore = len(listeners.free) // after the slots are taken
308		c.OnDestroy(nil)
309		Client{}.OnDestroy(func() { t.Error("zero client listener ran") })
310	}) {
311		t.Fatal("display stopped")
312	}
313	if createErr != nil {
314		t.Fatal(createErr)
315	}
316	select {
317	case <-fired:
318		t.Fatal("fired before disconnect")
319	case <-time.After(50 * time.Millisecond):
320	}
321	fds.closePeer()
322	select {
323	case <-fired:
324	case <-time.After(3 * time.Second):
325		t.Fatal("OnDestroy did not fire on disconnect")
326	}
327	d.Do(func() { free = len(listeners.free) })
328	if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 {
329		t.Fatalf("order %v, want [1 2 3]", order)
330	}
331	if fdError != nil || !fdOK || !resAlive {
332		t.Fatalf("during OnDestroy: fd ok=%v err=%v resource alive=%v", fdOK, fdError, resAlive)
333	}
334	if len(other) != 0 || res.Alive() {
335		t.Fatalf("map %v, resource alive %v", other, res.Alive())
336	}
337	if free != freeBefore+3 || len(live.clientGone) != 0 {
338		t.Fatalf("listener slots not recycled: free %d -> %d, live %d", freeBefore, free, len(live.clientGone))
339	}
340}
341
342func TestOnDestroyOnDisplayClose(t *testing.T) {
343	d, err := NewDisplay()
344	if err != nil {
345		t.Fatal(err)
346	}
347	fds := socketPair(t)
348	c, err := d.CreateClient(fds.srv)
349	if err != nil {
350		t.Fatal(err)
351	}
352	n := 0
353	c.OnDestroy(func() { n++ })
354	c.OnDestroy(func() { n += 10 })
355	d.Close()
356	d.Close()
357	if n != 11 {
358		t.Fatalf("listeners ran %d, want 11 (each once)", n)
359	}
360}
361
362func TestOnDestroyOnRunStop(t *testing.T) {
363	d, stop := clientDisplay(t)
364	fds := socketPair(t)
365	c := adopt(t, d, fds.srv)
366	n := 0
367	d.Do(func() { c.OnDestroy(func() { n++ }) })
368	stop()
369	if n != 1 {
370		t.Fatalf("listener ran %d times, want 1", n)
371	}
372}
373
374// A client destroyed at disconnect must not fire again when the display stops.
375func TestOnDestroyOnce(t *testing.T) {
376	d, stop := clientDisplay(t)
377	fds := socketPair(t)
378	c := adopt(t, d, fds.srv)
379	fired := make(chan struct{}, 4)
380	d.Do(func() { c.OnDestroy(func() { fired <- struct{}{} }) })
381	fds.closePeer()
382	select {
383	case <-fired:
384	case <-time.After(3 * time.Second):
385		t.Fatal("OnDestroy did not fire")
386	}
387	stop()
388	if len(fired) != 0 {
389		t.Fatal("OnDestroy fired twice")
390	}
391}
392
393// A peer that already hung up does not stop adoption; the client is then
394// destroyed and its listener runs.
395func TestCreateClientPeerAlreadyClosed(t *testing.T) {
396	d, _ := clientDisplay(t)
397	fds := socketPair(t)
398	fds.closePeer()
399	id := identify(t, fds.srv) // before adoption: the client may die at once
400	gone := make(chan struct{})
401	var err error
402	// Adopt and register in one call, before the loop can destroy the client.
403	if !d.Do(func() {
404		var c Client
405		if c, err = d.CreateClient(fds.srv); err == nil {
406			c.OnDestroy(func() { close(gone) })
407		}
408	}) {
409		t.Fatal("display stopped")
410	}
411	if err != nil {
412		t.Fatal(err)
413	}
414	select {
415	case <-gone:
416	case <-time.After(3 * time.Second):
417		t.Fatal("client with a closed peer was not destroyed")
418	}
419	d.Do(func() {})
420	if stillOpen(fds.srv, id) {
421		t.Fatal("adopted fd not closed")
422	}
423}
424
425// A listener may register on another live client from inside its callback,
426// even though it is handed the slot that is firing.
427func TestOnDestroyRegistersOnOtherClient(t *testing.T) {
428	d, _ := clientDisplay(t)
429	a, b := socketPair(t), socketPair(t)
430	ca, cb := adopt(t, d, a.srv), adopt(t, d, b.srv)
431	aGone := make(chan struct{})
432	bGone := make(chan struct{}, 2)
433	var slotA, slotB uintptr
434	d.Do(func() {
435		for l := range live.clientGone {
436			t.Errorf("unexpected live listener %#x", l)
437		}
438		ca.OnDestroy(func() {
439			cb.OnDestroy(func() { bGone <- struct{}{} })
440			for l := range live.clientGone {
441				slotB = l
442			}
443			close(aGone)
444		})
445		for l := range live.clientGone {
446			slotA = l
447		}
448	})
449	a.closePeer()
450	select {
451	case <-aGone:
452	case <-time.After(3 * time.Second):
453		t.Fatal("first client's listener did not fire")
454	}
455	if slotA == 0 || slotA != slotB {
456		t.Fatalf("freed slot not reused: %#x then %#x", slotA, slotB)
457	}
458	select {
459	case <-bGone:
460		t.Fatal("second client's listener fired early")
461	case <-time.After(50 * time.Millisecond):
462	}
463	b.closePeer()
464	select {
465	case <-bGone:
466	case <-time.After(3 * time.Second):
467		t.Fatal("listener registered from a callback did not fire")
468	}
469	time.Sleep(50 * time.Millisecond)
470	if len(bGone) != 0 {
471		t.Fatal("listener fired twice")
472	}
473}
474
475// More listeners than one mapped chunk holds: all fire once, every slot comes
476// back, and a later registration reuses a recycled slot.
477func TestOnDestroyManyListeners(t *testing.T) {
478	d, _ := clientDisplay(t)
479	fds := socketPair(t)
480	fds2 := socketPair(t)
481	c := adopt(t, d, fds.srv)
482	const n = 3*listenerChunkSize/listenerSlot + 5
483	var fired, chunksBefore int
484	done := make(chan struct{})
485	var before, after int
486	d.Do(func() {
487		for i := 0; i < n; i++ {
488			c.OnDestroy(func() {
489				fired++
490				if fired == n {
491					close(done)
492				}
493			})
494		}
495		before = len(listeners.free)
496		chunksBefore = len(listeners.chunks)
497	})
498	if chunksBefore < 3 {
499		t.Fatalf("test did not cross a chunk boundary: %d chunks", chunksBefore)
500	}
501	fds.closePeer()
502	select {
503	case <-done:
504	case <-time.After(3 * time.Second):
505		t.Fatal("not every listener fired")
506	}
507	var reused bool
508	var firedNow int
509	d.Do(func() {
510		firedNow = fired
511		after = len(listeners.free)
512		c2, err := d.CreateClient(fds2.srv)
513		if err != nil {
514			t.Error(err)
515			return
516		}
517		c2.OnDestroy(func() {})
518		reused = len(listeners.chunks) == chunksBefore && len(listeners.free) == after-1
519	})
520	if after != before+n {
521		t.Fatalf("free slots %d -> %d, want +%d", before, after, n)
522	}
523	if firedNow != n || !reused {
524		t.Fatalf("fired %d, slot reused %v", firedNow, reused)
525	}
526}

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 14

↑↓ moveenter openesc close