feat/freeze-key 753c87f

server.go RawHistoryFolder

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)                       {}

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 2

Tags 9

↑↓ moveenter openesc close