blob: f38c8a58d2c400ec105b9a99fe9597f5ef805cf3 [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"
"strings"
"time"
)
// timeout for this program.
const timeout = time.Hour
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.")
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")
}
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); err != nil {
die("%s\n", err)
}
}
// runTest runs the test on device via adb.
func runTest(ctx context.Context, device Device, archive, testRunner 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", getArchiveExtractionDirOnDevice(device), getArchiveExtractionDirOnDevice(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)
}
// Run test.
stdin := fmt.Sprintf("cd %s && %s", getArchiveExtractionDirOnDevice(device), testRunner)
if err := adbWithStdin(ctx, stdin, "shell", "su", "root"); err != nil {
return fmt.Errorf("while running the test: %s", 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/revenge_of_the_skiabot directory is writable for 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/revenge_of_the_skiabot/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"
}
// 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)
}