1package wayland
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
9 wlr "github.com/bnema/go-wayland-bindings/server/wlrscreencopy"
10 "github.com/bnema/neferwl/internal/logging"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/bnema/wlturbo"
13 "github.com/bnema/zerowrap"
14 "golang.org/x/sys/unix"
15)
16
17// admissionHarness runs a real Server with real protocol clients. All fields
18// except the server internals read through display.Do are test-goroutine only.
19type admissionHarness struct {
20 s *Server
21 dir string
22 requests chan ports.CaptureRequest
23 replies chan ports.CaptureDone
24 held []ports.CaptureRequest // dup'd descriptors handed to the test
25}
26
27type admissionClient struct {
28 c *wlturbo.Display
29 wlr uint32
30 output uint32
31 buf uint32
32 session uint32 // ext session, created on first use
33 extMgr uint32
34}
35
36// pendingCapture is one capture request and the events of its frame.
37type pendingCapture struct {
38 frame uint32
39 events *captureEvents
40 failed uint16 // opcode of the failed event
41 ext bool
42}
43
44func newAdmissionHarness(t *testing.T, queue int) *admissionHarness {
45 t.Helper()
46 return newAdmissionHarnessWith(t, queue, Options{CaptureAllow: allowStore(t, "*\n")}, logging.For(context.Background(), "wayland"))
47}
48
49// newAdmissionHarnessWith runs the server with extra options (RuntimeDir and
50// Outputs are the harness's) and a logger.
51func newAdmissionHarnessWith(t *testing.T, queue int, opts Options, log zerowrap.Logger) *admissionHarness {
52 t.Helper()
53 h := &admissionHarness{dir: t.TempDir(), requests: make(chan ports.CaptureRequest, queue), replies: make(chan ports.CaptureDone, maxCaptureInflight+2)}
54 // Registered first so it runs after Run stopped and the clients closed:
55 // close every dup'd descriptor, including the ones a failed test left in
56 // the queue or in held.
57 t.Cleanup(func() {
58 for len(h.requests) > 0 {
59 h.held = append(h.held, <-h.requests)
60 }
61 for _, r := range h.held {
62 _ = r.Dst.File.Close()
63 }
64 })
65 out := ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 2, Height: 2}, Width: 2, Height: 2, Scale: 1}}
66 opts.RuntimeDir, opts.Outputs = h.dir, out
67 s, err := New(opts, Channels{Captures: h.requests, Captured: h.replies}, log)
68 if err != nil {
69 t.Fatal(err)
70 }
71 h.s = s
72 ctx, cancel := context.WithCancel(context.Background())
73 done := make(chan error, 1)
74 go func() { done <- s.Run(ctx) }()
75 t.Cleanup(func() {
76 cancel()
77 if err := <-done; err != nil {
78 t.Errorf("Run: %v", err)
79 }
80 })
81 return h
82}
83
84func (h *admissionHarness) client(t *testing.T) *admissionClient {
85 t.Helper()
86 return h.clientOn(t, protocolClient(t, h.s, h.dir))
87}
88
89// clientOn binds the capture globals on an already connected client.
90func (h *admissionHarness) clientOn(t *testing.T, c *wlturbo.Display) *admissionClient {
91 t.Helper()
92 g, ok := c.Registry().FindGlobal("wl_output")
93 if !ok {
94 t.Fatal("output missing")
95 }
96 out, err := bindWireID(c, g.Name, g.Interface, 4)
97 if err != nil {
98 t.Fatal(err)
99 }
100 registerProtocol(t, c, out)
101 _, buf, fd := captureSmallBuffer(t, c)
102 t.Cleanup(func() { _ = unix.Close(fd) })
103 return &admissionClient{c: c, wlr: bindVersion(t, c, "zwlr_screencopy_manager_v1", 3), output: out, buf: buf}
104}
105
106func (a *admissionClient) newFrame() (uint32, *captureEvents) {
107 frame := a.c.AllocateID()
108 ev := &captureEvents{events: make(chan uint16, 8)}
109 ev.SetID(frame)
110 registerWireProxy(a.c, ev)
111 return frame, ev
112}
113
114// captureWlr asks for a wlr capture of the whole output.
115func (a *admissionClient) captureWlr(t *testing.T) pendingCapture {
116 t.Helper()
117 frame, ev := a.newFrame()
118 requestProtocol(t, a.c, a.wlr, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), a.output)
119 requestProtocol(t, a.c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, a.buf)
120 if err := a.c.Roundtrip(); err != nil {
121 t.Fatal(err)
122 }
123 return pendingCapture{frame: frame, events: ev, failed: uint16(wlr.ZwlrScreencopyFrameV1EventFailed)}
124}
125
126// captureExt asks for an ext capture. The session allows one live frame, so
127// the caller destroys the previous frame first.
128func (a *admissionClient) captureExt(t *testing.T) pendingCapture {
129 t.Helper()
130 if a.session == 0 {
131 a.extMgr, a.session, _ = captureTestExt(t, a.c, a.output)
132 }
133 frame, ev := a.newFrame()
134 requestProtocol(t, a.c, a.session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
135 requestProtocol(t, a.c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, a.buf)
136 requestProtocol(t, a.c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
137 if err := a.c.Roundtrip(); err != nil {
138 t.Fatal(err)
139 }
140 return pendingCapture{frame: frame, events: ev, failed: uint16(ext.ExtImageCopyCaptureFrameV1EventFailed), ext: true}
141}
142
143func (a *admissionClient) destroy(t *testing.T, p pendingCapture) {
144 t.Helper()
145 op := wlr.ZwlrScreencopyFrameV1RequestDestroy
146 if p.ext {
147 op = ext.ExtImageCopyCaptureFrameV1RequestDestroy
148 }
149 requestProtocol(t, a.c, p.frame, op)
150 if err := a.c.Roundtrip(); err != nil {
151 t.Fatal(err)
152 }
153}
154
155func (h *admissionHarness) inflight(t *testing.T) int {
156 t.Helper()
157 n := -1
158 if !h.s.display.Do(func() { n = len(h.s.captureInflight) }) {
159 t.Fatal("display stopped")
160 }
161 return n
162}
163
164func (h *admissionHarness) waitInflight(t *testing.T, want int) {
165 t.Helper()
166 deadline := time.Now().Add(2 * time.Second)
167 for h.inflight(t) != want {
168 if time.Now().After(deadline) {
169 t.Fatalf("inflight %d, want %d", h.inflight(t), want)
170 }
171 time.Sleep(time.Millisecond)
172 }
173}
174
175// accepted reports whether the capture reached the backend queue; a refused
176// capture is answered with failed and queues no request. Accepted requests
177// are kept in held so cleanup closes their descriptors.
178func (h *admissionHarness) accepted(t *testing.T, p pendingCapture) (ports.CaptureRequest, bool) {
179 t.Helper()
180 var last uint16
181 for len(p.events.events) > 0 {
182 last = <-p.events.events
183 }
184 select {
185 case req := <-h.requests:
186 if last == p.failed {
187 t.Fatal("failed frame queued a request")
188 }
189 h.held = append(h.held, req)
190 return req, true
191 default:
192 // A refused wlr frame fails at once; an ext frame waits for a
193 // retry (captureRetryFor) rather than ending the client's stream.
194 if last != p.failed && (!p.ext || last != 0) {
195 t.Fatalf("refused capture not failed: last event %d", last)
196 }
197 return ports.CaptureRequest{}, false
198 }
199}
200
201func (h *admissionHarness) complete(t *testing.T, req ports.CaptureRequest, remaining int) {
202 t.Helper()
203 _ = req.Dst.File.Close()
204 h.replies <- ports.CaptureDone{ID: req.ID, Output: req.Output, Time: time.Now()}
205 h.waitInflight(t, remaining)
206}
207
208// saturate fills the cap with captures whose frames are destroyed right away,
209// and returns their requests.
210func (h *admissionHarness) saturate(t *testing.T, a *admissionClient, capture func(*testing.T) pendingCapture) []ports.CaptureRequest {
211 t.Helper()
212 var reqs []ports.CaptureRequest
213 for i := 0; i < maxCaptureInflight; i++ {
214 p := capture(t)
215 req, ok := h.accepted(t, p)
216 if !ok {
217 t.Fatalf("capture %d refused below the cap", i)
218 }
219 reqs = append(reqs, req)
220 a.destroy(t, p)
221 }
222 if got := h.inflight(t); got != maxCaptureInflight {
223 t.Fatalf("inflight %d after destroying frames", got)
224 }
225 return reqs
226}
227
228func TestCaptureAdmissionSaturation(t *testing.T) {
229 h := newAdmissionHarness(t, maxCaptureInflight+2)
230 a := h.client(t)
231 var frames []pendingCapture
232 var reqs []ports.CaptureRequest
233 for i := 0; i < maxCaptureInflight; i++ {
234 p := a.captureWlr(t)
235 req, ok := h.accepted(t, p)
236 if !ok {
237 t.Fatalf("capture %d refused below the cap", i)
238 }
239 reqs, frames = append(reqs, req), append(frames, p)
240 }
241 if got := h.inflight(t); got != maxCaptureInflight {
242 t.Fatalf("inflight %d", got)
243 }
244 if _, ok := h.accepted(t, a.captureWlr(t)); ok {
245 t.Fatal("capture admitted over the cap")
246 }
247 // Destroying the client frames must not release the backend's credit.
248 for _, p := range frames {
249 a.destroy(t, p)
250 }
251 if got := h.inflight(t); got != maxCaptureInflight {
252 t.Fatalf("inflight %d after destroy", got)
253 }
254 if _, ok := h.accepted(t, a.captureWlr(t)); ok {
255 t.Fatal("destroying frames bypassed the cap")
256 }
257 // A backend completion for a destroyed frame restores exactly one credit.
258 h.complete(t, reqs[0], maxCaptureInflight-1)
259 req, ok := h.accepted(t, a.captureWlr(t))
260 if !ok {
261 t.Fatal("completion did not restore credit")
262 }
263 h.waitInflight(t, maxCaptureInflight)
264 if _, ok := h.accepted(t, a.captureWlr(t)); ok {
265 t.Fatal("cap exceeded after refill")
266 }
267 h.complete(t, req, maxCaptureInflight-1)
268 for i, r := range reqs[1:] {
269 h.complete(t, r, maxCaptureInflight-2-i)
270 }
271 h.waitInflight(t, 0)
272}
273
274func TestExtCaptureAdmissionSaturation(t *testing.T) {
275 h := newAdmissionHarness(t, maxCaptureInflight+2)
276 a := h.client(t)
277 reqs := h.saturate(t, a, a.captureExt)
278 // Every frame is destroyed, yet the cap still holds.
279 p := a.captureExt(t)
280 if _, ok := h.accepted(t, p); ok {
281 t.Fatal("destroyed ext frames bypassed the cap")
282 }
283 a.destroy(t, p)
284 h.complete(t, reqs[0], maxCaptureInflight-1)
285 p = a.captureExt(t)
286 req, ok := h.accepted(t, p)
287 if !ok {
288 t.Fatal("completion did not restore ext credit")
289 }
290 a.destroy(t, p)
291 h.waitInflight(t, maxCaptureInflight)
292 p = a.captureExt(t)
293 if _, ok := h.accepted(t, p); ok {
294 t.Fatal("cap exceeded after ext refill")
295 }
296 // A live refused frame would retry into the freed credit: drop it.
297 a.destroy(t, p)
298 h.complete(t, req, maxCaptureInflight-1)
299 for i, r := range reqs[1:] {
300 h.complete(t, r, maxCaptureInflight-2-i)
301 }
302 h.waitInflight(t, 0)
303}
304
305func TestCaptureAdmissionSurvivesDisconnect(t *testing.T) {
306 h := newAdmissionHarness(t, maxCaptureInflight+2)
307 a := h.client(t)
308 reqs := h.saturate(t, a, a.captureExt)
309 if err := a.c.Close(); err != nil {
310 t.Fatal(err)
311 }
312 // Wait until the display loop dropped the dead client's reply callbacks.
313 deadline := time.Now().Add(2 * time.Second)
314 for {
315 replies := -1
316 if !h.s.display.Do(func() { replies = len(h.s.captureReplies) }) {
317 t.Fatal("display stopped")
318 }
319 if replies == 0 {
320 break
321 }
322 if time.Now().After(deadline) {
323 t.Fatalf("%d reply callbacks retained after disconnect", replies)
324 }
325 time.Sleep(time.Millisecond)
326 }
327 if got := h.inflight(t); got != maxCaptureInflight {
328 t.Fatalf("inflight %d after disconnect", got)
329 }
330 // A new client cannot use the dead client's credits until completion.
331 b := h.client(t)
332 if _, ok := h.accepted(t, b.captureWlr(t)); ok {
333 t.Fatal("disconnect bypassed the cap")
334 }
335 h.complete(t, reqs[0], maxCaptureInflight-1)
336 req, ok := h.accepted(t, b.captureWlr(t))
337 if !ok {
338 t.Fatal("completion after disconnect did not restore credit")
339 }
340 h.complete(t, req, maxCaptureInflight-1)
341 for i, r := range reqs[1:] {
342 h.complete(t, r, maxCaptureInflight-2-i)
343 }
344 h.waitInflight(t, 0)
345}
346
347func TestCaptureAdmissionReleasedOnEnqueueFailure(t *testing.T) {
348 h := newAdmissionHarness(t, 1)
349 a := h.client(t)
350 // The first capture fills the one-slot queue and stays in flight.
351 a.captureWlr(t)
352 h.waitInflight(t, 1)
353 // The second is refused by the full queue and must not keep its credit.
354 p := a.captureWlr(t)
355 h.waitInflight(t, 1)
356 var last uint16
357 for len(p.events.events) > 0 {
358 last = <-p.events.events
359 }
360 if last != p.failed || len(h.requests) != 1 {
361 t.Fatalf("last event %d, queued %d; want failed and 1", last, len(h.requests))
362 }
363 req := <-h.requests
364 h.held = append(h.held, req)
365 h.complete(t, req, 0)
366 // With the queue drained and credit restored a new capture is accepted.
367 req, ok := h.accepted(t, a.captureWlr(t))
368 if !ok {
369 t.Fatal("capture refused after enqueue failure")
370 }
371 h.complete(t, req, 0)
372}