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}