Files
gio/cmd/gogio/android_test.go
T
Daniel Martí 8d1e53cfa9 cmd/gogio: add the first Android end-to-end test
It passes the whole e2e test flow on my real device, a OnePlus 5 running
LineageOS 16.0 (Android 9).

I was also successful at running it against an x86-64 Android 8.0
emulator, but I'm not including any of that just yet. A patch later this
week will include a piece of code to set up and start an emulator, which
CI can then use to run the test.

Also stop requiring the screen dimensions to be enforced when running in
non-headless mode. An Android emulator can run at an arbitrary
resolution, and even in headless mode, but a real Android device will
have its own predefined resolution. Forcing the test user to set the
-headless=false flag to not get annoying "unexpected dimensions" errors
would be annoying.

That check doesn't really mean much, as our test app doesn't care about
the screen resolution. And we were only doing the check sometimes. Drop
it entirely, making the resolution parameters merely a hint so that we
can keep the drivers a bit more consistent.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
2020-01-29 14:50:42 +01:00

177 lines
4.5 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bufio"
"bytes"
"context"
"fmt"
"image"
"image/png"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
)
type AndroidTestDriver struct {
t *testing.T
frameNotifs chan bool
sdkDir string
adbPath string
}
var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
func (d *AndroidTestDriver) Start(t_ *testing.T, path string, width, height int) {
d.frameNotifs = make(chan bool, 1)
d.t = t_
d.sdkDir = os.Getenv("ANDROID_HOME")
if d.sdkDir == "" {
d.t.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.t.Skipf("no Android devices attached via adb; skipping")
case 1:
default:
d.t.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.t.Skipf("Android device isn't awake; skipping")
}
// First, build the app.
dir, err := ioutil.TempDir("", "gio-endtoend-android")
if err != nil {
d.t.Fatal(err)
}
d.t.Cleanup(func() { os.RemoveAll(dir) })
apk := filepath.Join(dir, "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.t.Fatalf("could not build app: %s:\n%s", err, out)
}
// 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.t.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.t.Fatal(err)
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
d.t.Fatal(err)
}
d.t.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")
// 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.
waitForFrame(d.t, d.frameNotifs)
}
func (d *AndroidTestDriver) Screenshot() image.Image {
out := d.adb("shell", "screencap", "-p")
img, err := png.Decode(bytes.NewReader(out))
if err != nil {
d.t.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.t.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.t.Errorf("%s", out)
d.t.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.
waitForFrame(d.t, d.frameNotifs)
}