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