android_test.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package main_test
   4  
   5  import (
   6  	"bytes"
   7  	"context"
   8  	"fmt"
   9  	"image"
  10  	"image/png"
  11  	"os"
  12  	"os/exec"
  13  	"path/filepath"
  14  	"regexp"
  15  )
  16  
  17  type AndroidTestDriver struct {
  18  	driverBase
  19  
  20  	sdkDir  string
  21  	adbPath string
  22  }
  23  
  24  var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
  25  
  26  func (d *AndroidTestDriver) Start(path string) {
  27  	d.sdkDir = os.Getenv("ANDROID_SDK_ROOT")
  28  	if d.sdkDir == "" {
  29  		d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT")
  30  	}
  31  	d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb")
  32  	if _, err := os.Stat(d.adbPath); os.IsNotExist(err) {
  33  		d.Skipf("adb not found")
  34  	}
  35  
  36  	devOut := bytes.TrimSpace(d.adb("devices"))
  37  	devices := rxAdbDevice.FindAllSubmatch(devOut, -1)
  38  	switch len(devices) {
  39  	case 0:
  40  		d.Skipf("no Android devices attached via adb; skipping")
  41  	case 1:
  42  	default:
  43  		d.Skipf("multiple Android devices attached via adb; skipping")
  44  	}
  45  
  46  	// If the device is attached but asleep, it's probably just charging.
  47  	// Don't use it; the screen needs to be on and unlocked for the test to
  48  	// work.
  49  	if !bytes.Contains(
  50  		d.adb("shell", "dumpsys", "power"),
  51  		[]byte(" mWakefulness=Awake"),
  52  	) {
  53  		d.Skipf("Android device isn't awake; skipping")
  54  	}
  55  
  56  	// First, build the app.
  57  	apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
  58  	d.gogio("-target=android", "-appid="+appid, "-o="+apk, path)
  59  
  60  	// Make sure the app isn't installed already, and try to uninstall it
  61  	// when we finish. Previous failed test runs might have left the app.
  62  	d.tryUninstall()
  63  	d.adb("install", apk)
  64  	d.Cleanup(d.tryUninstall)
  65  
  66  	// Force our e2e app to be fullscreen, so that the android system bar at
  67  	// the top doesn't mess with our screenshots.
  68  	// TODO(mvdan): is there a way to do this via gio, so that we don't need
  69  	// to set up a global Android setting via the shell?
  70  	d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid)
  71  
  72  	// Make sure the app isn't already running.
  73  	d.adb("shell", "pm", "clear", appid)
  74  
  75  	// Start listening for log messages.
  76  	{
  77  		ctx, cancel := context.WithCancel(context.Background())
  78  		cmd := exec.CommandContext(ctx, d.adbPath,
  79  			"logcat",
  80  			"-s",       // suppress other logs
  81  			"-T1",      // don't show previous log messages
  82  			appid+":*", // show all logs from our gio app ID
  83  		)
  84  		output, err := cmd.StdoutPipe()
  85  		if err != nil {
  86  			d.Fatal(err)
  87  		}
  88  		cmd.Stderr = cmd.Stdout
  89  		d.output = output
  90  		if err := cmd.Start(); err != nil {
  91  			d.Fatal(err)
  92  		}
  93  		d.Cleanup(cancel)
  94  	}
  95  
  96  	// Start the app.
  97  	d.adb("shell", "monkey", "-p", appid, "1")
  98  
  99  	// Wait for the gio app to render.
 100  	d.waitForFrame()
 101  }
 102  
 103  func (d *AndroidTestDriver) Screenshot() image.Image {
 104  	out := d.adb("shell", "screencap", "-p")
 105  	img, err := png.Decode(bytes.NewReader(out))
 106  	if err != nil {
 107  		d.Fatal(err)
 108  	}
 109  	return img
 110  }
 111  
 112  func (d *AndroidTestDriver) tryUninstall() {
 113  	cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid)
 114  	out, err := cmd.CombinedOutput()
 115  	if err != nil {
 116  		if bytes.Contains(out, []byte("Unknown package")) {
 117  			// The package is not installed. Don't log anything.
 118  			return
 119  		}
 120  		d.Logf("could not uninstall: %v\n%s", err, out)
 121  	}
 122  }
 123  
 124  func (d *AndroidTestDriver) adb(args ...interface{}) []byte {
 125  	strs := []string{}
 126  	for _, arg := range args {
 127  		strs = append(strs, fmt.Sprint(arg))
 128  	}
 129  	cmd := exec.Command(d.adbPath, strs...)
 130  	out, err := cmd.CombinedOutput()
 131  	if err != nil {
 132  		d.Errorf("%s", out)
 133  		d.Fatal(err)
 134  	}
 135  	return out
 136  }
 137  
 138  func (d *AndroidTestDriver) Click(x, y int) {
 139  	d.adb("shell", "input", "tap", x, y)
 140  
 141  	// Wait for the gio app to render after this click.
 142  	d.waitForFrame()
 143  }
 144