1 // SPDX-License-Identifier: Unlicense OR MIT
2 3 package main_test
4 5 import (
6 "context"
7 "image"
8 "io"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "runtime"
13 "sync"
14 "time"
15 16 "golang.org/x/image/draw"
17 )
18 19 // Wine is tightly coupled with X11 at the moment, and we can reuse the same
20 // methods to automate screenshots and clicks. The main difference is how we
21 // build and run the app.
22 23 // The only quirk is that it seems impossible for the Wine window to take the
24 // entirety of the X server's dimensions, even if we try to resize it to take
25 // the entire display. It seems to want to leave some vertical space empty,
26 // presumably for window decorations or the "start" bar on Windows. To work
27 // around that, make the X server 50x50px bigger, and crop the screenshots back
28 // to the original size.
29 30 type WineTestDriver struct {
31 X11TestDriver
32 }
33 34 func (d *WineTestDriver) Start(path string) {
35 d.needPrograms("wine")
36 37 // First, build the app.
38 bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe")
39 flags := []string{"build", "-o=" + bin}
40 if raceEnabled {
41 if runtime.GOOS != "windows" {
42 // cross-compilation disables CGo, which breaks -race.
43 d.Skipf("can't cross-compile -race for Windows; skipping")
44 }
45 flags = append(flags, "-race")
46 }
47 flags = append(flags, path)
48 cmd := exec.Command("go", flags...)
49 cmd.Env = os.Environ()
50 cmd.Env = append(cmd.Env, "GOOS=windows")
51 if out, err := cmd.CombinedOutput(); err != nil {
52 d.Fatalf("could not build app: %s:\n%s", err, out)
53 }
54 55 var wg sync.WaitGroup
56 d.Cleanup(wg.Wait)
57 58 // Add 50x50px to the display dimensions, as discussed earlier.
59 d.startServer(&wg, d.width+50, d.height+50)
60 61 // Then, start our program via Wine on the X server above.
62 {
63 cacheDir, err := os.UserCacheDir()
64 if err != nil {
65 d.Fatal(err)
66 }
67 // Use a wine directory separate from the default ~/.wine, so
68 // that the user's winecfg doesn't affect our test. This will
69 // default to ~/.cache/gio-e2e-wine. We use the user's cache,
70 // to reuse a previously set up wineprefix.
71 wineprefix := filepath.Join(cacheDir, "gio-e2e-wine")
72 73 // First, ensure that wineprefix is up to date with wineboot.
74 // Wait for this separately from the first frame, as setting up
75 // a new prefix might take 5s on its own.
76 env := []string{
77 "DISPLAY=" + d.display,
78 "WINEDEBUG=fixme-all", // hide "fixme" noise
79 "WINEPREFIX=" + wineprefix,
80 81 // Disable wine-gecko (Explorer) and wine-mono (.NET).
82 // Otherwise, if not installed, wineboot will get stuck
83 // with a prompt to install them on the virtual X
84 // display. Moreover, Gio doesn't need either, and wine
85 // is faster without them.
86 "WINEDLLOVERRIDES=mscoree,mshtml=",
87 }
88 {
89 start := time.Now()
90 cmd := exec.Command("wine", "wineboot", "-i")
91 cmd.Env = env
92 // Use a combined output pipe instead of CombinedOutput,
93 // so that we only wait for the child process to exit,
94 // and we don't need to wait for all of wine's
95 // grandchildren to exit and stop writing. This is
96 // relevant as wine leaves "wineserver" lingering for
97 // three seconds by default, to be reused later.
98 stdout, err := cmd.StdoutPipe()
99 if err != nil {
100 d.Fatal(err)
101 }
102 cmd.Stderr = cmd.Stdout
103 if err := cmd.Run(); err != nil {
104 io.Copy(os.Stderr, stdout)
105 d.Fatal(err)
106 }
107 d.Logf("set up WINEPREFIX in %s", time.Since(start))
108 }
109 110 ctx, cancel := context.WithCancel(context.Background())
111 cmd := exec.CommandContext(ctx, "wine", bin)
112 cmd.Env = env
113 output, err := cmd.StdoutPipe()
114 if err != nil {
115 d.Fatal(err)
116 }
117 cmd.Stderr = cmd.Stdout
118 d.output = output
119 if err := cmd.Start(); err != nil {
120 d.Fatal(err)
121 }
122 d.Cleanup(cancel)
123 wg.Add(1)
124 go func() {
125 if err := cmd.Wait(); err != nil && ctx.Err() == nil {
126 d.Error(err)
127 }
128 wg.Done()
129 }()
130 }
131 // Wait for the gio app to render.
132 d.waitForFrame()
133 134 // xdotool seems to fail at actually moving the window if we use it
135 // immediately after Gio is ready. Why?
136 // We can't tell if the windowmove operation worked until we take a
137 // screenshot, because the getwindowgeometry op reports the 0x0
138 // coordinates even if the window wasn't moved properly.
139 // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that.
140 // TODO(mvdan): revisit this, when you have a spare three hours.
141 time.Sleep(400 * time.Millisecond)
142 id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio")
143 d.xdotool("windowmove", "--sync", id, 0, 0)
144 }
145 146 func (d *WineTestDriver) Screenshot() image.Image {
147 img := d.X11TestDriver.Screenshot()
148 // Crop the screenshot back to the original dimensions.
149 cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height))
150 draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src)
151 return cropped
152 }
153