blob: 0e6bf7e9eba8136d4a4e80334c59facb72a0c437 [file] [log] [blame]
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This program is the brains behind the adb_test Bazel rule, which allows running a test on an
// Android device via adb.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
const (
// timeout for this program.
timeout = time.Hour
// adbTestOutputDirEnvVar is the environment variable that tells the test running on device where
// to write output files, if any.
adbTestOutputDirEnvVar = "ADB_TEST_OUTPUT_DIR"
)
type Device string
const (
Pixel5 = Device("pixel_5")
Pixel7 = Device("pixel_7")
)
var AllDevices = []Device{
// TODO(lovisolo): Add more devices.
Pixel5,
Pixel7,
}
func main() {
deviceFlag := flag.String("device", "", `Device under test, e.g. "pixel_5".`)
archiveFlag := flag.String("archive", "", "Tarball with the payload to upload to the device under test.")
testRunnerFlag := flag.String("test-runner", "", "Path to the test runner inside the tarball.")
outputDirFlag := flag.String("output-dir", "", "Path on the host machine where to write any outputs produced by the test.")
flag.Parse()
if *deviceFlag == "" {
die("Flag --device is required.\n")
}
if *archiveFlag == "" {
die("Flag --archive is required.\n")
}
if *testRunnerFlag == "" {
die("Flag --test-runner is required.\n")
}
// Fail early if the output directory on the host machine is not empty or if it's non-writable.
if *outputDirFlag != "" {
// Check whether the directory exists.
fileInfo, err := os.Stat(*outputDirFlag)
if err != nil {
die("while stating output dir %q: %s\n", *outputDirFlag, err)
}
if !fileInfo.IsDir() {
die("output dir %q is not a directory.\n", *outputDirFlag)
}
// Check whether the directory is empty.
entries, err := os.ReadDir(*outputDirFlag)
if err != nil {
die("while listing the contents of output dir %q: %s\n", *outputDirFlag, err)
}
if len(entries) != 0 {
die("output dir %q is not empty.\n", *outputDirFlag)
}
// Check whether the directory is writable by creating and then removing an empty file.
testFile := filepath.Join(*outputDirFlag, "test")
if err := os.WriteFile(testFile, []byte{}, 0644); err != nil {
die("while writing test file %q in output dir: %s\n", testFile, err)
}
if err := os.Remove(testFile); err != nil {
die("while deleting test file %q in output dir: %s\n", testFile, err)
}
}
var device Device
for _, d := range AllDevices {
if *deviceFlag == string(d) {
device = d
}
}
if device == "" {
die("Unknown device: %q\n", *deviceFlag)
}
ctx, cancelFn := context.WithTimeout(context.Background(), timeout)
defer cancelFn()
if err := runTest(ctx, device, *archiveFlag, *testRunnerFlag, *outputDirFlag); err != nil {
die("%s\n", err)
}
}
// runTest runs the test on device via adb.
func runTest(ctx context.Context, device Device, archive, testRunner, outputDir string) error {
// TODO(lovisolo): Add any necessary device-specific setup steps such as turning cores on/off and
// setting the CPU/GPU frequencies.
// TODO(lovisolo): Should we check that the machine is attached to the expected device type?
// E.g. run "adb devices -l" and check that the output contains
// "model:Pixel_5".
// Clean up the device before running the test. Previous tests might have left the device in a
// dirty state.
cleanUpDevice := func(device Device) error {
return adb(ctx, "shell", "su", "root", "rm", "-rf", getArchivePathOnDevice(device), getArchiveExtractionDirOnDevice(device), getOutputDirOnDevice(device))
}
if err := cleanUpDevice(device); err != nil {
return fmt.Errorf("while cleaning up the device before running the test: %s", err)
}
// Also clean up device after running the test.
defer func() {
if err := cleanUpDevice(device); err != nil {
die(fmt.Sprintf("while cleaning up the device after running the test: %s\n", err))
}
}()
// Upload archive to device.
if err := adb(ctx, "push", archive, getArchivePathOnDevice(device)); err != nil {
return fmt.Errorf("while pushing archive to device: %s", err)
}
// Extract archive.
if err := adb(ctx, "shell", "su", "root", "mkdir", "-p", getArchiveExtractionDirOnDevice(device)); err != nil {
return fmt.Errorf("while creating archive extraction directory on device: %s", err)
}
if err := adb(ctx, "shell", "su", "root", "tar", "xzvf", getArchivePathOnDevice(device), "-C", getArchiveExtractionDirOnDevice(device)); err != nil {
return fmt.Errorf("while extracting archive on device: %s", err)
}
// Create on-device output dir if necessary.
if outputDir != "" {
if err := adb(ctx, "shell", "su", "root", "mkdir", "-p", getOutputDirOnDevice(device)); err != nil {
return fmt.Errorf("while creating output dir on device: %s", err)
}
}
// If necessary, we will tell the test runner where to store output files via an environment
// variable.
outputDirEnvVar := ""
if outputDir != "" {
outputDirEnvVar = fmt.Sprintf("%s=%s", adbTestOutputDirEnvVar, getOutputDirOnDevice(device))
}
// Run test.
stdin := fmt.Sprintf("cd %s && %s %s", getArchiveExtractionDirOnDevice(device), outputDirEnvVar, testRunner)
if err := adbWithStdin(ctx, stdin, "shell", "su", "root"); err != nil {
return fmt.Errorf("while running the test: %s", err)
}
// Pull output files from the device if necessary.
if outputDir != "" {
// This will save the output files to <output dir>/<output dir on device>.
if err := adb(ctx, "pull", getOutputDirOnDevice(device), outputDir); err != nil {
return fmt.Errorf("while pulling on-device output dir %q into host output dir %q: %s", getOutputDirOnDevice(device), outputDir, err)
}
// But we want the output files to be placed in <output dir>, so we'll move them one by one.
srcDir := filepath.Join(outputDir, filepath.Base(getOutputDirOnDevice(device)))
dstDir := outputDir
entries, err := os.ReadDir(srcDir)
if err != nil {
return fmt.Errorf("while reading the contents of output dir %q: %s", outputDir, err)
}
for _, entry := range entries {
oldPath := filepath.Join(srcDir, entry.Name())
newPath := filepath.Join(dstDir, entry.Name())
if err := os.Rename(oldPath, newPath); err != nil {
return fmt.Errorf("while renaming %q to %q: %s", oldPath, newPath, err)
}
}
// Finally, delete the spurious <output dir>/<output dir on device> directory created by
// "adb pull".
if err := os.Remove(srcDir); err != nil {
return fmt.Errorf("while removing directory %q: %s", srcDir, err)
}
}
return nil
}
// getArchivePathOnDevice returns the path in the device's file system where the archive should be
// uploaded.
func getArchivePathOnDevice(device Device) string {
// The /sdcard directory is writable by non-root users, but files in this directory cannot be
// executed. For this reason, we extract the archive in a directory under /data, which allows
// executing files but requires root privileges.
//
// This might change in the future based on the device type, whether or not it's rooted, etc.
return "/sdcard/bazel-adb-test.tar.gz"
}
// getArchiveExtractionDirOnDevice returns the directory in the device's file system where the
// archive should be extracted.
func getArchiveExtractionDirOnDevice(device Device) string {
// This might change in the future based on the device type, whether or not it's rooted, etc.
return "/data/bazel-adb-test"
}
// getOutputDirOnDevice returns the directory in the device's file system where the test should
// write any output files. These files will then be copied from the device to the machine where adb
// is running.
func getOutputDirOnDevice(device Device) string {
// We have tests write output files to a directory under /sdcard, rather than /data, because the
// /data directory permissions make it impossible to "adb pull" from it.
//
// This might change in the future based on the device type, whether or not it's rooted, etc.
return "/sdcard/bazel-adb-test-output-dir"
}
// adb runs adb with the given arguments.
func adb(ctx context.Context, args ...string) error {
return adbWithStdin(ctx, "", args...)
}
// adbWithStdin runs adb with the given arguments, and pipes the given input via standard input.
func adbWithStdin(ctx context.Context, stdin string, args ...string) error {
timestamp := time.Now().Format(time.RFC3339)
commandAndArgs := strings.Join(append([]string{"adb"}, args...), " ")
withStdin := ""
if stdin != "" {
withStdin = fmt.Sprintf(" with standard input %q", stdin)
}
fmt.Printf("[%s] Executing: %q%s...\n", timestamp, commandAndArgs, withStdin)
cmd := exec.CommandContext(ctx, "adb", args...)
if stdin != "" {
cmd.Stdin = bytes.NewBufferString(stdin)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func die(msg string, a ...interface{}) {
fmt.Fprintf(os.Stderr, msg, a...)
os.Exit(1)
}