v0.7.0 f00b1b1

feasibility_test.go RawHistoryFolder

1package e2e_test
2
3import (
4	"context"
5	"os"
6	"os/exec"
7	"path/filepath"
8	"strings"
9	"testing"
10	"time"
11
12	"github.com/bnema/purego-libwayland/protocol/wayland"
13	"github.com/bnema/purego-libwayland/server"
14	"github.com/bnema/wlturbo"
15	"golang.org/x/sys/unix"
16)
17
18// startServer runs a display with a wl_compositor global on a private socket.
19// commits counts wl_surface.commit requests received by Go handlers.
20func startServer(t *testing.T, captured ...chan *server.Resource) (socket string, d *server.Display, commits chan uint32) {
21	t.Helper()
22	d, err := server.NewDisplay()
23	if err != nil {
24		t.Fatal(err)
25	}
26	socket = filepath.Join(t.TempDir(), "wayland-test")
27	// libwayland takes ownership of the listening fd.
28	fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
29	if err != nil {
30		t.Fatal(err)
31	}
32	if err := unix.Bind(fd, &unix.SockaddrUnix{Name: socket}); err != nil {
33		t.Fatal(err)
34	}
35	if err := unix.Listen(fd, 128); err != nil {
36		t.Fatal(err)
37	}
38	if err := d.AddSocketFD(fd); err != nil {
39		t.Fatal(err)
40	}
41
42	commits = make(chan uint32, 16)
43	var serial uint32
44	surface := &surfaceHandler{commits: commits, serial: &serial, t: t}
45	compositor := &compositorHandler{surface: surface, t: t}
46	if len(captured) > 0 {
47		compositor.captured = captured[0]
48	}
49	err = wayland.NewCompositorGlobal(d, 1, func(c server.Client, version, id uint32) {
50		if _, err := wayland.NewCompositor(c, int32(version), id, compositor); err != nil {
51			t.Error(err)
52		}
53	})
54	if err != nil {
55		t.Fatal(err)
56	}
57
58	ctx, cancel := context.WithCancel(context.Background())
59	done := make(chan error, 1)
60	go func() { done <- d.Run(ctx) }()
61	t.Cleanup(func() {
62		cancel()
63		select {
64		case err := <-done:
65			if err != nil {
66				t.Error(err)
67			}
68		case <-time.After(3 * time.Second):
69			t.Fatal("server did not stop")
70		}
71	})
72	return socket, d, commits
73}
74
75// Gates 1 and 2: the global exists and wayland-info lists it.
76func TestWaylandInfoListsCompositor(t *testing.T) {
77	if _, err := exec.LookPath("wayland-info"); err != nil {
78		t.Skip("wayland-info not installed")
79	}
80	socket, _, _ := startServer(t)
81	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
82	defer cancel()
83	cmd := exec.CommandContext(ctx, "wayland-info")
84	cmd.Env = []string{"XDG_RUNTIME_DIR=" + filepath.Dir(socket), "WAYLAND_DISPLAY=" + filepath.Base(socket)}
85	out, err := cmd.CombinedOutput()
86	if err != nil {
87		t.Fatalf("wayland-info: %v\n%s", err, out)
88	}
89	if !strings.Contains(string(out), "wl_compositor") {
90		t.Fatalf("wl_compositor missing from wayland-info output:\n%s", out)
91	}
92}
93
94// Gates 3 and 4: requests reach Go handlers, events reach the client, and a
95// disconnect frees every resource. Run repeatedly and under -race.
96func TestRequestEventRoundtripAndDisconnect(t *testing.T) {
97	socket, d, commits := startServer(t)
98
99	for round := range 20 {
100		c, err := wlturbo.Connect(socket)
101		if err != nil {
102			t.Fatal(err)
103		}
104		if err := c.Roundtrip(); err != nil {
105			t.Fatal(err)
106		}
107		g, ok := c.Registry().FindGlobal("wl_compositor")
108		if !ok {
109			t.Fatal("wl_compositor not advertised")
110		}
111		comp, err := c.Registry().BindID(g.Name, g.Interface, 1)
112		if err != nil {
113			t.Fatal(err)
114		}
115		surf := c.AllocateID()
116		must(t, c.SendRequest(comp, uint16(wayland.CompositorRequestCreateSurface), surf))
117		cb := c.AllocateID()
118		gotDone := make(chan uint32, 1)
119		cbProxy := &doneProxy{done: gotDone}
120		cbProxy.SetID(cb)
121		c.Context().Register(cbProxy)
122		must(t, c.SendRequest(surf, uint16(wayland.SurfaceRequestFrame), cb))
123		must(t, c.SendRequest(surf, uint16(wayland.SurfaceRequestCommit)))
124		must(t, c.Roundtrip())
125
126		select {
127		case id := <-commits:
128			if id != surf {
129				t.Fatalf("commit on %d, want %d", id, surf)
130			}
131		case <-time.After(2 * time.Second):
132			t.Fatal("commit never reached the Go handler")
133		}
134		select {
135		case serial := <-gotDone:
136			if serial != uint32(round+1) {
137				t.Fatalf("callback serial %d, want %d", serial, round+1)
138			}
139		default:
140			t.Fatal("wl_callback.done never reached the client")
141		}
142		_ = c.Close()
143
144		// libwayland frees the client's resources once it notices the hangup.
145		deadline := time.Now().Add(2 * time.Second)
146		for {
147			var n int
148			if !d.Do(func() { n = server.LiveResources() }) {
149				t.Fatal("display stopped")
150			}
151			if n == 0 {
152				break
153			}
154			if time.Now().After(deadline) {
155				t.Fatalf("round %d: %d resources still alive after disconnect", round, n)
156			}
157			time.Sleep(5 * time.Millisecond)
158		}
159	}
160}
161
162// A resource retained by Go must be harmless after libwayland destroys its client.
163func TestPostEventAfterDisconnect(t *testing.T) {
164	captured := make(chan *server.Resource, 1)
165	socket, d, _ := startServer(t, captured)
166	c, err := wlturbo.Connect(socket)
167	if err != nil {
168		t.Fatal(err)
169	}
170	if err := c.Roundtrip(); err != nil {
171		t.Fatal(err)
172	}
173	// Create a surface retained by the server after the client disconnects.
174	g, ok := c.Registry().FindGlobal("wl_compositor")
175	if !ok {
176		t.Fatal("wl_compositor not advertised")
177	}
178	comp, err := c.Registry().BindID(g.Name, g.Interface, 1)
179	if err != nil {
180		t.Fatal(err)
181	}
182	surf := c.AllocateID()
183	must(t, c.SendRequest(comp, uint16(wayland.CompositorRequestCreateSurface), surf))
184	if err := c.Roundtrip(); err != nil {
185		t.Fatal(err)
186	}
187	stale := <-captured
188	_ = c.Close()
189	deadline := time.Now().Add(2 * time.Second)
190	for {
191		var n int
192		if !d.Do(func() { n = server.LiveResources() }) {
193			t.Fatal("display stopped")
194		}
195		if n == 0 {
196			break
197		}
198		if time.Now().After(deadline) {
199			t.Fatalf("%d resources still alive", n)
200		}
201		time.Sleep(5 * time.Millisecond)
202	}
203	if !d.Do(func() {
204		if stale == nil {
205			t.Error("resource was not created")
206			return
207		}
208		if stale.Alive() {
209			t.Error("resource still alive")
210		}
211		stale.PostEvent(0)
212		stale.PostError(0, "gone")
213		if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) {
214			t.Error("destroyed resource returned nonzero metadata")
215		}
216	}) {
217		t.Fatal("display stopped")
218	}
219}
220
221// doneProxy records wl_callback.done.
222type doneProxy struct {
223	wlturbo.BaseProxy
224	done chan uint32
225}
226
227func (p *doneProxy) Dispatch(e *wlturbo.Event) {
228	if e.Opcode == uint16(wayland.CallbackEventDone) {
229		p.done <- e.Uint32()
230	}
231}
232
233func must(t *testing.T, err error) {
234	t.Helper()
235	if err != nil {
236		t.Fatal(err)
237	}
238}
239
240type compositorHandler struct {
241	surface  *surfaceHandler
242	t        *testing.T
243	captured chan *server.Resource
244}
245
246func (h *compositorHandler) CreateSurface(self *wayland.Compositor, id uint32) {
247	r, err := wayland.NewSurface(self.Client(), self.Version(), id, h.surface)
248	if err != nil {
249		h.t.Error(err)
250		return
251	}
252	if h.captured != nil {
253		h.captured <- r.Resource
254	}
255}
256func (h *compositorHandler) CreateRegion(self *wayland.Compositor, id uint32) {
257	if _, err := wayland.NewRegion(self.Client(), self.Version(), id, regionHandler{}); err != nil {
258		h.t.Error(err)
259	}
260}
261func (*compositorHandler) Release(*wayland.Compositor) {}
262
263type regionHandler struct{}
264
265func (regionHandler) Destroy(*wayland.Region)                              {}
266func (regionHandler) Add(*wayland.Region, int32, int32, int32, int32)      {}
267func (regionHandler) Subtract(*wayland.Region, int32, int32, int32, int32) {}
268
269type surfaceHandler struct {
270	commits chan uint32
271	serial  *uint32
272	t       *testing.T
273}
274
275func (*surfaceHandler) Destroy(*wayland.Surface)                               {}
276func (*surfaceHandler) Attach(*wayland.Surface, *wayland.Buffer, int32, int32) {}
277func (*surfaceHandler) Damage(*wayland.Surface, int32, int32, int32, int32)    {}
278func (h *surfaceHandler) Frame(self *wayland.Surface, id uint32) {
279	cb, err := wayland.NewCallback(self.Client(), 1, id, nil)
280	if err != nil {
281		h.t.Error(err)
282		return
283	}
284	*h.serial++
285	cb.SendDone(*h.serial)
286	cb.Destroy()
287}
288func (*surfaceHandler) SetOpaqueRegion(*wayland.Surface, *wayland.Region)         {}
289func (*surfaceHandler) SetInputRegion(*wayland.Surface, *wayland.Region)          {}
290func (h *surfaceHandler) Commit(self *wayland.Surface)                            { h.commits <- self.ID() }
291func (*surfaceHandler) SetBufferTransform(*wayland.Surface, int32)                {}
292func (*surfaceHandler) SetBufferScale(*wayland.Surface, int32)                    {}
293func (*surfaceHandler) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
294func (*surfaceHandler) Offset(*wayland.Surface, int32, int32)                     {}
295func (*surfaceHandler) GetRelease(*wayland.Surface, uint32)                       {}
296
297// The client PID comes from the socket credentials; in-process clients share ours.
298func TestClientPID(t *testing.T) {
299	captured := make(chan *server.Resource, 1)
300	socket, d, _ := startServer(t, captured)
301	c, err := wlturbo.Connect(socket)
302	if err != nil {
303		t.Fatal(err)
304	}
305	defer c.Close()
306	must(t, c.Roundtrip())
307	g, ok := c.Registry().FindGlobal("wl_compositor")
308	if !ok {
309		t.Fatal("wl_compositor not advertised")
310	}
311	comp, err := c.Registry().BindID(g.Name, g.Interface, 1)
312	must(t, err)
313	must(t, c.SendRequest(comp, uint16(wayland.CompositorRequestCreateSurface), c.AllocateID()))
314	must(t, c.Roundtrip())
315	r := receive(t, captured)
316	pid := make(chan int, 1)
317	if !d.Do(func() { pid <- r.Client().PID() }) {
318		t.Fatal("display stopped")
319	}
320	if got := <-pid; got != os.Getpid() {
321		t.Fatalf("pid %d, want %d", got, os.Getpid())
322	}
323}
324
325// A removed global disappears from the registry of new clients.
326func TestGlobalRemove(t *testing.T) {
327	socket, d, _ := startServer(t)
328	var g *server.Global
329	added := make(chan error, 1)
330	if !d.Do(func() {
331		var err error
332		g, err = d.AddGlobal(wayland.OutputInterface, 4, func(server.Client, uint32, uint32) {})
333		added <- err
334	}) {
335		t.Fatal("display stopped")
336	}
337	must(t, <-added)
338	has := func() bool {
339		c, err := wlturbo.Connect(socket)
340		must(t, err)
341		defer c.Close()
342		must(t, c.Roundtrip())
343		_, ok := c.Registry().FindGlobal("wl_output")
344		return ok
345	}
346	if !has() {
347		t.Fatal("wl_output not advertised")
348	}
349	if !d.Do(func() { g.Remove(); g.Remove() }) {
350		t.Fatal("display stopped")
351	}
352	if has() {
353		t.Fatal("wl_output still advertised after Remove")
354	}
355}

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 14

↑↓ moveenter openesc close