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}