Files
gio/cmd/gogio/android_test.go
T
Daniel Martí 9bbeb92b61 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>
2020-03-02 17:34:59 +01:00

151 lines
3.6 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bufio"
"bytes"
"context"
"fmt"
"image"
"image/png"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
type AndroidTestDriver struct {
driverBase
sdkDir string
adbPath string
}
var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
func (d *AndroidTestDriver) Start(path string, width, height int) {
d.sdkDir = os.Getenv("ANDROID_HOME")
if d.sdkDir == "" {
d.Skipf("Android SDK is required; set $ANDROID_HOME")
}
d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb")
devOut := bytes.TrimSpace(d.adb("devices"))
devices := rxAdbDevice.FindAllSubmatch(devOut, -1)
switch len(devices) {
case 0:
d.Skipf("no Android devices attached via adb; skipping")
case 1:
default:
d.Skipf("multiple Android devices attached via adb; skipping")
}
// If the device is attached but asleep, it's probably just charging.
// Don't use it; the screen needs to be on and unlocked for the test to
// work.
if !bytes.Contains(
d.adb("shell", "dumpsys", "power"),
[]byte(" mWakefulness=Awake"),
) {
d.Skipf("Android device isn't awake; skipping")
}
// First, build the app.
apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
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.
d.tryUninstall()
d.adb("install", apk)
d.Cleanup(d.tryUninstall)
// Force our e2e app to be fullscreen, so that the android system bar at
// the top doesn't mess with our screenshots.
// TODO(mvdan): is there a way to do this via gio, so that we don't need
// to set up a global Android setting via the shell?
d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid)
// Make sure the app isn't already running.
d.adb("shell", "pm", "clear", appid)
// Start listening for log messages.
{
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, d.adbPath,
"logcat",
"-s", // suppress other logs
"-T1", // don't show prevoius log messages
"gio:*", // show all logs from gio
)
logcat, err := cmd.StdoutPipe()
if err != nil {
d.Fatal(err)
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
go func() {
scanner := bufio.NewScanner(logcat)
for scanner.Scan() {
line := scanner.Text()
if strings.HasSuffix(line, ": frame ready") {
d.frameNotifs <- true
}
}
}()
}
// Start the app.
d.adb("shell", "monkey", "-p", appid, "1")
// Wait for the gio app to render.
d.waitForFrame()
}
func (d *AndroidTestDriver) Screenshot() image.Image {
out := d.adb("shell", "screencap", "-p")
img, err := png.Decode(bytes.NewReader(out))
if err != nil {
d.Fatal(err)
}
return img
}
func (d *AndroidTestDriver) tryUninstall() {
cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid)
out, err := cmd.CombinedOutput()
if err != nil {
if bytes.Contains(out, []byte("Unknown package")) {
// The package is not installed. Don't log anything.
return
}
d.Logf("could not uninstall: %v\n%s", err, out)
}
}
func (d *AndroidTestDriver) adb(args ...interface{}) []byte {
strs := []string{}
for _, arg := range args {
strs = append(strs, fmt.Sprint(arg))
}
cmd := exec.Command(d.adbPath, strs...)
out, err := cmd.CombinedOutput()
if err != nil {
d.Errorf("%s", out)
d.Fatal(err)
}
return out
}
func (d *AndroidTestDriver) Click(x, y int) {
d.adb("shell", "input", "tap", x, y)
// Wait for the gio app to render after this click.
d.waitForFrame()
}