blob: e0264de2d8f92c57e25e0ec0c778482b339436ab [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package orchestrate
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
ffx "go.fuchsia.dev/fuchsia/tools/orchestrate/ffx"
utils "go.fuchsia.dev/fuchsia/tools/orchestrate/utils"
)
// TestOrchestrator uses FFX to run Fuchsia component tests.
type TestOrchestrator struct {
ffx *ffx.Ffx
deviceConfig *DeviceConfig
ffxLogProc *os.Process
targetLogFile *os.File
}
var (
ffxDaemonLog = filepath.Join(os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"), "ffx_daemon.log")
ffxConfigDump = filepath.Join(os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"), "ffx_config.txt")
subrunnerLog = filepath.Join(os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"), "subrunner.log")
targetLog = filepath.Join(os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"), "target.log")
targetSymLog = filepath.Join(os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"), "target.symbolized.log")
summaryPath = filepath.Join(os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"), "summary.json")
)
// NewTestOrchestrator creates a TestOrchestrator with default dependencies.
func NewTestOrchestrator(deviceConfig *DeviceConfig) *TestOrchestrator {
return &TestOrchestrator{
deviceConfig: deviceConfig,
}
}
func (r *TestOrchestrator) instantiateFfx(in *RunInput) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("os.Getwd: %w", err)
}
ffxOpt := &ffx.Option{
ExePath: filepath.Join(wd, in.Target().FfxPath),
LogDir: os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"),
}
f, err := ffx.New(ffxOpt)
if err != nil {
return fmt.Errorf("ffx.New: %w", err)
}
r.ffx = f
return nil
}
// Run executes tests.
func (r *TestOrchestrator) Run(in *RunInput, testCmd []string) error {
if len(in.Cipd()) > 0 {
fmt.Println("=== orchestrate - Downloading CIPD packages (0/6) ===")
if err := r.cipdEnsure(in); err != nil {
return fmt.Errorf("cipdEnsure: %w", err)
}
}
if in.IsTarget() {
fmt.Println("=== orchestrate - Setting up ffx (1/6) ===")
if err := r.instantiateFfx(in); err != nil {
return fmt.Errorf("instantiateFfx: %w", err)
}
defer func() {
if err := r.ffx.Close(); err != nil {
fmt.Printf("ffx.Close: %v\n", err)
}
}()
if err := r.setupFfx(); err != nil {
return fmt.Errorf("setupFfx: %w", err)
}
defer r.stopDaemon()
productDir := ""
if in.Target().TransferURL != "" {
fmt.Println("=== orchestrate - Downloading Product Bundle (2/6) ===")
var err error
productDir, err = r.downloadProductBundle(in)
if err != nil {
return fmt.Errorf("downloadProductBundle: %w", err)
}
} else if in.Target().LocalPB != "" {
fmt.Println("=== orchestrate - Local Product Bundle (2/6) ===")
productDir = in.Target().LocalPB
}
if in.IsHardware() {
fmt.Println("=== orchestrate - Flashing Device (3/6) ===")
if err := r.flashDevice(productDir); err != nil {
return fmt.Errorf("flashDevice: %w", err)
}
} else if in.IsEmulator() {
fmt.Println("=== orchestrate - Starting Emulator (3/6) ===")
if err := r.startEmulator(productDir); err != nil {
return fmt.Errorf("startEmulator: %w", err)
}
defer r.stopEmulator()
}
fmt.Println("=== orchestrate - Serving Packages (4/6) ===")
if err := r.servePackages(in, productDir); err != nil {
return fmt.Errorf("servePackages: %w", err)
}
defer r.stopPackageServer()
fmt.Println("=== orchestrate - Reach Device (5/6) ===")
if err := r.reachDevice(); err != nil {
return fmt.Errorf("reachDevice: %w", err)
}
defer r.stopFfxLog()
} else {
fmt.Println("=== orchestrate - Skipped Target Provisioning (1-5/6) ===")
}
fmt.Println("=== orchestrate - Test (6/6) ===")
if err := r.test(testCmd, in); err != nil {
return fmt.Errorf("test: %w", err)
}
return nil
}
/* Step 0 - Downloading CIPD packages. */
func (r *TestOrchestrator) cipdEnsure(in *RunInput) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("os.Getwd: %w", err)
}
for destPath, cipdSpec := range in.Cipd() {
split := strings.SplitN(cipdSpec, ":", 2)
ensureLine := fmt.Sprintf("%s\t%s\n", split[0], split[1])
cipdCmd := []string{
"cipd",
"ensure",
"-ensure-file",
"-",
"-root",
filepath.Join(wd, destPath),
"-service-account-json",
":gce",
}
fmt.Printf("Running command: %+v stdin: %s", cipdCmd, ensureLine)
cmd := exec.Command(cipdCmd[0], cipdCmd[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = strings.NewReader(ensureLine)
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
return fmt.Errorf("cmd.Run: %w", err)
}
}
return nil
}
/* Step 1 - Setting up ffx. */
func (r *TestOrchestrator) setupFfx() error {
cmds := [][]string{
{"config", "set", "log.level", "Debug"},
{"config", "set", "test.experimental_json_input", "true"},
{"config", "set", "fastboot.flash.timeout_rate", "4"},
{"config", "set", "discovery.mdns.enabled", "false"},
{"config", "set", "fastboot.usb.disabled", "true"},
{"config", "set", "proactive_log.enabled", "false"},
{"config", "set", "daemon.autostart", "false"},
{"config", "set", "overnet.cso", "only"},
{"config", "set", "ffx-repo-add", "true"},
}
for _, cmd := range cmds {
if out, err := r.ffx.RunCmdSync(cmd...); err != nil {
return fmt.Errorf("ffx setup %v: %w out: %s", cmd, err, out)
}
}
if err := r.dumpFfxConfig(); err != nil {
return fmt.Errorf("dumpFfxConfig: %w", err)
}
if err := r.daemonStart(); err != nil {
return fmt.Errorf("ffx daemon start: %w", err)
}
if err := r.ffx.WaitForDaemon(context.Background()); err != nil {
return fmt.Errorf("ffx daemon wait: %w", err)
}
return nil
}
func (r *TestOrchestrator) dumpFfxConfig() error {
logFile, err := os.Create(ffxConfigDump)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
defer func() {
if err := logFile.Close(); err != nil {
fmt.Printf("logFile.Close: %v\n", err)
}
}()
cmd := r.ffx.Cmd("config", "get")
cmd.Stdout = logFile
cmd.Stderr = logFile
return cmd.Run()
}
func (r *TestOrchestrator) daemonStart() error {
logFile, err := os.Create(ffxDaemonLog)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("os.Getwd: %w", err)
}
cmd := r.ffx.Cmd("daemon", "start")
sshDir := filepath.Join(wd, "openssh-portable", "bin")
cmd.Env = appendPath(cmd.Env, sshDir)
cmd.Stdout = logFile
cmd.Stderr = logFile
return cmd.Start()
}
func appendPath(environ []string, dirs ...string) []string {
result := []string{}
for _, e := range environ {
if strings.HasPrefix(e, "PATH=") {
result = append(result, fmt.Sprintf("%s:%s", e, strings.Join(dirs, ":")))
} else {
result = append(result, e)
}
}
return result
}
/* Step 2 - Downloading product bundle. */
func (r *TestOrchestrator) downloadProductBundle(in *RunInput) (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("os.Getwd: %w", err)
}
dir := filepath.Join(wd, "ffx-product-bundle")
ffxArgs := []string{
"product",
"download",
in.Target().TransferURL,
dir,
}
if in.Target().FfxluciauthPath != "" {
ffxArgs = append(ffxArgs, "--auth", in.Target().FfxluciauthPath)
}
_, err = r.ffx.RunCmdSync(ffxArgs...)
if err != nil {
return "", fmt.Errorf("ffx product download: %w", err)
}
return dir, nil
}
/* Step 3 - Flashing device OR Starting emulator. */
func (r *TestOrchestrator) flashDevice(productDir string) error {
if err := r.ffx.Flash(r.deviceConfig.FastbootSerial, productDir, ""); err != nil {
return fmt.Errorf("ffx flash: %w", err)
}
return nil
}
func (r *TestOrchestrator) startEmulator(productDir string) error {
if _, err := r.ffx.RunCmdSync("emu", "start", productDir, "--net", "user", "--headless"); err != nil {
return fmt.Errorf("ffx emu start: %w", err)
}
return nil
}
/* Step 4 - Serving packages. */
func (r *TestOrchestrator) servePackages(in *RunInput, productDir string) error {
if out, err := r.ffx.RunCmdSync("repository", "add-from-pm", productDir); err != nil {
return fmt.Errorf("ffx repository add-from-pm: %w out: %s", err, out)
}
// It is important to always publish, even if there is nothing in
// in.Target().PackageArchives, because it will force the package metadata
// to be refreshed (see b/309847820).
publishArgs := []string{"repository", "publish", productDir}
for _, far := range in.Target().PackageArchives {
publishArgs = append(publishArgs, "--package-archive", far)
}
if out, err := r.ffx.RunCmdSync(publishArgs...); err != nil {
return fmt.Errorf("ffx %v: %w out: %v", publishArgs, err, out)
}
for _, buildID := range in.Target().BuildIds {
if out, err := r.ffx.RunCmdSync("debug", "symbol-index", "add", buildID); err != nil {
return fmt.Errorf("ffx debug symbol-index add %s: %w out: %s", buildID, err, out)
}
}
if err := r.serveAndWait(); err != nil {
return fmt.Errorf("serveAndWait: %w", err)
}
if _, err := r.ffx.RunCmdSync("repository", "list"); err != nil {
return fmt.Errorf("ffx repository list: %w", err)
}
return nil
}
func (r *TestOrchestrator) serveAndWait() error {
port := os.Getenv("FUCHSIA_PACKAGE_SERVER_PORT")
if port == "" {
port = "8083"
}
addr := fmt.Sprintf("[::]:%s", port)
if _, err := r.ffx.RunCmdAsync("repository", "server", "start", "--address", addr); err != nil {
return fmt.Errorf("ffx repository server start: %w", err)
}
return utils.RunWithRetries(context.Background(), 500*time.Millisecond, 5, func() error {
req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%s", port), nil)
if err != nil {
return fmt.Errorf("http.NewRequest: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("http.DefaultClient.Do: %w", err)
}
// Check the response status code
if resp.StatusCode != 200 {
return fmt.Errorf("resp.StatusCode: got %d, want 200", resp.StatusCode)
}
return nil
})
}
/* Step 5 - Reach Device */
func (r *TestOrchestrator) reachDevice() error {
if r.deviceConfig != nil {
addr := r.deviceConfig.Network.IPv4
if _, err := r.ffx.RunCmdSync("target", "add", addr, "--nowait"); err != nil {
return fmt.Errorf("ffx target add: %w", err)
}
}
if _, err := r.ffx.RunCmdSync("target", "wait"); err != nil {
return fmt.Errorf("ffx target wait: %w", err)
}
if _, err := r.ffx.RunCmdSync("target", "list"); err != nil {
return fmt.Errorf("ffx target list: %w", err)
}
if err := r.dumpFfxLog(); err != nil {
return fmt.Errorf("dumpFfxLog: %w", err)
}
if out, err := r.ffx.RunCmdSync(
"target",
"repository",
"register",
"--repository",
"devhost",
"--alias",
"fuchsia.com",
"--alias",
"chromium.org"); err != nil {
return fmt.Errorf("ffx target repository register: %w out: %s", err, out)
}
return nil
}
func (r *TestOrchestrator) dumpFfxLog() error {
logFile, err := os.Create(targetLog)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
r.targetLogFile = logFile
cmd := r.ffx.Cmd("log", "--no-symbolize")
cmd.Stdout = logFile
cmd.Stderr = logFile
if err := cmd.Start(); err != nil {
return fmt.Errorf("cmd.Start: %w", err)
}
go func() {
if err := cmd.Wait(); err != nil {
fmt.Printf("cmd.Wait: %v", err)
}
}()
r.ffxLogProc = cmd.Process
return nil
}
/* Step 6 - Test */
func (r *TestOrchestrator) test(testCmd []string, in *RunInput) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("os.Getwd: %w", err)
}
logFile, err := os.Create(subrunnerLog)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
defer func() {
if err := logFile.Close(); err != nil {
fmt.Printf("logFile.Close: %v\n", err)
}
}()
// Prepare the env for target tests:
// 1. Applies default ffx cmd environment variables
// (eg: isolation, disabling analytics).
// 2. Adds ffx so that downstream can call "ffx" without having to leak its
// full path.
// 3. Add openssh to PATH.
env := os.Environ()
if in.IsTarget() {
env = r.ffx.ApplyEnv(env)
ffxDir := filepath.Dir(filepath.Join(wd, in.Target().FfxPath))
if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", ffxDir, os.Getenv("PATH"))); err != nil {
return fmt.Errorf("os.Setenv: %w", err)
}
sshDir := filepath.Join(wd, "openssh-portable", "bin")
env = appendPath(env, sshDir, ffxDir)
}
// Create cmd AFTER setting the PATH so that it will correctly resolve testCmd[0]
cmd := exec.Command(testCmd[0], testCmd[1:]...)
cmd.Env = env
// Setup pipes to forward subcmd stdout and stderr to logFile and os.Stdout.
pipeOut := io.MultiWriter(logFile, os.Stdout)
cmd.Stdout = pipeOut
cmd.Stderr = pipeOut
fmt.Printf("Running test: %+v\n", cmd.Args)
testErr := cmd.Run()
fmt.Printf("Pausing 10 seconds for log flush...\n")
time.Sleep(10 * time.Second)
if in.IsTarget() {
if _, err := r.ffx.RunCmdSync("target", "snapshot", "-d", os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR")); err != nil {
fmt.Printf("target snapshot: %v\n", err)
}
}
if err := r.writeTestSummary(testErr); err != nil {
return fmt.Errorf("writeTestSummary: %w", err)
}
// TODO(b/322928092): Disable and remove this once `orchestrate` is the
// entrypoint for all bazel_build_test_upload invocations.
if in.HasExperiment("orchestrate-error-on-test-failure") && testErr != nil {
return fmt.Errorf("Test Failures: %w", err)
}
return nil
}
// testSummary determines the data for out/summary.json
type testSummary struct {
Success bool `json:"success"`
}
func (r *TestOrchestrator) writeTestSummary(testErr error) error {
if testErr != nil {
fmt.Printf("Tests failed: %v\n", testErr)
}
summary := &testSummary{
Success: testErr == nil,
}
if err := os.MkdirAll(filepath.Dir(summaryPath), 0755); err != nil {
return fmt.Errorf("os.MkdirAll: %w", err)
}
if err := writeJSON(summaryPath, summary); err != nil {
return fmt.Errorf("writeJSON: %w", err)
}
return nil
}
func writeJSON(filename string, data any) error {
rawData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("json.MarshalIndent: %w", err)
}
if err = os.WriteFile(filename, rawData, 0644); err != nil {
return fmt.Errorf("os.WriteFile: %w", err)
}
return nil
}
/* Cleanup */
func (r *TestOrchestrator) stopPackageServer() {
if _, err := r.ffx.RunCmdSync("repository", "server", "stop"); err != nil {
fmt.Printf("ffx repository server stop: %v", err)
}
}
func (r *TestOrchestrator) stopEmulator() {
if _, err := r.ffx.RunCmdSync("emu", "stop", "--all"); err != nil {
fmt.Printf("ffx emu stop: %v", err)
}
}
func (r *TestOrchestrator) stopDaemon() {
if _, err := r.ffx.RunCmdSync("daemon", "stop", "--no-wait"); err != nil {
fmt.Printf("ffx daemon stop: %v", err)
}
}
func (r *TestOrchestrator) stopFfxLog() {
if r.ffxLogProc == nil {
return
}
if err := r.ffxLogProc.Kill(); err != nil {
fmt.Printf("ffxLogProc.Kill: %v\n", err)
}
if err := r.targetLogFile.Close(); err != nil {
fmt.Printf("targetLogFile.Close: %v\n", err)
}
// Symbolize logs
if err := r.Symbolize(targetLog, targetSymLog); err != nil {
fmt.Printf("Symbolize: %v\n", err)
}
}
// Symbolize uses ffx to symbolize the log output.
func (r *TestOrchestrator) Symbolize(input, output string) error {
logFile, err := os.Open(input)
if err != nil {
return fmt.Errorf("os.Open(%q): %w", input, err)
}
defer func() {
if err := logFile.Close(); err != nil {
fmt.Printf("logFile.Close: %v\n", err)
}
}()
symbolizedFile, err := os.Create(output)
if err != nil {
return fmt.Errorf("os.Create(%q): %w", output, err)
}
defer func() {
if err := symbolizedFile.Close(); err != nil {
fmt.Printf("symbolizedFile.Close: %v\n", err)
}
}()
cmd := r.ffx.Cmd("debug", "symbolize")
cmd.Stdin = logFile
cmd.Stdout = symbolizedFile
cmd.Stderr = symbolizedFile
return cmd.Run()
}