1//go:build linux
2
3package protocol_test
4
5import (
6 "context"
7 "fmt"
8 "net"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "syscall"
14 "testing"
15 "time"
16
17 "github.com/bnema/wlturbo"
18 "github.com/bnema/wlturbo/protocol/core"
19 "github.com/bnema/wlturbo/protocol/drmsyncobj"
20 "github.com/bnema/wlturbo/protocol/fractionalscale"
21 "github.com/bnema/wlturbo/protocol/linuxdmabuf"
22 "github.com/bnema/wlturbo/protocol/viewporter"
23 "github.com/bnema/wlturbo/protocol/xdgshell"
24)
25
26func TestHeadlessNeferWL(t *testing.T) {
27 binary := os.Getenv("WLTURBO_HEADLESS")
28 if binary == "" {
29 t.Skip("set WLTURBO_HEADLESS to a NeferWL binary")
30 }
31 binary, e := filepath.Abs(binary)
32 noerr(t, e)
33 runtimeDir := filepath.Join(t.TempDir(), "run")
34 noerr(t, os.Mkdir(runtimeDir, 0700))
35 configDir := filepath.Join(t.TempDir(), "config")
36 noerr(t, os.Mkdir(configDir, 0700))
37 ctx, cancel := context.WithTimeout(context.Background(), 18*time.Second)
38 defer cancel()
39 cmd := exec.Command(binary, "--backend=headless", "--no-terminal", "--no-xwayland", "--size", "640x480", "--timeout", "20s")
40 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
41 cmd.Env = append(cleanDisplayEnv(os.Environ()), "XDG_RUNTIME_DIR="+runtimeDir, "XDG_CONFIG_HOME="+configDir)
42 log, e := os.Create(filepath.Join(t.TempDir(), "neferwl.log"))
43 noerr(t, e)
44 defer log.Close()
45 cmd.Stdout = log
46 cmd.Stderr = log
47 noerr(t, cmd.Start())
48 t.Cleanup(func() { _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); _ = cmd.Wait() })
49 // A failed startup must still terminate its process group and reap it.
50 socket := ""
51 for socket == "" {
52 if e := ctx.Err(); e != nil {
53 t.Fatalf("headless socket timeout: %v\n%s", e, headlessLog(log))
54 }
55 paths, _ := filepath.Glob(filepath.Join(runtimeDir, "wayland-*"))
56 for _, path := range paths {
57 info, e := os.Stat(path)
58 if e == nil && info.Mode()&os.ModeSocket != 0 {
59 socket = path
60 break
61 }
62 }
63 if socket == "" {
64 time.Sleep(20 * time.Millisecond)
65 }
66 }
67 conn, e := net.DialTimeout("unix", socket, time.Second)
68 noerr(t, e)
69 d, e := wlturbo.ConnectFromConn(conn)
70 noerr(t, e)
71 defer d.Close()
72 // Close the socket when the test deadline expires so blocking Dispatch exits.
73 stop := context.AfterFunc(ctx, func() { _ = d.Close() })
74 defer stop()
75 noerr(t, d.Roundtrip())
76 bind := func(iface string, supported uint32, proxy wlturbo.Proxy) uint32 {
77 t.Helper()
78 v, err := d.Registry().BindNegotiated(iface, supported, proxy)
79 if err != nil {
80 t.Fatalf("bind %s: %v\n%s", iface, err, headlessLog(log))
81 }
82 t.Logf("%s negotiated=%d", iface, v)
83 return v
84 }
85 compositor := core.NewCompositor(d.Context())
86 bind(core.CompositorInterface, 6, compositor)
87 wm := xdgshell.NewXdgWmBase(d.Context())
88 bind(xdgshell.XdgWmBaseInterface, 7, wm)
89 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
90 if bind(linuxdmabuf.LinuxDmabufInterface, 4, dm) < 4 {
91 t.Fatal("dmabuf feedback requires v4")
92 }
93 syncManager := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
94 bind(drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, syncManager)
95 dataManager := core.NewDataDeviceManager(d.Context())
96 bind(core.DataDeviceManagerInterface, 4, dataManager)
97 vp := viewporter.NewWpViewporter(d.Context())
98 bind(viewporter.WpViewporterInterface, 1, vp)
99 if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); ok {
100 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
101 v := bind(g.Interface, 1, fs)
102 if v != 1 {
103 t.Fatalf("fractional scale version %d", v)
104 }
105 noerr(t, fs.Destroy())
106 } else {
107 t.Log("fractional scale absent")
108 }
109 surface, e := compositor.CreateSurface()
110 noerr(t, e)
111 xs, e := wm.GetXdgSurface(surface)
112 noerr(t, e)
113 top, e := xs.GetToplevel()
114 noerr(t, e)
115 feedback, e := dm.GetSurfaceFeedback(surface)
116 noerr(t, e)
117 ss, e := syncManager.GetSurface(surface)
118 noerr(t, e)
119 configured, done, mainDevice, tableOK := false, false, false, false
120 top.OnConfigure(func(w, h int32, states []byte) { t.Logf("xdg_toplevel.configure %dx%d", w, h) })
121 xs.OnConfigure(func(serial uint32) {
122 noerr(t, xs.AckConfigure(serial))
123 configured = true
124 t.Logf("xdg_surface.configure ack=%d", serial)
125 })
126 feedback.OnMainDevice(func(b []byte) { mainDevice = len(b) == 8; t.Logf("main_device=%x", b) })
127 feedback.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
128 entries, e := linuxdmabuf.ReadFormatTable(fd, size)
129 if e != nil {
130 t.Errorf("format table: %v", e)
131 } else {
132 tableOK = len(entries) > 0
133 t.Logf("format-table entries=%d", len(entries))
134 }
135 })
136 feedback.OnDone(func() { done = true })
137 // Initial empty commit requests xdg configure; no buffer or window is shown.
138 noerr(t, surface.Commit())
139 for (!configured || !done) && ctx.Err() == nil {
140 if e := d.Dispatch(); e != nil {
141 t.Fatalf("dispatch: %v\n%s", e, headlessLog(log))
142 }
143 }
144 if !configured || !done || !mainDevice || !tableOK {
145 t.Fatalf("configure=%t feedback=%t main_device=%t table=%t: %v\n%s", configured, done, mainDevice, tableOK, ctx.Err(), headlessLog(log))
146 }
147 noerr(t, surface.Commit())
148 // Drop all child objects while their parents are still alive.
149 noerr(t, ss.Destroy())
150 noerr(t, feedback.Destroy())
151 noerr(t, top.Destroy())
152 noerr(t, xs.Destroy())
153 noerr(t, surface.Destroy())
154 noerr(t, vp.Destroy())
155 noerr(t, dataManager.Release())
156 noerr(t, syncManager.Destroy())
157 noerr(t, dm.Destroy())
158 noerr(t, wm.Destroy())
159 noerr(t, d.Close())
160}
161func cleanDisplayEnv(in []string) []string {
162 out := make([]string, 0, len(in))
163 for _, e := range in {
164 if strings.HasPrefix(e, "DISPLAY=") || strings.HasPrefix(e, "WAYLAND_DISPLAY=") || strings.HasPrefix(e, "XDG_RUNTIME_DIR=") || strings.HasPrefix(e, "XDG_CONFIG_HOME=") {
165 continue
166 }
167 out = append(out, e)
168 }
169 return out
170}
171func headlessLog(f *os.File) string {
172 b, e := os.ReadFile(f.Name())
173 if e != nil {
174 return fmt.Sprint(e)
175 }
176 if len(b) > 8192 {
177 b = b[len(b)-8192:]
178 }
179 return string(b)
180}