wayland_test.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package main_test
4
5 import (
6 "bufio"
7 "bytes"
8 "context"
9 "fmt"
10 "image"
11 "image/png"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "regexp"
16 "strings"
17 "sync"
18 "text/template"
19 "time"
20 )
21
22 type WaylandTestDriver struct {
23 driverBase
24
25 runtimeDir string
26 socket string
27 display string
28 }
29
30 // No bars or anything fancy. Just a white background with our dimensions.
31 var tmplSwayConfig = template.Must(template.New("").Parse(`
32 output * bg #FFFFFF solid_color
33 output * mode {{.Width}}x{{.Height}}
34 default_border none
35 `))
36
37 var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
38
39 func (d *WaylandTestDriver) Start(path string) {
40 // We want os.Environ, so that it can e.g. find $DISPLAY to run within
41 // X11. wlroots env vars are documented at:
42 // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
43 env := os.Environ()
44 if *headless {
45 env = append(env, "WLR_BACKENDS=headless")
46 }
47
48 d.needPrograms(
49 "sway", // to run a wayland compositor
50 "grim", // to take screenshots
51 "swaymsg", // to send input
52 )
53
54 // First, build the app.
55 dir := d.tempDir("gio-endtoend-wayland")
56 bin := filepath.Join(dir, "red")
57 flags := []string{"build", "-tags", "nox11", "-o=" + bin}
58 if raceEnabled {
59 flags = append(flags, "-race")
60 }
61 flags = append(flags, path)
62 cmd := exec.Command("go", flags...)
63 if out, err := cmd.CombinedOutput(); err != nil {
64 d.Fatalf("could not build app: %s:\n%s", err, out)
65 }
66
67 conf := filepath.Join(dir, "config")
68 f, err := os.Create(conf)
69 if err != nil {
70 d.Fatal(err)
71 }
72 defer f.Close()
73 if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
74 d.width, d.height,
75 }); err != nil {
76 d.Fatal(err)
77 }
78
79 d.socket = filepath.Join(dir, "socket")
80 env = append(env, "SWAYSOCK="+d.socket)
81 d.runtimeDir = dir
82 env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
83
84 var wg sync.WaitGroup
85 d.Cleanup(wg.Wait)
86
87 // First, start sway.
88 {
89 ctx, cancel := context.WithCancel(context.Background())
90 cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
91 cmd.Env = env
92 stderr, err := cmd.StderrPipe()
93 if err != nil {
94 d.Fatal(err)
95 }
96 if err := cmd.Start(); err != nil {
97 d.Fatal(err)
98 }
99 d.Cleanup(cancel)
100 d.Cleanup(func() {
101 // Give it a chance to exit gracefully, cleaning up
102 // after itself. After 10ms, the deferred cancel above
103 // will signal an os.Kill.
104 cmd.Process.Signal(os.Interrupt)
105 time.Sleep(10 * time.Millisecond)
106 })
107
108 // Wait for sway to be ready. We probably don't need a deadline
109 // here.
110 br := bufio.NewReader(stderr)
111 for {
112 line, err := br.ReadString('\n')
113 if err != nil {
114 d.Fatal(err)
115 }
116 if m := rxSwayReady.FindStringSubmatch(line); m != nil {
117 d.display = m[1]
118 break
119 }
120 }
121
122 wg.Add(1)
123 go func() {
124 if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
125 // Don't print all stderr, since we use --verbose.
126 // TODO(mvdan): if it's useful, probably filter
127 // errors and show them.
128 d.Error(err)
129 }
130 wg.Done()
131 }()
132 }
133
134 // Then, start our program on the sway compositor above.
135 {
136 ctx, cancel := context.WithCancel(context.Background())
137 cmd := exec.CommandContext(ctx, bin)
138 cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
139 output, err := cmd.StdoutPipe()
140 if err != nil {
141 d.Fatal(err)
142 }
143 cmd.Stderr = cmd.Stdout
144 d.output = output
145 if err := cmd.Start(); err != nil {
146 d.Fatal(err)
147 }
148 d.Cleanup(cancel)
149 wg.Add(1)
150 go func() {
151 if err := cmd.Wait(); err != nil && ctx.Err() == nil {
152 d.Error(err)
153 }
154 wg.Done()
155 }()
156 }
157
158 // Wait for the gio app to render.
159 d.waitForFrame()
160 }
161
162 func (d *WaylandTestDriver) Screenshot() image.Image {
163 cmd := exec.Command("grim", "/dev/stdout")
164 cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
165 out, err := cmd.CombinedOutput()
166 if err != nil {
167 d.Errorf("%s", out)
168 d.Fatal(err)
169 }
170 img, err := png.Decode(bytes.NewReader(out))
171 if err != nil {
172 d.Fatal(err)
173 }
174 return img
175 }
176
177 func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
178 strs := []string{"--socket", d.socket}
179 for _, arg := range args {
180 strs = append(strs, fmt.Sprint(arg))
181 }
182 cmd := exec.Command("swaymsg", strs...)
183 if out, err := cmd.CombinedOutput(); err != nil {
184 d.Errorf("%s", out)
185 d.Fatal(err)
186 }
187 }
188
189 func (d *WaylandTestDriver) Click(x, y int) {
190 d.swaymsg("seat", "-", "cursor", "set", x, y)
191 d.swaymsg("seat", "-", "cursor", "press", "button1")
192 d.swaymsg("seat", "-", "cursor", "release", "button1")
193
194 // Wait for the gio app to render after this click.
195 d.waitForFrame()
196 }
197