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) {