detached 1a6c6c9

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

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 1

↑↓ moveenter openesc close