v0.3.0 96f3aeb

extensions_test.go RawHistoryFolder

1//go:build linux
2
3package protocol_test
4
5import (
6	"encoding/binary"
7	"errors"
8	"net"
9	"os"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/bnema/wlturbo"
15	"github.com/bnema/wlturbo/protocol/core"
16	"github.com/bnema/wlturbo/protocol/cursorshape"
17	"github.com/bnema/wlturbo/protocol/drmsyncobj"
18	"github.com/bnema/wlturbo/protocol/fractionalscale"
19	"github.com/bnema/wlturbo/protocol/linuxdmabuf"
20	"github.com/bnema/wlturbo/protocol/textinput"
21	"github.com/bnema/wlturbo/protocol/viewporter"
22	"github.com/bnema/wlturbo/protocol/xdgshell"
23	"golang.org/x/sys/unix"
24)
25
26func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
27	t.Helper()
28	fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
29	if e != nil {
30		t.Fatal(e)
31	}
32	a := os.NewFile(uintptr(fds[0]), "a")
33	b := os.NewFile(uintptr(fds[1]), "b")
34	ca, e := net.FileConn(a)
35	a.Close()
36	if e != nil {
37		t.Fatal(e)
38	}
39	cb, e := net.FileConn(b)
40	b.Close()
41	if e != nil {
42		t.Fatal(e)
43	}
44	t.Cleanup(func() { ca.Close(); cb.Close() })
45	return ca.(*net.UnixConn), cb.(*net.UnixConn)
46}
47func msg(id uint32, op uint16, body ...uint32) []byte {
48	b := make([]byte, 8+4*len(body))
49	binary.NativeEndian.PutUint32(b, id)
50	binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
51	for i, v := range body {
52		binary.NativeEndian.PutUint32(b[8+i*4:], v)
53	}
54	return b
55}
56func payload(id uint32, op uint16, b ...byte) []byte {
57	p := make([]byte, 8+len(b))
58	copy(p, msg(id, op))
59	copy(p[8:], b)
60	binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
61	return p
62}
63func arr(b []byte) []byte {
64	out := make([]byte, 4+(len(b)+3)&^3)
65	binary.NativeEndian.PutUint32(out, uint32(len(b)))
66	copy(out[4:], b)
67	return out
68}
69func str(s string) []byte { return arr(append([]byte(s), 0)) }
70func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) {
71	t.Helper()
72	var rights []byte
73	if len(fd) > 0 {
74		rights = unix.UnixRights(fd...)
75	}
76	if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil {
77		t.Fatal(e)
78	}
79}
80func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
81	t.Helper()
82	p.SetReadDeadline(time.Now().Add(2 * time.Second))
83	b := make([]byte, 4096)
84	n, e := p.Read(b)
85	if e != nil {
86		t.Fatal(e)
87	}
88	if n < 8 {
89		t.Fatal("short request")
90	}
91	return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n]
92}
93func noerr(t *testing.T, e error) {
94	t.Helper()
95	if e != nil {
96		t.Fatal(e)
97	}
98}
99func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, announced uint32, supported uint32, proxy wlturbo.Proxy) {
100	t.Helper()
101	// A fake server must advertise a global before clients can negotiate it.
102	announcement := append(msg(0, 0, name)[8:], str(iface)...)
103	announcement = append(announcement, msg(0, 0, announced)[8:]...)
104	send(t, p, payload(d.Registry().ID(), 0, announcement...))
105	noerr(t, d.Dispatch())
106	v, err := d.Registry().BindNegotiated(iface, supported, proxy)
107	noerr(t, err)
108	want := min(announced, supported)
109	_, op, b := request(t, p)
110	if op != 0 || v != want || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != want || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
111		t.Fatalf("bad bind %s announced=%d supported=%d negotiated=%d wire=%x", iface, announced, supported, v, b)
112	}
113}
114func TestExtensionsOverSocketpair(t *testing.T) {
115	c, p := pair(t)
116	d, e := wlturbo.ConnectFromConn(c)
117	noerr(t, e)
118	defer d.Close()
119	request(t, p)
120	// A registry announcement is the only input to optional-global discovery.
121	send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...))
122	noerr(t, d.Dispatch())
123	if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 {
124		t.Fatal("announced fractional scale missing", g, ok)
125	}
126	if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
127		t.Fatal("absent text input")
128	}
129	// Remove the discovery-only announcement before the bind matrix assigns
130	// its own global name to this interface.
131	send(t, p, msg(d.Registry().ID(), 1, 11))
132	noerr(t, d.Dispatch())
133	surface := core.NewSurface(d.Context())
134	surface.SetID(d.AllocateID())
135	d.Context().Register(surface)
136	seat := core.NewSeat(d.Context())
137	seat.SetID(d.AllocateID())
138	d.Context().Register(seat)
139	wm := xdgshell.NewXdgWmBase(d.Context())
140	bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 6, 7, wm)
141	xs, e := wm.GetXdgSurface(surface)
142	noerr(t, e)
143	request(t, p)
144	top, e := xs.GetToplevel()
145	noerr(t, e)
146	request(t, p)
147	var sequence []string
148	top.OnConfigure(func(w, h int32, states []byte) {
149		if w != 800 || h != 600 {
150			t.Fatalf("size %d,%d", w, h)
151		}
152		sequence = append(sequence, "top")
153	})
154	xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) })
155	send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...))
156	noerr(t, d.Dispatch())
157	send(t, p, msg(xs.ID(), 0, 9))
158	noerr(t, d.Dispatch())
159	_, op, b := request(t, p)
160	if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" {
161		t.Fatalf("xdg %d %x %v", op, b, sequence)
162	}
163	noerr(t, top.Destroy())
164	request(t, p)
165	noerr(t, xs.Destroy())
166	request(t, p)
167	noerr(t, wm.Destroy())
168	request(t, p)
169	dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
170	bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, 4, dm)
171	fb, e := dm.GetDefaultFeedback()
172	noerr(t, e)
173	request(t, p)
174	table, e := os.CreateTemp(t.TempDir(), "table")
175	noerr(t, e)
176	var bytes [32]byte
177	binary.NativeEndian.PutUint32(bytes[:], 0x34325258)
178	binary.NativeEndian.PutUint64(bytes[8:], 19)
179	binary.NativeEndian.PutUint32(bytes[16:], 0x34325241)
180	binary.NativeEndian.PutUint64(bytes[24:], 42)
181	_, e = table.Write(bytes[:])
182	noerr(t, e)
183	defer table.Close()
184	var entries []linuxdmabuf.FormatEntry
185	var indices []uint16
186	var feedback []string
187	fb.OnMainDevice(func(dev []byte) {
188		if len(dev) != 8 {
189			t.Fatalf("device %x", dev)
190		}
191		feedback = append(feedback, "main")
192	})
193	fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
194		entries, e = linuxdmabuf.ReadFormatTable(fd, size)
195		noerr(t, e)
196		feedback = append(feedback, "table")
197	})
198	fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") })
199	fb.OnTrancheFormats(func(data []byte) {
200		indices, e = linuxdmabuf.TrancheIndices(data, len(entries))
201		noerr(t, e)
202		feedback = append(feedback, "formats")
203	})
204	fb.OnTrancheFlags(func(flags uint32) {
205		if flags != 1 {
206			t.Fatal(flags)
207		}
208		feedback = append(feedback, "flags")
209	})
210	fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") })
211	fb.OnDone(func() { feedback = append(feedback, "done") })
212	send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...))
213	noerr(t, d.Dispatch())
214	send(t, p, msg(fb.ID(), 1, 32), int(table.Fd()))
215	noerr(t, d.Dispatch())
216	send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...))
217	noerr(t, d.Dispatch())
218	send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...))
219	noerr(t, d.Dispatch())
220	send(t, p, msg(fb.ID(), 6, 1))
221	noerr(t, d.Dispatch())
222	send(t, p, msg(fb.ID(), 3))
223	noerr(t, d.Dispatch())
224	send(t, p, msg(fb.ID(), 0))
225	noerr(t, d.Dispatch())
226	if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" {
227		t.Fatalf("feedback %v %v %v", entries, indices, feedback)
228	}
229	noerr(t, fb.Destroy())
230	request(t, p)
231	params, e := dm.CreateParams()
232	noerr(t, e)
233	request(t, p)
234	f, e := os.Open("/dev/null")
235	noerr(t, e)
236	noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0))
237	request(t, p)
238	if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
239		t.Fatal("sent fd not closed")
240	}
241	f.Close()
242	var buffer *core.Buffer
243	params.OnCreated(func(b *core.Buffer) { buffer = b })
244	params.OnFailed(func() { sequence = append(sequence, "failed") })
245	noerr(t, params.Create(1, 1, 0x34325258, 0))
246	request(t, p)
247	send(t, p, msg(params.ID(), 0, 999))
248	noerr(t, d.Dispatch())
249	if buffer == nil || buffer.ID() != 999 {
250		t.Fatal("typed buffer missing")
251	}
252	noerr(t, buffer.Destroy())
253	request(t, p)
254	noerr(t, params.Destroy())
255	request(t, p)
256	failedParams, e := dm.CreateParams()
257	noerr(t, e)
258	request(t, p)
259	failedParams.OnFailed(func() { sequence = append(sequence, "failed") })
260	noerr(t, failedParams.Create(1, 1, 0x34325258, 0))
261	request(t, p)
262	send(t, p, msg(failedParams.ID(), 1))
263	noerr(t, d.Dispatch())
264	if sequence[len(sequence)-1] != "failed" {
265		t.Fatal(sequence)
266	}
267	noerr(t, failedParams.Destroy())
268	request(t, p)
269	immedParams, e := dm.CreateParams()
270	noerr(t, e)
271	request(t, p)
272	immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0)
273	noerr(t, e)
274	request(t, p)
275	noerr(t, immediate.Destroy())
276	request(t, p)
277	noerr(t, immedParams.Destroy())
278	request(t, p)
279	noerr(t, dm.Destroy())
280	request(t, p)
281	sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
282	bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, sync)
283	ss, e := sync.GetSurface(surface)
284	noerr(t, e)
285	request(t, p)
286	f, e = os.Open("/dev/null")
287	noerr(t, e)
288	tl, e := sync.ImportTimeline(int(f.Fd()))
289	noerr(t, e)
290	request(t, p)
291	if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
292		t.Fatal("timeline fd not closed")
293	}
294	f.Close()
295	noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456))
296	_, op, b = request(t, p)
297	if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 {
298		t.Fatalf("acquire %d %x", op, b)
299	}
300	noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc))
301	_, op, b = request(t, p)
302	if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 {
303		t.Fatalf("release %d %x", op, b)
304	}
305	noerr(t, ss.Destroy())
306	request(t, p)
307	noerr(t, tl.Destroy())
308	request(t, p)
309	noerr(t, sync.Destroy())
310	request(t, p)
311	vp := viewporter.NewWpViewporter(d.Context())
312	bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, 1, vp)
313	view, e := vp.GetViewport(surface)
314	noerr(t, e)
315	request(t, p)
316	noerr(t, view.Destroy())
317	request(t, p)
318	noerr(t, vp.Destroy())
319	request(t, p)
320	fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
321	bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, 1, fs)
322	scale, e := fs.GetFractionalScale(surface)
323	noerr(t, e)
324	request(t, p)
325	var preferred uint32
326	scale.OnPreferredScale(func(v uint32) { preferred = v })
327	send(t, p, msg(scale.ID(), 0, 150))
328	noerr(t, d.Dispatch())
329	if preferred != 150 {
330		t.Fatal(preferred)
331	}
332	noerr(t, scale.Destroy())
333	request(t, p)
334	noerr(t, fs.Destroy())
335	request(t, p)
336	ti := textinput.NewTextInputManagerV3(d.Context())
337	bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 2, 1, ti)
338	input, e := ti.GetTextInput(seat)
339	noerr(t, e)
340	request(t, p)
341	noerr(t, input.Enable())
342	request(t, p)
343	noerr(t, input.SetSurroundingText("hello", 5, 5))
344	request(t, p)
345	noerr(t, input.Commit())
346	request(t, p)
347	var events []string
348	input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) })
349	input.OnCommitString(func(s string) { events = append(events, "commit:"+s) })
350	input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") })
351	input.OnDone(func(uint32) { events = append(events, "done") })
352	send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...))
353	noerr(t, d.Dispatch())
354	send(t, p, payload(input.ID(), 3, str("hi")...))
355	noerr(t, d.Dispatch())
356	send(t, p, msg(input.ID(), 4, 1, 2))
357	noerr(t, d.Dispatch())
358	send(t, p, msg(input.ID(), 5, 1))
359	noerr(t, d.Dispatch())
360	if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" {
361		t.Fatal(events)
362	}
363	noerr(t, input.Disable())
364	request(t, p)
365	noerr(t, input.Commit())
366	request(t, p)
367	noerr(t, input.Destroy())
368	request(t, p)
369	noerr(t, ti.Destroy())
370	request(t, p)
371	// Core data-device remains in protocol/core; verify its typed child and
372	// both directions of clipboard descriptor ownership.
373	mgr := core.NewDataDeviceManager(d.Context())
374	bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, 4, mgr)
375	device, e := mgr.GetDataDevice(seat)
376	noerr(t, e)
377	request(t, p)
378	source, e := mgr.CreateDataSource()
379	noerr(t, e)
380	request(t, p)
381	var offer *core.DataOffer
382	device.OnDataOffer(func(o *core.DataOffer) { offer = o })
383	send(t, p, msg(device.ID(), 0, 998))
384	noerr(t, d.Dispatch())
385	if offer == nil || offer.ID() != 998 {
386		t.Fatal("offer not typed")
387	}
388	read, write, e := os.Pipe()
389	noerr(t, e)
390	defer read.Close()
391	noerr(t, offer.Receive("text/plain", int(write.Fd())))
392	request(t, p)
393	if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil {
394		t.Fatal("clipboard sent fd retained")
395	}
396	write.Close()
397	source.OnSend(func(mime string, fd *wlturbo.OwnedFD) {
398		if mime != "text/plain" {
399			t.Fatal(mime)
400		}
401		n, e := fd.Take()
402		noerr(t, e)
403		unix.Close(n)
404	})
405	received, e := os.Open("/dev/null")
406	noerr(t, e)
407	send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd()))
408	noerr(t, d.Dispatch())
409	received.Close()
410	noerr(t, offer.Destroy())
411	request(t, p)
412	noerr(t, source.Destroy())
413	request(t, p)
414	noerr(t, device.Release())
415	request(t, p)
416	// release is a version 4 request; the manager was negotiated at 3, so it
417	// must be refused locally instead of reaching the compositor.
418	if err := mgr.Release(); !errors.Is(err, wlturbo.ErrVersionTooLow) {
419		t.Fatalf("release at v3 = %v, want ErrVersionTooLow", err)
420	}
421	// Cursor shape: get a device for a wl_pointer and set a shape at an
422	// enter serial; check the exact wire arguments.
423	pointer := core.NewPointer(d.Context())
424	pointer.SetID(d.AllocateID())
425	d.Context().Register(pointer)
426	shapes := cursorshape.NewWpCursorShapeManager(d.Context())
427	bind(t, d, p, 8, cursorshape.WpCursorShapeManagerInterface, 2, 1, shapes)
428	shape, e := shapes.GetPointer(pointer)
429	noerr(t, e)
430	if id, op, b := request(t, p); id != shapes.ID() || op != 1 || binary.NativeEndian.Uint32(b) != shape.ID() || binary.NativeEndian.Uint32(b[4:]) != pointer.ID() {
431		t.Fatalf("get_pointer wire id=%d op=%d %x", id, op, b)
432	}
433	noerr(t, shape.SetShape(42, cursorshape.SHAPE_POINTER))
434	if id, op, b := request(t, p); id != shape.ID() || op != 1 || binary.NativeEndian.Uint32(b) != 42 || binary.NativeEndian.Uint32(b[4:]) != 4 {
435		t.Fatalf("set_shape wire id=%d op=%d %x", id, op, b)
436	}
437	noerr(t, shape.Destroy())
438	request(t, p)
439	noerr(t, shapes.Destroy())
440	request(t, p)
441
442}
443
444func TestLiveFeedback(t *testing.T) {
445	if os.Getenv("WLTURBO_LIVE") != "1" {
446		t.Skip("set WLTURBO_LIVE=1")
447	}
448	d, e := wlturbo.Connect("")
449	noerr(t, e)
450	defer d.Close()
451	noerr(t, d.Roundtrip())
452	for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface, cursorshape.WpCursorShapeManagerInterface} {
453		g, ok := d.Registry().FindGlobal(iface)
454		if !ok {
455			t.Logf("%s absent", iface)
456			continue
457		}
458		v := g.Version
459		cap := uint32(1)
460		if iface == xdgshell.XdgWmBaseInterface {
461			cap = 7
462		}
463		if iface == linuxdmabuf.LinuxDmabufInterface {
464			cap = 4
465		}
466		if v > cap {
467			v = cap
468		}
469		t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version)
470		if iface != linuxdmabuf.LinuxDmabufInterface {
471			continue
472		}
473		if v < 4 {
474			t.Fatal("dmabuf feedback requires v4")
475		}
476		dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
477		negotiated, err := d.Registry().BindNegotiated(iface, 4, dm)
478		noerr(t, err)
479		if negotiated != v {
480			t.Fatalf("dmabuf negotiated=%d want=%d", negotiated, v)
481		}
482		fb, e := dm.GetDefaultFeedback()
483		noerr(t, e)
484		done := false
485		fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) })
486		fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
487			entries, err := linuxdmabuf.ReadFormatTable(fd, size)
488			if err != nil {
489				t.Errorf("table: %v", err)
490			}
491			t.Logf("format-table entries=%d", len(entries))
492		})
493		fb.OnDone(func() { done = true })
494		for i := 0; i < 100 && !done; i++ {
495			noerr(t, d.Dispatch())
496		}
497		if !done {
498			t.Fatal("no feedback done")
499		}
500		noerr(t, fb.Destroy())
501		noerr(t, dm.Destroy())
502	}
503}

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