blob: 727d3a11dc64e9a9e6923b0977f2db0a49504915 [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 (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"sort"
"strings"
"time"
"fuchsia.googlesource.com/tools/botanist"
"fuchsia.googlesource.com/tools/runtests"
"fuchsia.googlesource.com/tools/tap"
"fuchsia.googlesource.com/tools/testrunner"
"fuchsia.googlesource.com/tools/testsharder"
"golang.org/x/crypto/ssh"
)
// TODO(IN-824): Produce a tar archive of all output files.
const (
// Default amount of time to wait before failing to perform any IO action.
defaultIOTimeout = 1 * time.Minute
// The username used to authenticate with the Fuchsia device.
sshUser = "fuchsia"
// The test output directory to create on the Fuchsia device.
fuchsiaOutputDir = "/data/infra/testrunner"
)
// Command-line flags
var (
// Whether to show Usage and exit.
help bool
// The directory where output should be written. This path will contain both
// summary.json and the set of output files for each test.
outputDir string
// The path to a file containing properties of the Fuchsia device to use for testing.
deviceFilepath string
)
// TestRecorder records the details of test run.
type TestRecorder func(runtests.TestDetails)
func usage() {
fmt.Println(`
testrunner [flags] tests-file
Executes all tests found in the JSON [tests-file]
Required environment variables:
"NODENAME": The nodename of the attached Fuchsia device to use for testing. This
can usually be found using the 'netls' tool.
"SSH_KEY": Path to the SSH private key used to connect to the Fuchsia device.
`)
}
func init() {
flag.BoolVar(&help, "help", false, "Whether to show Usage and exit.")
flag.StringVar(&outputDir, "output", "", "Directory where output should be written")
flag.Usage = usage
}
func main() {
flag.Parse()
if help || flag.NArg() != 1 {
flag.Usage()
flag.PrintDefaults()
return
}
if outputDir == "" {
log.Fatal("-output is required")
}
// Load tests.
testsPath := flag.Arg(0)
tests, err := testrunner.LoadTests(testsPath)
if err != nil {
log.Fatalf("failed to load tests from %q: %v", testsPath, err)
}
// Prepare outputs.
tapp := tap.NewProducer(os.Stdout)
tapp.Plan(len(tests))
var summary runtests.TestSummary
recordDetails := func(details runtests.TestDetails) {
tapp.Ok(details.Result == runtests.TestSuccess, details.Name)
summary.Tests = append(summary.Tests, details)
}
// Execute.
if err := execute(tests, recordDetails); err != nil {
log.Fatal(err)
}
// Write Summary.
file, err := os.Create(path.Join(outputDir, "summary.json"))
if err != nil {
log.Fatal(err)
}
encoder := json.NewEncoder(file)
if err := encoder.Encode(summary); err != nil {
log.Fatal(err)
}
}
func execute(tests []testsharder.Test, recorder TestRecorder) error {
// Validate inputs.
nodename := os.Getenv("NODENAME")
if nodename == "" {
return errors.New("missing environment variable NODENAME")
}
privateKeyPath := os.Getenv("SSH_KEY")
if privateKeyPath == "" {
return errors.New("missing environment variable SSH_KEY")
}
// Initialize the connection to the Fuchsia device.
sshClient, err := sshIntoNode(nodename, privateKeyPath)
if err != nil {
return fmt.Errorf("failed to connect to node %q: %v", nodename, err)
}
fuchsiaTester := &FuchsiaTester{
remoteOutputDir: fuchsiaOutputDir,
delegate: &SSHTester{
client: sshClient,
},
}
// Partition the tests into groups according to OS.
groups := groupTests(tests, func(test testsharder.Test) string {
sys := strings.ToLower(test.OS)
switch sys {
case "fuchsia", "linux", "mac":
return sys
}
return "unknown"
})
// Fail fast if any test cannot be run.
if unknownTests, ok := groups["unknown"]; ok {
return fmt.Errorf("could not determine the runtime system for following tests %v", unknownTests)
}
// Execute UNIX tests locally, assuming we're running in a UNIX environment.
var localTests []testsharder.Test
localTests = append(localTests, groups["linux"]...)
localTests = append(localTests, groups["mac"]...)
if len(localTests) > 0 {
if err := runTests(localTests, RunTestInSubprocess, outputDir, recorder); err != nil {
return err
}
}
// Execute Fuchsia tests.
if fuchsiaTests, ok := groups["fuchsia"]; ok {
// TODO(IN-824): Record log_listener output.
if err := runTests(fuchsiaTests, fuchsiaTester.Test, outputDir, recorder); err != nil {
return err
}
}
return nil
}
// groupTests splits a list of tests into named subgroups according to the names returned
// by `name`. Within any subgroup, the list of tests is sorted by test name.
func groupTests(input []testsharder.Test, name func(testsharder.Test) string) map[string][]testsharder.Test {
tests := make([]testsharder.Test, len(input))
copy(tests, input)
sort.SliceStable(tests, func(i, j int) bool {
return tests[i].Name < tests[j].Name
})
output := make(map[string][]testsharder.Test)
for _, test := range tests {
group := name(test)
output[group] = append(output[group], test)
}
return output
}
func sshIntoNode(nodename, privateKeyPath string) (*ssh.Client, error) {
privateKey, err := ioutil.ReadFile(privateKeyPath)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: sshUser,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
Timeout: defaultIOTimeout,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
return botanist.SSHIntoNode(context.Background(), nodename, config)
}
func runTests(tests []testsharder.Test, tester Tester, outputDir string, record TestRecorder) error {
for _, test := range tests {
details, err := runTest(context.Background(), test, tester, outputDir)
if err != nil {
log.Println(err)
}
if details != nil {
record(*details)
}
}
return nil
}
func runTest(ctx context.Context, test testsharder.Test, tester Tester, outputDir string) (*runtests.TestDetails, error) {
// Prepare an output file for the test.
workspace := path.Join(outputDir, test.Name)
if err := os.MkdirAll(workspace, os.FileMode(0755)); err != nil {
return nil, err
}
output, err := os.Create(path.Join(workspace, runtests.TestOutputFilename))
if err != nil {
return nil, err
}
defer output.Close()
// Execute the test.
result := runtests.TestSuccess
multistdout := io.MultiWriter(output, os.Stdout)
multistderr := io.MultiWriter(output, os.Stderr)
if err := tester(ctx, test, multistdout, multistderr); err != nil {
result = runtests.TestFailure
log.Println(err)
}
// Record the test details in the summary.
return &runtests.TestDetails{
Name: test.Name,
OutputFile: output.Name(),
Result: result,
}, nil
}