1// Package testserver is a minimal in-process Wayland compositor for tests: a
2// real libwayland server (purego-libwayland) that speaks wl_output, wl_shm,
3// ext-image-capture-source-v1, ext-image-copy-capture-v1, ext-workspace-v1 and
4// NeferWL's capture extension. It is a protocol peer, not a substitute for a
5// project type: clients connect to its socket like to any compositor.
6//
7// All server state is touched only on the display goroutine: handlers run
8// there, and tests reach it through Do.
9package testserver
10
11import (
12 "context"
13 "os"
14 "path/filepath"
15 "testing"
16
17 "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
18 "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
19 "github.com/bnema/purego-libwayland/protocol/extworkspace"
20 "github.com/bnema/purego-libwayland/protocol/wayland"
21 "github.com/bnema/purego-libwayland/server"
22 "golang.org/x/sys/unix"
23
24 "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
25)
26
27// Token is the exclusion token the server hands out.
28const Token = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
29
30// wl_shm formats.
31const (
32 ARGB8888 = 0
33 XRGB8888 = 1
34)
35
36// OutputSpec describes one wl_output. The capture buffer of an output source
37// is its mode size.
38type OutputSpec struct {
39 Name string
40 Width, Height uint32
41 Scale int32
42 Version uint32 // 0 selects 4
43 // LogicalWidth/Height, when set, are announced through xdg-output.
44 LogicalWidth, LogicalHeight int32
45}
46
47// WorkspaceSpec describes one ext-workspace handle; all of them live in one
48// group that holds every output.
49type WorkspaceSpec struct {
50 ID string // empty: a temporary workspace, no id event
51 Name string
52 Output string // the output whose buffer size a workspace source has
53 Active bool
54 Hidden bool
55 // Frame is the output-local logical rectangle (x, y, width, height) that
56 // neferwl_workspace_frame_v1 reports; zero reports nothing.
57 Frame [4]int32
58}
59
60// Config selects what the server offers.
61type Config struct {
62 Outputs []OutputSpec // default: one 8x4 output "TEST-1"
63 // NoCopyCapture leaves out ext-image-copy-capture and the output source.
64 NoCopyCapture bool
65 Formats []uint32 // shm formats announced, default XRGB8888
66 // NoShmFormat announces size and done but no shm_format.
67 NoShmFormat bool
68 // Hold keeps every captured frame pending until Release.
69 Hold bool
70 // HoldAfterFirst answers the first frame of each session at once and holds
71 // the following ones until Release, like a compositor that waits for
72 // damage on a static screen.
73 HoldAfterFirst bool
74 // Workspaces enables ext-workspace-v1 when non-nil (possibly empty).
75 Workspaces []WorkspaceSpec
76 HasWorkspaces bool
77 // NeferwlSource and NeferwlExclusion enable the extension's two globals.
78 NeferwlSource bool
79 NeferwlExclusion bool
80 // ExclusionFail answers get_exclusion with failed(ExclusionFailReason-1)
81 // when non-zero.
82 ExclusionFail uint32
83 // AttachFail answers attach_surface with failed(AttachFail-1) when non-zero.
84 AttachFail uint32
85 // AttachSilent never answers attach_surface.
86 AttachSilent bool
87 // StopAtCreate answers every create_session with stopped and no
88 // constraints, like a compositor that refuses the client.
89 StopAtCreate bool
90 // Transform is reported by every ready frame (wl_output.transform).
91 Transform uint32
92 // FailFrames answers that many capture requests with failed(FailReason).
93 FailFrames int
94 FailReason uint32
95}
96
97// Stats are counters observed by tests.
98type Stats struct {
99 Sessions, Frames, Captures, Ready int
100 DestroyedSessions int
101 Buffers, DestroyedBuffers int
102 LastSource string // "output", "region", "workspace"
103 LastRegion [4]int32
104 Exclusions int
105 Attached []string
106 DestroyedExclusions int
107 WorkspaceFrames int // neferwl_workspace_frame_v1 objects created
108 DestroyedWorkspaceFrames int
109 DestroyedXdgOutputs int
110}
111
112// Server is a running test compositor.
113type Server struct {
114 // Path is the socket path for wayland.New.
115 Path string
116
117 d *server.Display
118 cfg Config
119
120 outputs []*output
121 bufs map[*server.Resource]*buffer
122 sources map[*server.Resource]*source
123 sessions []*session
124 workspaces []*workspace
125 wsMgrs []*wsManager
126 layers []*server.Resource
127 frames []*frameObject
128 stats Stats
129 held []*frame
130 seq uint32
131}
132
133type output struct {
134 spec OutputSpec
135 global *server.Global
136 res []*wayland.Output
137 removed bool
138}
139
140// Start runs a compositor on a fresh socket until the test ends.
141func Start(t testing.TB, cfg Config) *Server {
142 t.Helper()
143 if len(cfg.Outputs) == 0 {
144 cfg.Outputs = []OutputSpec{{Name: "TEST-1", Width: 8, Height: 4, Scale: 1}}
145 }
146 if len(cfg.Formats) == 0 {
147 cfg.Formats = []uint32{XRGB8888}
148 }
149 d, err := server.NewDisplay()
150 if err != nil {
151 // NEFERCAP_REQUIRE_WAYLAND=1 (set by make test and make race) turns a
152 // missing libwayland-server into a failure, so the suite cannot pass
153 // without its protocol tests.
154 if os.Getenv("NEFERCAP_REQUIRE_WAYLAND") == "1" {
155 t.Fatalf("libwayland-server unavailable: %v", err)
156 }
157 t.Skipf("libwayland-server unavailable: %v", err)
158 }
159 dir, err := filepath.EvalSymlinks(t.TempDir())
160 if err != nil {
161 t.Fatal(err)
162 }
163 path := filepath.Join(dir, "wl-test")
164 fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
165 if err != nil {
166 t.Fatal(err)
167 }
168 if err := unix.Bind(fd, &unix.SockaddrUnix{Name: path}); err != nil {
169 t.Fatal(err)
170 }
171 if err := unix.Listen(fd, 16); err != nil {
172 t.Fatal(err)
173 }
174 if err := d.AddSocketFD(fd); err != nil { // libwayland owns fd now
175 t.Fatal(err)
176 }
177 s := &Server{Path: path, d: d, cfg: cfg, bufs: map[*server.Resource]*buffer{}, sources: map[*server.Resource]*source{}}
178 ctx, cancel := context.WithCancel(context.Background())
179 done := make(chan error, 1)
180 go func() { done <- d.Run(ctx) }()
181 if !d.Do(func() { s.setup(t) }) {
182 t.Fatal("test compositor stopped")
183 }
184 t.Cleanup(func() {
185 cancel()
186 <-done
187 })
188 return s
189}
190
191// Do runs f on the display goroutine, where server state may be touched.
192func (s *Server) Do(f func()) {
193 if !s.d.Do(f) {
194 // stopped: nothing to observe
195 return
196 }
197}
198
199// Stats returns a snapshot of the counters.
200func (s *Server) Stats() Stats {
201 var st Stats
202 s.Do(func() {
203 st = s.stats
204 st.Attached = append([]string(nil), s.stats.Attached...)
205 })
206 return st
207}
208
209func (s *Server) setup(t testing.TB) {
210 must := func(err error) {
211 if err != nil {
212 t.Error(err)
213 }
214 }
215 for _, spec := range s.cfg.Outputs {
216 s.addOutput(spec, must)
217 }
218 must(wayland.NewShmGlobal(s.d, 1, func(c server.Client, v, id uint32) {
219 shm, err := wayland.NewShm(c, int32(v), id, shmHandler{s})
220 if err != nil {
221 t.Error(err)
222 return
223 }
224 for _, f := range []uint32{ARGB8888, XRGB8888} {
225 shm.SendFormat(f)
226 }
227 }))
228 if s.xdgOutputWanted() {
229 s.addXdgOutput(must)
230 }
231 must(wayland.NewCompositorGlobal(s.d, 4, func(c server.Client, v, id uint32) {
232 if _, err := wayland.NewCompositor(c, int32(v), id, compositorHandler{}); err != nil {
233 t.Error(err)
234 }
235 }))
236 if !s.cfg.NoCopyCapture {
237 must(extimagecapturesource.NewExtOutputImageCaptureSourceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
238 if _, err := extimagecapturesource.NewExtOutputImageCaptureSourceManagerV1(c, int32(v), id, outputSourceManager{s}); err != nil {
239 t.Error(err)
240 }
241 }))
242 must(extimagecopycapture.NewExtImageCopyCaptureManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
243 if _, err := extimagecopycapture.NewExtImageCopyCaptureManagerV1(c, int32(v), id, copyManager{s}); err != nil {
244 t.Error(err)
245 }
246 }))
247 }
248 if s.cfg.HasWorkspaces {
249 for _, w := range s.cfg.Workspaces {
250 s.workspaces = append(s.workspaces, &workspace{spec: w})
251 }
252 must(extworkspace.NewExtWorkspaceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
253 m := &wsManager{s: s}
254 res, err := extworkspace.NewExtWorkspaceManagerV1(c, int32(v), id, m)
255 if err != nil {
256 t.Error(err)
257 return
258 }
259 m.res = res
260 s.wsMgrs = append(s.wsMgrs, m)
261 m.announce(c)
262 }))
263 }
264 if s.cfg.NeferwlSource {
265 must(neferwl.NewNeferwlImageCaptureSourceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
266 if _, err := neferwl.NewNeferwlImageCaptureSourceManagerV1(c, int32(v), id, neferwlSources{s}); err != nil {
267 t.Error(err)
268 }
269 }))
270 }
271 if s.cfg.NeferwlExclusion {
272 must(neferwl.NewNeferwlCaptureExclusionManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
273 if _, err := neferwl.NewNeferwlCaptureExclusionManagerV1(c, int32(v), id, exclusionManager{s}); err != nil {
274 t.Error(err)
275 }
276 }))
277 }
278}
279
280type compositorHandler struct{}
281
282func (compositorHandler) CreateSurface(self *wayland.Compositor, id uint32) {
283 _, _ = wayland.NewSurface(self.Client(), 4, id, surfaceHandler{})
284}
285func (compositorHandler) CreateRegion(*wayland.Compositor, uint32) {}
286func (compositorHandler) Release(*wayland.Compositor) {}
287
288type surfaceHandler struct{}
289
290func (surfaceHandler) Destroy(*wayland.Surface) {}
291func (surfaceHandler) Attach(*wayland.Surface, *wayland.Buffer, int32, int32) {}
292func (surfaceHandler) Damage(*wayland.Surface, int32, int32, int32, int32) {}
293func (surfaceHandler) Frame(*wayland.Surface, uint32) {}
294func (surfaceHandler) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
295func (surfaceHandler) SetInputRegion(*wayland.Surface, *wayland.Region) {}
296func (surfaceHandler) Commit(*wayland.Surface) {}
297func (surfaceHandler) SetBufferTransform(*wayland.Surface, int32) {}
298func (surfaceHandler) SetBufferScale(*wayland.Surface, int32) {}
299func (surfaceHandler) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
300func (surfaceHandler) Offset(*wayland.Surface, int32, int32) {}
301func (surfaceHandler) GetRelease(*wayland.Surface, uint32) {}