x11_test.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package main_test
4
5 import (
6 "bytes"
7 "context"
8 "fmt"
9 "image"
10 "image/png"
11 "io"
12 "math/rand"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "sync"
17 "time"
18 )
19
20 type X11TestDriver struct {
21 driverBase
22
23 display string
24 }
25
26 func (d *X11TestDriver) Start(path string) {
27 // First, build the app.
28 bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
29 flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
30 if raceEnabled {
31 flags = append(flags, "-race")
32 }
33 flags = append(flags, path)
34 cmd := exec.Command("go", flags...)
35 if out, err := cmd.CombinedOutput(); err != nil {
36 d.Fatalf("could not build app: %s:\n%s", err, out)
37 }
38
39 var wg sync.WaitGroup
40 d.Cleanup(wg.Wait)
41
42 d.startServer(&wg, d.width, d.height)
43
44 // Then, start our program on the X server above.
45 {
46 ctx, cancel := context.WithCancel(context.Background())
47 cmd := exec.CommandContext(ctx, bin)
48 cmd.Env = []string{"DISPLAY=" + d.display}
49 output, err := cmd.StdoutPipe()
50 if err != nil {
51 d.Fatal(err)
52 }
53 cmd.Stderr = cmd.Stdout
54 d.output = output
55 if err := cmd.Start(); err != nil {
56 d.Fatal(err)
57 }
58 d.Cleanup(cancel)
59 wg.Add(1)
60 go func() {
61 if err := cmd.Wait(); err != nil && ctx.Err() == nil {
62 d.Error(err)
63 }
64 wg.Done()
65 }()
66 }
67
68 // Wait for the gio app to render.
69 d.waitForFrame()
70 }
71
72 func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) {
73 // Pick a random display number between 1 and 100,000. Most machines
74 // will only be using :0, so there's only a 0.001% chance of two
75 // concurrent test runs to run into a conflict.
76 rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
77 d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)
78
79 var xprog string
80 xflags := []string{
81 "-wr", // we want a white background; the default is black
82 }
83 if *headless {
84 xprog = "Xvfb" // virtual X server
85 xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
86 } else {
87 xprog = "Xephyr" // nested X server as a window
88 xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
89 }
90 xflags = append(xflags, d.display)
91
92 d.needPrograms(
93 xprog, // to run the X server
94 "scrot", // to take screenshots
95 "xdotool", // to send input
96 )
97 ctx, cancel := context.WithCancel(context.Background())
98 cmd := exec.CommandContext(ctx, xprog, xflags...)
99 combined := &bytes.Buffer{}
100 cmd.Stdout = combined
101 cmd.Stderr = combined
102 if err := cmd.Start(); err != nil {
103 d.Fatal(err)
104 }
105 d.Cleanup(cancel)
106 d.Cleanup(func() {
107 // Give it a chance to exit gracefully, cleaning up
108 // after itself. After 10ms, the deferred cancel above
109 // will signal an os.Kill.
110 cmd.Process.Signal(os.Interrupt)
111 time.Sleep(10 * time.Millisecond)
112 })
113
114 // Wait for the X server to be ready. The socket path isn't
115 // terribly portable, but that's okay for now.
116 withRetries(d.T, time.Second, func() error {
117 socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
118 _, err := os.Stat(socket)
119 return err
120 })
121
122 wg.Add(1)
123 go func() {
124 if err := cmd.Wait(); err != nil && ctx.Err() == nil {
125 // Print all output and error.
126 io.Copy(os.Stdout, combined)
127 d.Error(err)
128 }
129 wg.Done()
130 }()
131 }
132
133 func (d *X11TestDriver) Screenshot() image.Image {
134 cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
135 cmd.Env = []string{"DISPLAY=" + d.display}
136 out, err := cmd.CombinedOutput()
137 if err != nil {
138 d.Errorf("%s", out)
139 d.Fatal(err)
140 }
141 img, err := png.Decode(bytes.NewReader(out))
142 if err != nil {
143 d.Fatal(err)
144 }
145 return img
146 }
147
148 func (d *X11TestDriver) xdotool(args ...interface{}) string {
149 d.Helper()
150 strs := make([]string, len(args))
151 for i, arg := range args {
152 strs[i] = fmt.Sprint(arg)
153 }
154 cmd := exec.Command("xdotool", strs...)
155 cmd.Env = []string{"DISPLAY=" + d.display}
156 out, err := cmd.CombinedOutput()
157 if err != nil {
158 d.Errorf("%s", out)
159 d.Fatal(err)
160 }
161 return string(bytes.TrimSpace(out))
162 }
163
164 func (d *X11TestDriver) Click(x, y int) {
165 d.xdotool("mousemove", "--sync", x, y)
166 d.xdotool("click", "1")
167
168 // Wait for the gio app to render after this click.
169 d.waitForFrame()
170 }
171