v0.2.0 e178e0b

headless_surface_test.go RawHistoryFolder

1package neferclient_test
2
3import (
4	"context"
5	"runtime"
6	"testing"
7	"time"
8	"unsafe"
9
10	"github.com/stretchr/testify/mock"
11	"github.com/stretchr/testify/require"
12	"golang.org/x/sys/unix"
13
14	"github.com/bnema/neferclient"
15	neferclientmocks "github.com/bnema/neferclient/mocks"
16)
17
18func connectHeadless(t *testing.T) *neferclient.Conn {
19	t.Helper()
20	socket := startHeadless(t)
21	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
22	defer cancel()
23	for {
24		c, err := neferclient.Connect(ctx, socket)
25		if err == nil {
26			t.Cleanup(func() { _ = c.Close() })
27			for len(c.Outputs()) == 0 { // the virtual output may arrive late
28				select {
29				case <-c.Wake():
30					require.NoError(t, c.Dispatch(nil))
31				case <-ctx.Done():
32					t.Fatal("no output announced")
33				}
34			}
35			return c
36		}
37		if ctx.Err() != nil {
38			t.Fatal(err)
39		}
40		time.Sleep(5 * time.Millisecond)
41	}
42}
43
44func TestHeadlessLayerSurface(t *testing.T) {
45	c := connectHeadless(t)
46	s, err := c.NewLayerSurface(neferclient.LayerConfig{
47		Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
48		Width: 200, Height: 50,
49	})
50	require.NoError(t, err)
51
52	var configured, feedback bool
53	var cw, ch int32
54	h := neferclientmocks.NewMockHandler(t)
55	h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(_ neferclient.SurfaceID, w, hh int32) {
56		configured, cw, ch = true, w, hh
57	}).Return().Maybe()
58	h.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { feedback = true }).Return().Maybe()
59	h.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()
60	dispatchUntil(t, c, h, func() bool { return configured && feedback })
61
62	require.Equal(t, int32(200), cw)
63	require.Equal(t, int32(50), ch)
64	fb := s.Feedback()
65	require.NotNil(t, fb)
66	require.NotZero(t, fb.MainDevice)
67	require.NotEmpty(t, fb.Formats)
68	t.Logf("main device %#x, %d formats", fb.MainDevice, len(fb.Formats))
69	pw, ph, err := s.PhysicalSize()
70	require.NoError(t, err)
71	require.Positive(t, pw)
72	require.Positive(t, ph)
73	require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 10, Height: 10}}))
74	require.NoError(t, s.Close())
75	require.NoError(t, c.Roundtrip())
76}
77
78func TestHeadlessLockUnlock(t *testing.T) {
79	c := connectHeadless(t)
80	l, err := c.Lock()
81	require.NoError(t, err)
82	require.Error(t, l.Unlock(), "unlock before locked must be refused")
83
84	var locked bool
85	var surfaces []*neferclient.Surface
86	for _, o := range c.Outputs() {
87		s, err := l.NewSurface(o.Global)
88		require.NoError(t, err)
89		surfaces = append(surfaces, s)
90	}
91	h := neferclientmocks.NewMockHandler(t)
92	h.EXPECT().Locked().Run(func() { locked = true }).Return().Maybe()
93	h.EXPECT().Configure(mock.Anything, mock.Anything, mock.Anything).Return().Maybe()
94	h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
95	h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
96	dispatchUntil(t, c, h, func() bool { return locked })
97	require.True(t, l.Locked())
98	for _, s := range surfaces {
99		w, hh, _ := s.Size()
100		t.Logf("lock surface %d: %dx%d", s.ID(), w, hh)
101	}
102	require.Error(t, l.Close(), "a locked session is not closed, only unlocked")
103	require.NoError(t, l.Unlock())
104	require.False(t, l.Locked())
105	for _, s := range surfaces {
106		require.NoError(t, s.Close())
107	}
108	require.NoError(t, c.Roundtrip())
109}
110
111// TestHeadlessSeat binds the seat against a real compositor and drives it
112// through a surface: the seat bind, capability handling and cursor request
113// must not fail, whatever devices the headless backend offers.
114func TestHeadlessSeat(t *testing.T) {
115	c := connectHeadless(t)
116	s, err := c.NewLayerSurface(neferclient.LayerConfig{
117		Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
118		Width: 100, Height: 40, Keyboard: neferclient.KeyboardOnDemand,
119	})
120	require.NoError(t, err)
121	seat := c.Seat()
122	require.NotNil(t, seat)
123	seat.SetSecret(neferclient.NewSecretBuffer(16))
124	seat.SetSecret(nil)
125
126	var configured bool
127	h := neferclientmocks.NewMockHandler(t)
128	h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
129	h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
130	h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
131	h.EXPECT().Pointer(mock.Anything).Return().Maybe()
132	h.EXPECT().Key(mock.Anything).Return().Maybe()
133	h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return().Maybe()
134	h.EXPECT().SecretChanged(mock.Anything).Return().Maybe()
135	dispatchUntil(t, c, h, func() bool { return configured })
136	require.NoError(t, c.Roundtrip())
137	require.NoError(t, c.Dispatch(h))
138	require.NoError(t, seat.SetCursor(neferclient.CursorPointer))
139	require.NoError(t, s.Close())
140}
141
142// newUdmabuf returns a linear DMA-BUF made from a udmabuf; the test is skipped
143// when the machine lacks /dev/udmabuf.
144func newUdmabuf(t *testing.T, w, h int) int {
145	t.Helper()
146	size := (w*h*4 + 4095) &^ 4095
147	mem, err := unix.MemfdCreate("udmabuf", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
148	require.NoError(t, err)
149	t.Cleanup(func() { _ = unix.Close(mem) })
150	require.NoError(t, unix.Ftruncate(mem, int64(size)))
151	_, err = unix.FcntlInt(uintptr(mem), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK)
152	require.NoError(t, err)
153	dev, err := unix.Open("/dev/udmabuf", unix.O_RDWR|unix.O_CLOEXEC, 0)
154	if err != nil {
155		t.Skipf("no udmabuf: %v", err)
156	}
157	defer unix.Close(dev)
158	req := struct {
159		memfd, flags uint32
160		offset, size uint64
161	}{memfd: uint32(mem), size: uint64(size)}
162	fd, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(dev), 0x40187542, uintptr(unsafe.Pointer(&req)))
163	if errno != 0 {
164		t.Skipf("UDMABUF_CREATE: %v", errno)
165	}
166	t.Cleanup(func() { _ = unix.Close(int(fd)) })
167	return int(fd)
168}
169
170// newSyncobjFD returns an exported DRM syncobj for use as a timeline and a
171// function that signals one of its points; the test is skipped when there is
172// no render node.
173func newSyncobjFD(t *testing.T) (fd int, signal func(point uint64)) {
174	t.Helper()
175	dev, err := unix.Open("/dev/dri/renderD128", unix.O_RDWR|unix.O_CLOEXEC, 0)
176	if err != nil {
177		t.Skipf("no render node: %v", err)
178	}
179	t.Cleanup(func() { _ = unix.Close(dev) })
180	create := struct{ handle, flags uint32 }{} // struct drm_syncobj_create
181	if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(dev), 0xC00864BF, uintptr(unsafe.Pointer(&create))); errno != 0 {
182		t.Skipf("SYNCOBJ_CREATE: %v", errno)
183	}
184	exp := struct { // struct drm_syncobj_handle, 24 bytes
185		handle, flags uint32
186		fd, pad       int32
187		point         uint64
188	}{handle: create.handle, fd: -1}
189	if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(dev), 0xC01864C1, uintptr(unsafe.Pointer(&exp))); errno != 0 {
190		t.Skipf("SYNCOBJ_HANDLE_TO_FD: %v", errno)
191	}
192	t.Cleanup(func() { _ = unix.Close(int(exp.fd)) })
193	signal = func(point uint64) {
194		t.Helper()
195		// The kernel reads these through addresses stored in arg. Nothing
196		// between taking them and the syscall can move the stack, and
197		// KeepAlive holds them until it returns.
198		handles, points := [1]uint32{create.handle}, [1]uint64{point}
199		arg := struct { // struct drm_syncobj_timeline_array, 24 bytes
200			handles, points uint64
201			count, flags    uint32
202		}{uint64(uintptr(unsafe.Pointer(&handles))), uint64(uintptr(unsafe.Pointer(&points))), 1, 0}
203		_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(dev), 0xC01864CD, uintptr(unsafe.Pointer(&arg)))
204		runtime.KeepAlive(&handles)
205		runtime.KeepAlive(&points)
206		if errno != 0 {
207			t.Fatalf("SYNCOBJ_TIMELINE_SIGNAL: %v", errno)
208		}
209	}
210	return int(exp.fd), signal
211}
212
213// TestHeadlessReconfigureWhilePending drops every import of a mapped surface
214// while its frame is pending, imports again and presents again: the compositor
215// must raise no protocol error (the connection stays usable) and the frame
216// still arrives. The first commit's objects are destroyed before its acquire
217// point is signalled: a compositor that waits for acquire points still holds
218// that commit unapplied at that moment. Headless NeferWL applies commits
219// without waiting, so there it only shows the requests are accepted.
220func TestHeadlessReconfigureWhilePending(t *testing.T) {
221	c := connectHeadless(t)
222	const w, h = 200, 50
223	s, err := c.NewLayerSurface(neferclient.LayerConfig{
224		Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft, Width: w, Height: h,
225	})
226	require.NoError(t, err)
227	var configured, feedback bool
228	var frames int
229	hd := neferclientmocks.NewMockHandler(t)
230	hd.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
231	hd.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { feedback = true }).Return().Maybe()
232	hd.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()
233	hd.EXPECT().Frame(s.ID()).Run(func(neferclient.SurfaceID) { frames++ }).Return().Maybe()
234	dispatchUntil(t, c, hd, func() bool { return configured && feedback })
235	pw, ph, err := s.PhysicalSize()
236	require.NoError(t, err)
237
238	dmabuf := newUdmabuf(t, int(pw), int(ph))
239	// Separate acquire and release syncobjs keep their points independent.
240	acquire, signal := newSyncobjFD(t)
241	release, _ := newSyncobjFD(t)
242	imports := func() {
243		t.Helper()
244		require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: pw, Height: ph, FourCC: 0x34325258, PlaneCount: 1,
245			Planes: [4]neferclient.Plane{{FD: dmabuf, Stride: uint32(pw) * 4}}}))
246		require.NoError(t, s.ImportTimeline(1, acquire))
247		require.NoError(t, s.ImportTimeline(2, release))
248	}
249	present := func(point uint64) {
250		t.Helper()
251		require.NoError(t, s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2,
252			AcquirePoint: point, ReleasePoint: point, Opaque: true}))
253	}
254
255	imports()
256	present(1) // acquire point 1 is not signalled yet
257	// No Dispatch ran since the commit: its frame is pending for the library.
258	require.ErrorContains(t, s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}), "frame callback pending")
259	require.NoError(t, s.DestroyImports())
260	require.NoError(t, c.Roundtrip()) // a protocol error would fail here
261	signal(1)                         // lets a waiting compositor apply the commit
262	dispatchUntil(t, c, hd, func() bool { return frames == 1 })
263
264	imports()
265	signal(2)
266	present(2)
267	dispatchUntil(t, c, hd, func() bool { return frames == 2 })
268	require.NoError(t, s.DestroyImports())
269	require.NoError(t, s.Close())
270	require.NoError(t, c.Roundtrip())
271	require.NoError(t, c.Dispatch(hd))
272}

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 4

↑↓ moveenter openesc close