mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
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>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// 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)
|
||||
}
|
||||
+7
-15
@@ -14,14 +14,16 @@ var raceEnabled = false
|
||||
|
||||
var headless = flag.Bool("headless", true, "run end-to-end tests in headless mode")
|
||||
|
||||
const appid = "localhost.gogio.endtoend"
|
||||
|
||||
// TestDriver is implemented by each of the platforms we can run end-to-end
|
||||
// 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 {
|
||||
// Start provides the test driver with a testing.T, as well as the path
|
||||
// to the Gio app to use for the test. The app will be run with the
|
||||
// given width and height, and the platform's background should be
|
||||
// white.
|
||||
// to the Gio app to use for the test. The driver should attempt to run
|
||||
// the app with the given 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.
|
||||
@@ -51,6 +53,7 @@ func TestEndToEnd(t *testing.T) {
|
||||
{"X11", &X11TestDriver{}},
|
||||
{"Wayland", &WaylandTestDriver{}},
|
||||
{"JS", &JSTestDriver{}},
|
||||
{"Android", &AndroidTestDriver{}},
|
||||
}
|
||||
|
||||
for _, subtest := range subtests {
|
||||
@@ -73,18 +76,7 @@ func runEndToEndTest(t *testing.T, driver TestDriver) {
|
||||
wantColors := func(topLeft, topRight, botLeft, botRight color.RGBA) {
|
||||
t.Helper()
|
||||
img := driver.Screenshot()
|
||||
size_ := img.Bounds().Size()
|
||||
if size_ != size {
|
||||
if !*headless {
|
||||
// Some non-headless drivers, like Sway, may get
|
||||
// their window resized by the host window manager.
|
||||
// Run the rest of the test with the new size.
|
||||
size = size_
|
||||
} else {
|
||||
t.Fatalf("expected dimensions to be %v, got %v",
|
||||
size, size_)
|
||||
}
|
||||
}
|
||||
size = img.Bounds().Size()
|
||||
{
|
||||
minX, minY := 5, 5
|
||||
maxX, maxY := (size.X/2)-5, (size.Y/2)-5
|
||||
|
||||
Reference in New Issue
Block a user