~eliasnaur/gio

ref: 6a9a87046221602d1a323887cd619e75c81cc24e gio/cmd/gogio/windows_test.go -rw-r--r-- 4.6 KiB
6a9a8704Sebastien Binet app{,/internal/window}: make app.Main blocking on desktop platforms 2 years ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// SPDX-License-Identifier: Unlicense OR MIT

package main_test

import (
	"context"
	"image"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sync"
	"time"

	"golang.org/x/image/draw"
)

// Wine is tightly coupled with X11 at the moment, and we can reuse the same
// methods to automate screenshots and clicks. The main difference is how we
// build and run the app.

// The only quirk is that it seems impossible for the Wine window to take the
// entirety of the X server's dimensions, even if we try to resize it to take
// the entire display. It seems to want to leave some vertical space empty,
// presumably for window decorations or the "start" bar on Windows. To work
// around that, make the X server 50x50px bigger, and crop the screenshots back
// to the original size.

type WineTestDriver struct {
	X11TestDriver
}

func (d *WineTestDriver) Start(path string) {
	d.needPrograms("wine")

	// First, build the app.
	bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe")
	flags := []string{"build", "-o=" + bin}
	if raceEnabled {
		if runtime.GOOS != "windows" {
			// cross-compilation disables CGo, which breaks -race.
			d.Skipf("can't cross-compile -race for Windows; skipping")
		}
		flags = append(flags, "-race")
	}
	flags = append(flags, path)
	cmd := exec.Command("go", flags...)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "GOOS=windows")
	if out, err := cmd.CombinedOutput(); err != nil {
		d.Fatalf("could not build app: %s:\n%s", err, out)
	}

	var wg sync.WaitGroup
	d.Cleanup(wg.Wait)

	// Add 50x50px to the display dimensions, as discussed earlier.
	d.startServer(wg, d.width+50, d.height+50)

	// Then, start our program via Wine on the X server above.
	{
		cacheDir, err := os.UserCacheDir()
		if err != nil {
			d.Fatal(err)
		}
		// Use a wine directory separate from the default ~/.wine, so
		// that the user's winecfg doesn't affect our test. This will
		// default to ~/.cache/gio-e2e-wine. We use the user's cache,
		// to reuse a previously set up wineprefix.
		wineprefix := filepath.Join(cacheDir, "gio-e2e-wine")

		// First, ensure that wineprefix is up to date with wineboot.
		// Wait for this separately from the first frame, as setting up
		// a new prefix might take 5s on its own.
		env := []string{
			"DISPLAY=" + d.display,
			"WINEDEBUG=fixme-all", // hide "fixme" noise
			"WINEPREFIX=" + wineprefix,

			// Disable wine-gecko (Explorer) and wine-mono (.NET).
			// Otherwise, if not installed, wineboot will get stuck
			// with a prompt to install them on the virtual X
			// display. Moreover, Gio doesn't need either, and wine
			// is faster without them.
			"WINEDLLOVERRIDES=mscoree,mshtml=",
		}
		{
			start := time.Now()
			cmd := exec.Command("wine", "wineboot", "-i")
			cmd.Env = env
			// Use a combined output pipe instead of CombinedOutput,
			// so that we only wait for the child process to exit,
			// and we don't need to wait for all of wine's
			// grandchildren to exit and stop writing. This is
			// relevant as wine leaves "wineserver" lingering for
			// three seconds by default, to be reused later.
			stdout, err := cmd.StdoutPipe()
			if err != nil {
				d.Fatal(err)
			}
			cmd.Stderr = cmd.Stdout
			if err := cmd.Run(); err != nil {
				io.Copy(os.Stderr, stdout)
				d.Fatal(err)
			}
			d.Logf("set up WINEPREFIX in %s", time.Since(start))
		}

		ctx, cancel := context.WithCancel(context.Background())
		cmd := exec.CommandContext(ctx, "wine", bin)
		cmd.Env = env
		output, err := cmd.StdoutPipe()
		if err != nil {
			d.Fatal(err)
		}
		cmd.Stderr = cmd.Stdout
		d.output = output
		if err := cmd.Start(); err != nil {
			d.Fatal(err)
		}
		d.Cleanup(cancel)
		wg.Add(1)
		go func() {
			if err := cmd.Wait(); err != nil && ctx.Err() == nil {
				d.Error(err)
			}
			wg.Done()
		}()
	}
	// Wait for the gio app to render.
	d.waitForFrame()

	// xdotool seems to fail at actually moving the window if we use it
	// immediately after Gio is ready. Why?
	// We can't tell if the windowmove operation worked until we take a
	// screenshot, because the getwindowgeometry op reports the 0x0
	// coordinates even if the window wasn't moved properly.
	// A sleep of ~20ms seems to be enough on an idle laptop. Use 10x that.
	// TODO(mvdan): revisit this, when you have a spare three hours.
	time.Sleep(200 * time.Millisecond)
	id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio")
	d.xdotool("windowmove", "--sync", id, 0, 0)
}

func (d *WineTestDriver) Screenshot() image.Image {
	img := d.X11TestDriver.Screenshot()
	// Crop the screenshot back to the original dimensions.
	cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height))
	draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src)
	return cropped
}