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}