-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
144 lines (126 loc) · 4.64 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package main
import (
"bytes"
"fmt"
"github.com/jessevdk/go-flags"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
var opts struct {
MinSDKVersion int `short:"s" long:"min-sdk-version" description:"Minimum android SDK version" required:"true"`
}
func main() {
parser := flags.NewParser(&opts, flags.Default|flags.IgnoreUnknown)
parser.LongDescription = `
Run Golang tests on a connected Android device or emulator.
Device selection is delegated to adb, pass in ANDROID_SERIAL= environment variable to adb to specify
a device to run tests on.
Example:
ANDROID_SERIAL=emulator-1234 go-android-test -s 21 -run TestFoo
`
leftoverArgs, err := parser.Parse()
if err != nil {
os.Exit(1)
}
var exitCode int
defer func() { os.Exit(exitCode) }()
testDir, _ := os.MkdirTemp("", "go-android-test")
defer func() { _ = os.RemoveAll(testDir) }()
// "unique" name for compiled tests.
stamp := time.Now().Format("Jan-02-06-15-04-05.000000")
testBinary := filepath.Join(testDir, fmt.Sprintf("tests_%s", stamp))
adbPath, err := findADB()
if err != nil {
fmt.Printf("Fatal: locating adb: %s\n", err)
exitCode = 1
return
}
fmt.Printf("Located adb at: %s\n", adbPath)
// Ask the running Emulator for its abi so we know how to compile for it.
//
// Note: previously this was using ro.product.cpu.abilist, which on some devices would return
// a single abi, and on others a list. A single abi is expected by ndkenv.
// The Internets suggests that ro.product.cpu.abi might not be available on all versions of
// Android, so reverting to abilist and parsing it might be necessary if failures are seen here.
abi, _, err := cmd(adbPath, "shell", "getprop ro.product.cpu.abi")
if err != nil {
fmt.Printf("Fatal: Is an Android Emulator running?\n")
exitCode = 1
return
}
fmt.Printf("Running Android Emulator ABI is '%s'\n", abi)
// Cross-compile tests to an Android binary that's runnable on the emulator
fmt.Println("Cross-compiling tests")
_, _, err = cmd("ndkenv", "-a", abi, "-s", strconv.Itoa(opts.MinSDKVersion),
"go", "test", "-c", "-o", testBinary)
if err != nil {
fmt.Printf("Fatal: compiling tests: %s\n", err)
exitCode = 1
return
}
fmt.Printf("Compiled tests to '%s'\n", testBinary)
// Copy test binary into emulator at /data/local/tmp/<binary>
// Note: Binaries run from /data/local/tmp have greater permissions than those run from other
// locations. Notably, they're executable even when non-root, and allow for dlopen() to open
// system libraries (like libart.so)
fmt.Println("Copying compiled tests onto device")
// Use path (not filepath) for remote paths, because remote paths are always unix-style,
// whereas the development machine may be windows.
remoteTmpDir := path.Join("/data", "local", "tmp")
remoteTestBinary := path.Join(remoteTmpDir, filepath.Base(testBinary))
if _, _, err = cmd(adbPath, "push", testBinary, remoteTestBinary); err != nil {
fmt.Printf("Fatal: Copying to device: %s\n", err)
exitCode = 1
return
}
// Assumes all remaining args are test flags (e.g. -timeout), not flags for compilation or
// flags to be provided to the resulting binary when run
// TODO: Support non-test arguments for the test binary
for i, arg := range leftoverArgs {
if strings.HasPrefix(arg, "-") {
leftoverArgs[i] = fmt.Sprintf("-test.%s", strings.TrimPrefix(arg, "-"))
}
}
// This allows loading of system libraries (i.e. libart.so) to instantiate a JVM from inside
// the native process.
// Thanks to https://gershnik.github.io/2021/03/26/load-art-from-native.html for this!
var libraryPaths string
switch abi {
case "x86", "arm":
libraryPaths = "/apex/com.android.art/lib:/apex/com.android.runtime/lib"
default:
libraryPaths = "/apex/com.android.art/lib64:/apex/com.android.runtime/lib64"
}
// Run tests binary, then delete it.
// chmod is neccessary when running from windows, since pushed binary won't be executable
fmt.Println("Running tests on device")
testCmd := fmt.Sprintf(
"chmod +x \"%[2]s\"; LD_LIBRARY_PATH=%s \"%[2]s\" %s; rm -f \"%[2]s\"",
libraryPaths, remoteTestBinary, strings.Join(leftoverArgs, " "))
_, _, err = cmd(adbPath, "shell", testCmd)
if err != nil {
exitCode = err.(*exec.ExitError).ExitCode()
return
}
return
}
func cmd(name string, arg ...string) (stdout string, stderr string, err error) {
outBuf, errBuf := new(bytes.Buffer), new(bytes.Buffer)
c := exec.Command(name, arg...)
c.Stdout = io.MultiWriter(outBuf, os.Stdout)
c.Stderr = io.MultiWriter(errBuf, os.Stderr)
err = c.Run()
stdout = strings.TrimSpace(outBuf.String())
stderr = strings.TrimSpace(errBuf.String())
if err != nil {
err = fmt.Errorf("running %s: %s\n", c, err)
return
}
return
}