From 49000ae4a3e3af09e515f1319fc89c68dfc76c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Wed, 4 Mar 2020 22:29:38 +0000 Subject: [PATCH] cmd/gogio: add the first Windows e2e test via Wine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since Wine is heavily tied to X11, we build its end-to-end test driver on top of X11's. We use the same mechanism to start an X server, take screenshots, and issue clicks. Its only quirk is that it was difficult to get the screenshots to line up with Gio's window. The comments cover what we ended up with. The display dimensions are now part of driverBase, so that methods other than Start can also use them - this is necessary for the wine driver to crop screenshots. We also use a sleep for now; a comment explains why, and a TODO is left for future Dan to deal with. What we have now works, and I've spent enough hours on this patch as it is. Adding Wine to CI, and ensuring that the test passes there, is left for a follow-up patch. Signed-off-by: Daniel Martí --- cmd/gogio/android_test.go | 2 +- cmd/gogio/e2e_test.go | 20 ++++--- cmd/gogio/js_test.go | 4 +- cmd/gogio/wayland_test.go | 4 +- cmd/gogio/windows_test.go | 115 ++++++++++++++++++++++++++++++++++++++ cmd/gogio/x11_test.go | 11 ++-- 6 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 cmd/gogio/windows_test.go diff --git a/cmd/gogio/android_test.go b/cmd/gogio/android_test.go index a0e2a7f1..7c8e6b4d 100644 --- a/cmd/gogio/android_test.go +++ b/cmd/gogio/android_test.go @@ -25,7 +25,7 @@ type AndroidTestDriver struct { var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`) -func (d *AndroidTestDriver) Start(path string, width, height int) { +func (d *AndroidTestDriver) Start(path string) { d.sdkDir = os.Getenv("ANDROID_HOME") if d.sdkDir == "" { d.Skipf("Android SDK is required; set $ANDROID_HOME") diff --git a/cmd/gogio/e2e_test.go b/cmd/gogio/e2e_test.go index 54797929..66b30db4 100644 --- a/cmd/gogio/e2e_test.go +++ b/cmd/gogio/e2e_test.go @@ -26,15 +26,15 @@ const appid = "localhost.gogio.endtoend" // tests on. None of its methods return any errors, as the errors are directly // reported to testing.T via methods like Fatal. type TestDriver interface { - initBase(*testing.T) + initBase(t *testing.T, width, height int) // Start opens the Gio app found at path. The driver should attempt to - // run the app with the given width and height, and the platform's - // background should be white. + // run the app with the base driver's width and height, and the + // platform's background should be white. // // When the function returns, the gio app must be ready to use on the // platform, with its initial frame fully drawn. - Start(path string, width, height int) + Start(path string) // Screenshot takes a screenshot of the Gio app on the platform. Screenshot() image.Image @@ -48,6 +48,8 @@ type TestDriver interface { type driverBase struct { *testing.T + width, height int + // TODO(mvdan): Make this lower-level, so that each driver can simply // send us each line of output from the app. That will let us // deduplicate some code, and also show app output as test logs in a @@ -55,8 +57,9 @@ type driverBase struct { frameNotifs chan bool } -func (d *driverBase) initBase(t *testing.T) { +func (d *driverBase) initBase(t *testing.T, width, height int) { d.T = t + d.width, d.height = width, height d.frameNotifs = make(chan bool, 1) } @@ -76,6 +79,7 @@ func TestEndToEnd(t *testing.T) { {"Wayland", &WaylandTestDriver{}}, {"JS", &JSTestDriver{}}, {"Android", &AndroidTestDriver{}}, + {"Windows", &WineTestDriver{}}, } for _, subtest := range subtests { @@ -88,11 +92,11 @@ func TestEndToEnd(t *testing.T) { } func runEndToEndTest(t *testing.T, driver TestDriver) { - driver.initBase(t) - size := image.Point{X: 800, Y: 600} + driver.initBase(t, size.X, size.Y) + t.Log("starting driver and gio app") - driver.Start("testdata/red.go", size.X, size.Y) + driver.Start("testdata/red.go") beef := color.RGBA{R: 0xde, G: 0xad, B: 0xbe} white := color.RGBA{R: 0xff, G: 0xff, B: 0xff} diff --git a/cmd/gogio/js_test.go b/cmd/gogio/js_test.go index 1760bc14..94767974 100644 --- a/cmd/gogio/js_test.go +++ b/cmd/gogio/js_test.go @@ -26,7 +26,7 @@ type JSTestDriver struct { ctx context.Context } -func (d *JSTestDriver) Start(path string, width, height int) { +func (d *JSTestDriver) Start(path string) { if raceEnabled { d.Skipf("js/wasm doesn't support -race; skipping") } @@ -104,7 +104,7 @@ func (d *JSTestDriver) Start(path string, width, height int) { d.Cleanup(ts.Close) if err := chromedp.Run(ctx, - chromedp.EmulateViewport(int64(width), int64(height)), + chromedp.EmulateViewport(int64(d.width), int64(d.height)), chromedp.Navigate(ts.URL), ); err != nil { d.Fatal(err) diff --git a/cmd/gogio/wayland_test.go b/cmd/gogio/wayland_test.go index cb1a64b4..e6525165 100644 --- a/cmd/gogio/wayland_test.go +++ b/cmd/gogio/wayland_test.go @@ -37,7 +37,7 @@ default_border none var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`) -func (d *WaylandTestDriver) Start(path string, width, height int) { +func (d *WaylandTestDriver) Start(path string) { // We want os.Environ, so that it can e.g. find $DISPLAY to run within // X11. wlroots env vars are documented at: // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md @@ -72,7 +72,7 @@ func (d *WaylandTestDriver) Start(path string, width, height int) { } defer f.Close() if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{ - width, height, + d.width, d.height, }); err != nil { d.Fatal(err) } diff --git a/cmd/gogio/windows_test.go b/cmd/gogio/windows_test.go new file mode 100644 index 00000000..b659765e --- /dev/null +++ b/cmd/gogio/windows_test.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bufio" + "bytes" + "context" + "image" + "io" + "os" + "os/exec" + "path/filepath" + "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 { + 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. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "wine", bin) + cmd.Env = []string{"DISPLAY=" + d.display} + stdout, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + stderr := &bytes.Buffer{} + cmd.Stderr = stderr + + 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 { + // Print stderr and error. + io.Copy(os.Stdout, stderr) + d.Error(err) + } + wg.Done() + }() + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + if line == "frame ready" { + d.frameNotifs <- true + } + } + }() + + } + // 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 +} diff --git a/cmd/gogio/x11_test.go b/cmd/gogio/x11_test.go index 458cdba1..f035c914 100644 --- a/cmd/gogio/x11_test.go +++ b/cmd/gogio/x11_test.go @@ -24,7 +24,7 @@ type X11TestDriver struct { display string } -func (d *X11TestDriver) Start(path string, width, height int) { +func (d *X11TestDriver) Start(path string) { // First, build the app. bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red") flags := []string{"build", "-tags", "nowayland", "-o=" + bin} @@ -40,7 +40,7 @@ func (d *X11TestDriver) Start(path string, width, height int) { var wg sync.WaitGroup d.Cleanup(wg.Wait) - d.startServer(wg, width, height) + d.startServer(wg, d.width, d.height) // Then, start our program on the X server above. { @@ -159,17 +159,20 @@ func (d *X11TestDriver) Screenshot() image.Image { return img } -func (d *X11TestDriver) xdotool(args ...interface{}) { +func (d *X11TestDriver) xdotool(args ...interface{}) string { + d.Helper() strs := make([]string, len(args)) for i, arg := range args { strs[i] = fmt.Sprint(arg) } cmd := exec.Command("xdotool", strs...) cmd.Env = []string{"DISPLAY=" + d.display} - if out, err := cmd.CombinedOutput(); err != nil { + out, err := cmd.CombinedOutput() + if err != nil { d.Errorf("%s", out) d.Fatal(err) } + return string(bytes.TrimSpace(out)) } func (d *X11TestDriver) Click(x, y int) {