~eliasnaur/gio

9bbeb92b614c2520c99ebc33b5e822caf6325076 — Daniel Martí 6 months ago b064899
cmd/gogio: reuse the test binary to call gogio in the e2e tests

We were using 'go run . <args>' before, which works fine, but does mean
re-linking a new binary and throwing it away at each invocation. Given
that the end-to-end tests don't do all that much work besides building
the tiny red.go app, this amount of extra work was noticeable.

We can obtain statistics for the JS sub-test, which used 'go run', via
the perflock and benchcmd tools:

	$ go test -c
	$ perflock -governorp% benchcmd EndToEnd/JS ./gogio.test -test.run=EndToEnd/JS

After capturing those numbers before and after the change, we can then
compare them with benchstat. The CPU cost of the subtest is halved:

	name         old time/op         new time/op         delta
	EndToEnd/JS          1.42s ± 2%          1.07s ± 3%  -25.04%  (p=0.008 n=5+5)

	name         old user-time/op    new user-time/op    delta
	EndToEnd/JS          1.46s ± 3%          0.75s ± 5%  -48.34%  (p=0.008 n=5+5)

	name         old sys-time/op     new sys-time/op     delta
	EndToEnd/JS          366ms ±13%          224ms ± 7%  -38.79%  (p=0.008 n=5+5)

An alternative here would have been to refactor main.go to allow being
called directly. However, that would have required a non-trivial
refactor, since flag parsing is done via globals. Given that the
TestMain method is asy and keeps the main function simple, we've
decided to avoid a refactor.

While at it, remove the sleep in the Android driver to wait for the app
to come up on screen. Since we retry screenshots now, we no longer need
a static sleep. On average, we still need one retry for the initial
screenshot, but that's just 100ms versus the old 500ms. The maximum wait
time is also 2s here, which should scale better for slower devices.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
4 files changed, 27 insertions(+), 21 deletions(-)

M cmd/gogio/android_test.go
M cmd/gogio/e2e_test.go
M cmd/gogio/js_test.go
M cmd/gogio/main_test.go
M cmd/gogio/android_test.go => cmd/gogio/android_test.go +1 -15
@@ 14,7 14,6 @@ import (
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

type AndroidTestDriver struct {


@@ 55,13 54,7 @@ func (d *AndroidTestDriver) Start(path string, width, height int) {

	// First, build the app.
	apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")

	// TODO(mvdan): This is inefficient, as we link the gogio tool every time.
	// Consider options in the future. On the plus side, this is simple.
	cmd := exec.Command("go", "run", ".", "-target=android", "-appid="+appid, "-o="+apk, path)
	if out, err := cmd.CombinedOutput(); err != nil {
		d.Fatalf("could not build app: %s:\n%s", err, out)
	}
	d.gogio("-target=android", "-appid="+appid, "-o="+apk, path)

	// Make sure the app isn't installed already, and try to uninstall it
	// when we finish. Previous failed test runs might have left the app.


@@ 110,13 103,6 @@ func (d *AndroidTestDriver) Start(path string, width, height int) {
	// Start the app.
	d.adb("shell", "monkey", "-p", appid, "1")

	// Unfortunately, it seems like waiting for the initial frame isn't
	// enough. Most Android versions have animations when opening apps that
	// run for hundreds of milliseconds, so that's probably the reason.
	// TODO(mvdan): any way to wait for the screen to be ready without a
	// static sleep?
	time.Sleep(500 * time.Millisecond)

	// Wait for the gio app to render.
	d.waitForFrame()
}

M cmd/gogio/e2e_test.go => cmd/gogio/e2e_test.go +13 -0
@@ 280,3 280,16 @@ func (d *driverBase) tempDir(name string) string {
	d.Cleanup(func() { os.RemoveAll(dir) })
	return dir
}

func (d *driverBase) gogio(args ...string) {
	d.Helper()
	prog, err := os.Executable()
	if err != nil {
		d.Fatal(err)
	}
	cmd := exec.Command(prog, args...)
	cmd.Env = append(os.Environ(), "RUN_GOGIO=1")
	if out, err := cmd.CombinedOutput(); err != nil {
		d.Fatalf("gogio error: %s:\n%s", err, out)
	}
}

M cmd/gogio/js_test.go => cmd/gogio/js_test.go +1 -6
@@ 33,12 33,7 @@ func (d *JSTestDriver) Start(path string, width, height int) {

	// First, build the app.
	dir := d.tempDir("gio-endtoend-js")
	// TODO(mvdan): This is inefficient, as we link the gogio tool every time.
	// Consider options in the future. On the plus side, this is simple.
	cmd := exec.Command("go", "run", ".", "-target=js", "-o="+dir, path)
	if out, err := cmd.CombinedOutput(); err != nil {
		d.Fatalf("could not build app: %s:\n%s", err, out)
	}
	d.gogio("-target=js", "-o="+dir, path)

	// Second, start Chrome.
	opts := append(chromedp.DefaultExecAllocatorOptions[:],

M cmd/gogio/main_test.go => cmd/gogio/main_test.go +12 -0
@@ 1,9 1,21 @@
package main

import (
	"os"
	"testing"
)

func TestMain(m *testing.M) {
	if os.Getenv("RUN_GOGIO") != "" {
		// Allow the end-to-end tests to call the gogio tool without
		// having to build it from scratch, nor having to refactor the
		// main function to avoid using global variables.
		main()
		os.Exit(0) // main already exits, but just in case.
	}
	os.Exit(m.Run())
}

type expval struct {
	in, out string
}