blob: a62f8b6bc060b6a385d62fbf6a660c96170bfa45 [file] [log] [blame]
// Copyright 2019 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 main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"time"
build "go.fuchsia.dev/fuchsia/tools/build/lib"
"go.fuchsia.dev/fuchsia/tools/lib/color"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/net/sshutil"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
tap "go.fuchsia.dev/fuchsia/tools/testing/tap/lib"
testparser "go.fuchsia.dev/fuchsia/tools/testing/testparser/lib"
testrunner "go.fuchsia.dev/fuchsia/tools/testing/testrunner/lib"
"go.fuchsia.dev/fuchsia/tools/testing/util"
)
// Fuchsia-specific environment variables possibly exposed to the testrunner.
const (
nodenameEnvVar = "FUCHSIA_NODENAME"
sshKeyEnvVar = "FUCHSIA_SSH_KEY"
// A directory that will be automatically isolated on completion of a task.
testOutdirEnvVar = "FUCHSIA_TEST_OUTDIR"
)
// Command-line flags
var (
// Whether to show Usage and exit.
help bool
// The path where a directory containing test results should be created.
outDir string
// Working directory of the local testing subprocesses.
localWD string
// Whether to use runtests when executing tests on fuchsia. If false, the
// default will be run_test_component.
useRuntests bool
// Per-test timeout.
perTestTimeout time.Duration
)
func usage() {
fmt.Printf(`testrunner [flags] tests-file
Executes all tests found in the JSON [tests-file]
Fuchsia tests require both the nodename of the fuchsia instance and a private
SSH key corresponding to a authorized key to be set in the environment under
%s and %s respectively.
`, nodenameEnvVar, sshKeyEnvVar)
}
func init() {
flag.BoolVar(&help, "help", false, "Whether to show Usage and exit.")
flag.StringVar(&outDir, "out-dir", "", "Optional path where a directory containing test results should be created.")
flag.StringVar(&localWD, "C", "", "Working directory of local testing subprocesses; if unset the current working directory will be used.")
flag.BoolVar(&useRuntests, "use-runtests", false, "Whether to default to running fuchsia tests with runtests; if false, run_test_component will be used.")
// TODO(fxb/36480): Support different timeouts for different tests.
flag.DurationVar(&perTestTimeout, "per-test-timeout", 0, "Per-test timeout, applied to all tests. Ignored if <= 0.")
flag.Usage = usage
}
func main() {
flag.Parse()
if help || flag.NArg() != 1 {
flag.Usage()
flag.PrintDefaults()
return
}
l := logger.NewLogger(logger.DebugLevel, color.NewColor(color.ColorAuto), os.Stdout, os.Stderr, "testrunner ")
ctx := logger.WithLogger(context.Background(), l)
testsPath := flag.Arg(0)
tests, err := loadTests(testsPath)
if err != nil {
log.Fatalf("failed to load tests from %q: %v", testsPath, err)
}
// Configure a test outputs object, responsible for producing TAP output,
// recording data sinks, and archiving other test outputs.
testOutdir := filepath.Join(os.Getenv(testOutdirEnvVar), outDir)
if testOutdir == "" {
var err error
testOutdir, err = ioutil.TempDir("", "testrunner")
if err != nil {
log.Fatalf("failed to create a test output directory")
}
}
logger.Debugf(ctx, "test output directory: %s", testOutdir)
tapProducer := tap.NewProducer(os.Stdout)
tapProducer.Plan(len(tests))
outputs, err := createTestOutputs(tapProducer, testOutdir)
if err != nil {
log.Fatalf("failed to create test results object: %v", err)
}
defer outputs.Close()
nodename := os.Getenv(nodenameEnvVar)
sshKeyFile := os.Getenv(sshKeyEnvVar)
if err := execute(ctx, tests, outputs, nodename, sshKeyFile); err != nil {
log.Fatal(err)
}
}
func loadTests(path string) ([]build.Test, error) {
bytes, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", path, err)
}
var tests []build.Test
if err := json.Unmarshal(bytes, &tests); err != nil {
return nil, fmt.Errorf("failed to unmarshal %q: %w", path, err)
}
return tests, nil
}
type tester interface {
Test(context.Context, build.Test, io.Writer, io.Writer) (runtests.DataSinkReference, error)
Close() error
CopySinks(context.Context, []runtests.DataSinkReference) error
}
func execute(ctx context.Context, tests []build.Test, outputs *testOutputs, nodename, sshKeyFile string) error {
var localTests, fuchsiaTests []build.Test
for _, test := range tests {
switch test.OS {
case "fuchsia":
fuchsiaTests = append(fuchsiaTests, test)
case "linux":
if runtime.GOOS != "linux" {
return fmt.Errorf("cannot run linux tests when GOOS = %q", runtime.GOOS)
}
localTests = append(localTests, test)
case "mac":
if runtime.GOOS != "darwin" {
return fmt.Errorf("cannot run mac tests when GOOS = %q", runtime.GOOS)
}
localTests = append(localTests, test)
default:
return fmt.Errorf("test %#v has unsupported OS: %q", test, test.OS)
}
}
localEnv := append(os.Environ(),
// Tell tests written in Rust to print stack on failures.
"RUST_BACKTRACE=1",
)
localTester := newSubprocessTester(localWD, localEnv, perTestTimeout)
if err := runTests(ctx, localTests, localTester, outputs); err != nil {
return err
}
if len(fuchsiaTests) == 0 {
return nil
}
var t tester
var err error
if sshKeyFile != "" {
if nodename == "" {
return fmt.Errorf("%s must be set", nodenameEnvVar)
}
t, err = newFuchsiaSSHTester(ctx, nodename, sshKeyFile, outputs.outDir, useRuntests, perTestTimeout)
} else {
// TODO(fxbug.dev/41930): create a serial test runner in this case.
return fmt.Errorf("%s must be set", sshKeyEnvVar)
}
if err != nil {
return fmt.Errorf("failed to initialize fuchsia tester: %v", err)
}
defer t.Close()
return runTests(ctx, fuchsiaTests, t, outputs)
}
func runTests(ctx context.Context, tests []build.Test, t tester, outputs *testOutputs) error {
var sinks []runtests.DataSinkReference
for _, test := range tests {
result, err := runTest(ctx, test, t)
if errors.Is(err, sshutil.ConnectionError) {
return err
}
if err := outputs.record(*result); err != nil {
return err
}
sinks = append(sinks, result.DataSinks)
}
return t.CopySinks(ctx, sinks)
}
func runTest(ctx context.Context, test build.Test, t tester) (*testrunner.TestResult, error) {
result := runtests.TestSuccess
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
multistdout := io.MultiWriter(stdout, os.Stdout)
multistderr := io.MultiWriter(stderr, os.Stderr)
startTime := time.Now()
dataSinks, err := t.Test(ctx, test, multistdout, multistderr)
if err != nil {
result = runtests.TestFailure
logger.Errorf(ctx, err.Error())
if errors.Is(err, sshutil.ConnectionError) {
return nil, err
}
}
endTime := time.Now()
name := util.UniqueName(test)
// If test is a multiplier test, the name should end in a number.
// Re-append that to the result name.
re := regexp.MustCompile(`\([0-9]+\)$`)
index := re.FindString(test.Name)
if index != "" {
name += "-" + index
}
// Record the test details in the summary.
return &testrunner.TestResult{
Name: name,
GNLabel: test.Label,
Stdout: stdout.Bytes(),
Stderr: stderr.Bytes(),
Result: result,
Cases: testparser.Parse(stdout.Bytes()),
StartTime: startTime,
EndTime: endTime,
DataSinks: dataSinks,
}, nil
}