v0.5.0 089fa12

core_test.go RawHistoryFolder

1//go:build linux
2
3package core_test
4
5import (
6	"encoding/binary"
7	"errors"
8	"io"
9	"net"
10	"os"
11	"testing"
12	"time"
13
14	"github.com/bnema/wlturbo"
15	"github.com/bnema/wlturbo/protocol/core"
16	"golang.org/x/sys/unix"
17)
18
19func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
20	t.Helper()
21	f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
22	if err != nil {
23		t.Fatal(err)
24	}
25	a := os.NewFile(uintptr(f[0]), "a")
26	b := os.NewFile(uintptr(f[1]), "b")
27	ca, e := net.FileConn(a)
28	a.Close()
29	if e != nil {
30		t.Fatal(e)
31	}
32	cb, e := net.FileConn(b)
33	b.Close()
34	if e != nil {
35		t.Fatal(e)
36	}
37	t.Cleanup(func() { ca.Close(); cb.Close() })
38	return ca.(*net.UnixConn), cb.(*net.UnixConn)
39}
40func frame(id uint32, op uint16, body ...uint32) []byte {
41	p := make([]byte, 8+4*len(body))
42	binary.LittleEndian.PutUint32(p, id)
43	binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
44	for i, v := range body {
45		binary.LittleEndian.PutUint32(p[8+i*4:], v)
46	}
47	return p
48}
49func receiveRequest(t *testing.T, p *net.UnixConn) {
50	t.Helper()
51	p.SetReadDeadline(testDeadline())
52	b := make([]byte, 256)
53	if _, e := p.Read(b); e != nil {
54		t.Fatal(e)
55	}
56}
57func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) }
58func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
59	t.Helper()
60	var oob []byte
61	if len(fd) > 0 {
62		oob = unix.UnixRights(fd...)
63	}
64	if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil {
65		t.Fatal(e)
66	}
67}
68func TestGeneratedCoreOverSocketpair(t *testing.T) {
69	c, p := pair(t)
70	d, err := wlturbo.ConnectFromConn(c)
71	if err != nil {
72		t.Fatal(err)
73	}
74	defer d.Close()
75	receiveRequest(t, p) // get_registry
76	seat := core.NewSeat(d.Context())
77	if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil {
78		t.Fatal(err)
79	}
80	receiveRequest(t, p)
81	kb, err := seat.GetKeyboard()
82	if err != nil {
83		t.Fatal(err)
84	}
85	receiveRequest(t, p)
86	var key uint32
87	kb.OnKey(func(_, _, k, _ uint32) { key = k })
88	send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1))
89	if err := d.Dispatch(); err != nil {
90		t.Fatal(err)
91	}
92	if key != 42 {
93		t.Fatalf("key=%d", key)
94	}
95	compositor := core.NewCompositor(d.Context())
96	if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil {
97		t.Fatal(err)
98	}
99	receiveRequest(t, p)
100	surface, err := compositor.CreateSurface()
101	if err != nil {
102		t.Fatal(err)
103	}
104	receiveRequest(t, p)
105	callback, err := surface.Frame()
106	if err != nil {
107		t.Fatal(err)
108	}
109	receiveRequest(t, p)
110	var done uint32
111	callback.OnDone(func(ms uint32) { done = ms })
112	send(t, p, frame(callback.ID(), 0, 123))
113	if err := d.Dispatch(); err != nil {
114		t.Fatal(err)
115	}
116	if done != 123 {
117		t.Fatalf("done=%d", done)
118	}
119	send(t, p, frame(callback.ID(), 0, 124))
120	if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
121		t.Fatalf("destroyed callback: %v", err)
122	}
123}
124func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) {
125	c, p := pair(t)
126	d, e := wlturbo.ConnectFromConn(c)
127	if e != nil {
128		t.Fatal(e)
129	}
130	defer d.Close()
131	receiveRequest(t, p)
132	kb := core.NewKeyboard(d.Context())
133	kb.SetID(d.AllocateID())
134	d.Context().Register(kb)
135	r, w, e := os.Pipe()
136	if e != nil {
137		t.Fatal(e)
138	}
139	defer r.Close()
140	send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
141	w.Close()
142	if e := d.Dispatch(); e != nil {
143		t.Fatal(e)
144	}
145	buf := make([]byte, 1)
146	if n, e := r.Read(buf); n != 0 || e != io.EOF {
147		t.Fatalf("unclaimed FD n=%d err=%v", n, e)
148	}
149	r2, w2, e := os.Pipe()
150	if e != nil {
151		t.Fatal(e)
152	}
153	defer r2.Close()
154	received := 0
155	kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
156		if fd == nil {
157			t.Fatal("first FD missing")
158		}
159		n, e := fd.Take()
160		if e != nil {
161			t.Fatal(e)
162		}
163		received++
164		unix.Close(n)
165	})
166	kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
167		if fd != nil {
168			t.Fatal("duplicated owner")
169		}
170		received++
171	})
172	send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd()))
173	w2.Close()
174	if e := d.Dispatch(); e != nil {
175		t.Fatal(e)
176	}
177	if received != 2 {
178		t.Fatalf("handlers=%d", received)
179	}
180}
181func TestBufferReleaseAndOfferChild(t *testing.T) {
182	c, p := pair(t)
183	d, e := wlturbo.ConnectFromConn(c)
184	if e != nil {
185		t.Fatal(e)
186	}
187	defer d.Close()
188	receiveRequest(t, p)
189	b := core.NewBuffer(d.Context())
190	b.SetID(d.AllocateID())
191	d.Context().Register(b)
192	releases := 0
193	b.OnRelease(func() { releases++ })
194	send(t, p, frame(b.ID(), 0))
195	if e := d.Dispatch(); e != nil {
196		t.Fatal(e)
197	}
198	if releases != 1 {
199		t.Fatal("release not dispatched")
200	}
201	device := core.NewDataDevice(d.Context())
202	device.SetID(d.AllocateID())
203	d.Context().Register(device)
204	var offer *core.DataOffer
205	device.OnDataOffer(func(o *core.DataOffer) { offer = o })
206	send(t, p, frame(device.ID(), 0, 0xff000001))
207	if e := d.Dispatch(); e != nil {
208		t.Fatal(e)
209	}
210	if offer == nil || offer.ID() != 0xff000001 {
211		t.Fatalf("offer=%v", offer)
212	}
213}
214
215func TestMissingKeymapDescriptor(t *testing.T) {
216	c, p := pair(t)
217	d, e := wlturbo.ConnectFromConn(c)
218	if e != nil {
219		t.Fatal(e)
220	}
221	defer d.Close()
222	receiveRequest(t, p)
223	kb := core.NewKeyboard(d.Context())
224	kb.SetID(d.AllocateID())
225	d.Context().Register(kb)
226	send(t, p, frame(kb.ID(), 0, 1, 8))
227	if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
228		t.Fatalf("missing FD: %v", e)
229	}
230}
231
232func TestInterleavedFDAndPlainMessages(t *testing.T) {
233	c, p := pair(t)
234	d, e := wlturbo.ConnectFromConn(c)
235	if e != nil {
236		t.Fatal(e)
237	}
238	defer d.Close()
239	receiveRequest(t, p)
240	kb := core.NewKeyboard(d.Context())
241	kb.SetID(d.AllocateID())
242	d.Context().Register(kb)
243	calls := 0
244	kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
245		if fd == nil {
246			t.Fatal("FD not assigned")
247		}
248		_ = fd.Close()
249		calls++
250	})
251	kb.OnKey(func(_, _, key, _ uint32) {
252		if key != 33 {
253			t.Errorf("key=%d", key)
254		}
255		calls++
256	})
257	r, w, e := os.Pipe()
258	if e != nil {
259		t.Fatal(e)
260	}
261	defer r.Close()
262	send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
263	w.Close()
264	send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1))
265	if e := d.Dispatch(); e != nil {
266		t.Fatal(e)
267	}
268	if e := d.Dispatch(); e != nil {
269		t.Fatal(e)
270	}
271	if calls != 2 {
272		t.Fatalf("calls=%d", calls)
273	}
274}
275func TestMalformedGeneratedPayload(t *testing.T) {
276	c, p := pair(t)
277	d, e := wlturbo.ConnectFromConn(c)
278	if e != nil {
279		t.Fatal(e)
280	}
281	defer d.Close()
282	receiveRequest(t, p)
283	kb := core.NewKeyboard(d.Context())
284	kb.SetID(d.AllocateID())
285	d.Context().Register(kb)
286	send(t, p, frame(kb.ID(), 3, 1, 2))
287	if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
288		t.Fatalf("truncated key: %v", e)
289	}
290}
291
292func TestServerChildEventsAfterOffer(t *testing.T) {
293	c, p := pair(t)
294	d, e := wlturbo.ConnectFromConn(c)
295	if e != nil {
296		t.Fatal(e)
297	}
298	defer d.Close()
299	receiveRequest(t, p)
300	device := core.NewDataDevice(d.Context())
301	device.SetID(d.AllocateID())
302	d.Context().Register(device)
303	const offerID = 0xff000011
304	var offer *core.DataOffer
305	device.OnDataOffer(func(o *core.DataOffer) { offer = o })
306	send(t, p, frame(device.ID(), 0, offerID))
307	if e := d.Dispatch(); e != nil {
308		t.Fatal(e)
309	}
310	if offer == nil {
311		t.Fatal("missing concrete child")
312	}
313	got := ""
314	offer.OnOffer(func(mime string) { got = mime })
315	payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0)
316	binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16)
317	send(t, p, payload)
318	if e := d.Dispatch(); e != nil {
319		t.Fatal(e)
320	}
321	if got != "abc" {
322		t.Fatalf("typed child event: %q", got)
323	}
324}
325
326func TestRegistryBootstrapAnnouncements(t *testing.T) {
327	c, p := pair(t)
328	d, e := wlturbo.ConnectFromConn(c)
329	if e != nil {
330		t.Fatal(e)
331	}
332	defer d.Close()
333	receiveRequest(t, p)
334	var got wlturbo.Global
335	d.Registry().AddHandler("wl_compositor", func(r *wlturbo.Registry, n, v uint32) { got, _ = r.FindGlobalByName(n) })
336	name := "wl_compositor"
337	body := make([]byte, 4+4+((len(name)+1+3)&^3)+4)
338	binary.LittleEndian.PutUint32(body, 17)
339	binary.LittleEndian.PutUint32(body[4:], uint32(len(name)+1))
340	copy(body[8:], name)
341	binary.LittleEndian.PutUint32(body[len(body)-4:], 7)
342	msg := append(frame(d.Registry().ID(), 0), body...)
343	binary.LittleEndian.PutUint32(msg[4:], uint32(len(msg))<<16)
344	send(t, p, msg)
345	if e := d.Dispatch(); e != nil {
346		t.Fatal(e)
347	}
348	if got.Name != 17 || got.Interface != name || got.Version != 7 {
349		t.Fatalf("global: %+v", got)
350	}
351	send(t, p, frame(d.Registry().ID(), 1, 17))
352	if e := d.Dispatch(); e != nil {
353		t.Fatal(e)
354	}
355	if _, ok := d.Registry().FindGlobalByName(17); ok {
356		t.Fatal("global not removed")
357	}
358	send(t, p, frame(d.Registry().ID(), 2))
359	if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrUnknownOpcode) {
360		t.Fatalf("unknown registry opcode: %v", e)
361	}
362}
363func TestUnclaimedFirstHandlerFDClosed(t *testing.T) {
364	c, p := pair(t)
365	d, e := wlturbo.ConnectFromConn(c)
366	if e != nil {
367		t.Fatal(e)
368	}
369	defer d.Close()
370	receiveRequest(t, p)
371	kb := core.NewKeyboard(d.Context())
372	kb.SetID(d.AllocateID())
373	d.Context().Register(kb)
374	kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
375		if fd == nil {
376			t.Fatal("missing first owner")
377		}
378	})
379	kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
380		if fd != nil {
381			t.Fatal("duplicate owner")
382		}
383	})
384	r, w, e := os.Pipe()
385	if e != nil {
386		t.Fatal(e)
387	}
388	defer r.Close()
389	send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
390	w.Close()
391	if e := d.Dispatch(); e != nil {
392		t.Fatal(e)
393	}
394	if e := r.SetReadDeadline(testDeadline()); e != nil {
395		t.Fatal(e)
396	}
397	var b [1]byte
398	if n, e := r.Read(b[:]); n != 0 || e != io.EOF {
399		t.Fatalf("unclaimed FD not closed: %d %v", n, e)
400	}
401}
402
403func TestDestroyedProxyRejectsRequests(t *testing.T) {
404	c, p := pair(t)
405	d, err := wlturbo.ConnectFromConn(c)
406	if err != nil {
407		t.Fatal(err)
408	}
409	defer d.Close()
410	receiveRequest(t, p)
411	surface := core.NewSurface(d.Context())
412	surface.SetID(d.AllocateID())
413	d.Context().Register(surface)
414	if err := surface.Destroy(); err != nil {
415		t.Fatal(err)
416	}
417	receiveRequest(t, p)
418	if err := surface.Destroy(); err == nil {
419		t.Fatal("second destroy succeeded")
420	}
421	if err := surface.Commit(); err == nil {
422		t.Fatal("request on destroyed surface succeeded")
423	}
424	if _, err := surface.Frame(); err == nil {
425		t.Fatal("child request on destroyed surface succeeded")
426	}
427	p.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
428	_, err = p.Read(make([]byte, 32))
429	ne, ok := err.(net.Error)
430	if !ok || !ne.Timeout() {
431		t.Fatalf("unexpected bytes on wire: %v", err)
432	}
433}
434
435func TestConcurrentDestroySendsOnce(t *testing.T) {
436	c, p := pair(t)
437	d, err := wlturbo.ConnectFromConn(c)
438	if err != nil {
439		t.Fatal(err)
440	}
441	defer d.Close()
442	receiveRequest(t, p)
443	const rounds, destroyers, committers = 100, 4, 4
444	totalCommits := 0
445	for round := 0; round < rounds; round++ {
446		surface := core.NewSurface(d.Context())
447		surface.SetID(d.AllocateID())
448		d.Context().Register(surface)
449		start := make(chan struct{})
450		destroyed := make(chan error, destroyers)
451		committed := make(chan error, committers)
452		for i := 0; i < destroyers; i++ {
453			go func() { <-start; destroyed <- surface.Destroy() }()
454		}
455		for i := 0; i < committers; i++ {
456			go func() { <-start; committed <- surface.Commit() }()
457		}
458		close(start)
459		okDestroy, okCommit := 0, 0
460		for i := 0; i < destroyers; i++ {
461			if <-destroyed == nil {
462				okDestroy++
463			}
464		}
465		for i := 0; i < committers; i++ {
466			if <-committed == nil {
467				okCommit++
468			}
469		}
470		if okDestroy != 1 {
471			t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy)
472		}
473		totalCommits += okCommit
474		// Decode every frame: exactly okCommit commits (opcode 6), then one
475		// destroy (opcode 0) for this surface, and nothing after it.
476		frames := readFrames(t, p, okCommit+1)
477		for i, f := range frames {
478			if f.id != surface.ID() {
479				t.Fatalf("round %d: frame %d targets object %d", round, i, f.id)
480			}
481			last := i == len(frames)-1
482			if last && f.opcode != 0 || !last && f.opcode != 6 {
483				t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last)
484			}
485		}
486		p.SetReadDeadline(time.Now().Add(2 * time.Millisecond))
487		if n, err := p.Read(make([]byte, 8)); n != 0 {
488			t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err)
489		}
490		p.SetReadDeadline(time.Time{})
491	}
492	// Ordering is only exercised when some commits won the race.
493	if totalCommits == 0 {
494		t.Fatal("no commit succeeded in any round; ordering was not exercised")
495	}
496}
497
498type wireFrame struct {
499	id     uint32
500	opcode uint16
501}
502
503func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame {
504	t.Helper()
505	p.SetReadDeadline(testDeadline())
506	defer p.SetReadDeadline(time.Time{})
507	var out []wireFrame
508	var buf []byte
509	tmp := make([]byte, 4096)
510	for len(out) < n {
511		for len(buf) >= 8 {
512			size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16)
513			if size < 8 || len(buf) < size {
514				break
515			}
516			out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))})
517			buf = buf[size:]
518		}
519		if len(out) >= n {
520			break
521		}
522		m, err := p.Read(tmp)
523		if err != nil {
524			t.Fatalf("read frames: got %d of %d: %v", len(out), n, err)
525		}
526		buf = append(buf, tmp[:m]...)
527	}
528	if len(out) != n || len(buf) != 0 {
529		t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n)
530	}
531	return out
532}

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 8

↑↓ moveenter openesc close