| // 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" |
| "time" |
| |
| "fuchsia.googlesource.com/tools/botanist" |
| "fuchsia.googlesource.com/tools/runtests" |
| "fuchsia.googlesource.com/tools/testrunner" |
| "fuchsia.googlesource.com/tools/testsharder" |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| 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 filename to use for log_listener's stdout captured during Fuchsia tests. |
| syslogStdoutFilename = "syslog-stdout.txt" |
| |
| // The filename to use for log_listener's stderr captured during Fuchsia tests. |
| syslogStderrFilename = "syslog-stderr.txt" |
| ) |
| |
| // Command-line flags |
| var ( |
| // Whether to show Usage and exit. |
| help bool |
| |
| // The path where a tar archive containing test results should be created. |
| archive string |
| |
| // Working directory of the local testing subprocesses. |
| localWD string |
| ) |
| |
| // TestRunnerOutput manages the output of this test runner. |
| type TestRunnerOutput struct { |
| Summary *SummaryOutput |
| TAP *TAPOutput |
| Tar *TarOutput |
| } |
| |
| func (o *TestRunnerOutput) Record(result testrunner.TestResult) { |
| if o.Summary != nil { |
| o.Summary.Record(result) |
| } |
| |
| if o.TAP != nil { |
| o.TAP.Record(result) |
| } |
| |
| if o.Tar != nil { |
| o.Tar.Record(result) |
| } |
| } |
| |
| // Complete finishes producing output for the test run. |
| func (o *TestRunnerOutput) Complete() error { |
| if o.Tar == nil { |
| return nil |
| } |
| |
| bytes, err := json.Marshal(o.Summary.Summary) |
| if err != nil { |
| return err |
| } |
| |
| if err := o.Tar.TarFile(bytes, runtests.TestSummaryFilename); err != nil { |
| return err |
| } |
| |
| return o.Tar.Close() |
| } |
| |
| func usage() { |
| fmt.Println(`testrunner [flags] tests-file |
| |
| Executes all tests found in the JSON [tests-file] |
| Fuchsia tests require botanist.DeviceContext to have been registered and in the current |
| environment; for more details see |
| https://fuchsia.googlesource.com/tools/+/master/botanist/context.go.`) |
| } |
| |
| func init() { |
| flag.BoolVar(&help, "help", false, "Whether to show Usage and exit.") |
| flag.StringVar(&archive, "archive", "", "Optional path where a tar archive 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.Usage = usage |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| if help || flag.NArg() != 1 { |
| flag.Usage() |
| flag.PrintDefaults() |
| return |
| } |
| |
| // 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 test output drivers. |
| output := &TestRunnerOutput{ |
| TAP: NewTAPOutput(os.Stdout, len(tests)), |
| Summary: &SummaryOutput{}, |
| } |
| defer output.Complete() |
| |
| // Add an archive Output if specified. |
| if archive != "" { |
| output.Tar, err = NewTarOutput(archive) |
| if err != nil { |
| log.Fatalf("failed to initialize tar recorder: %v", err) |
| } |
| } |
| |
| // Prepare the Fuchsia DeviceContext, which will be nil if the testrunner is |
| // not run by botanist, which is not a failure mode. |
| devCtx, err := botanist.GetDeviceContext() |
| if err != nil { |
| log.Print(err) |
| } |
| |
| // Execute. |
| if err := execute(tests, output, devCtx); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| func execute(tests []testsharder.Test, output *TestRunnerOutput, devCtx *botanist.DeviceContext) error { |
| var linux, mac, fuchsia, unknown []testsharder.Test |
| for _, test := range tests { |
| switch test.OS { |
| case testsharder.Fuchsia: |
| fuchsia = append(fuchsia, test) |
| case testsharder.Linux: |
| linux = append(linux, test) |
| case testsharder.Mac: |
| mac = append(mac, test) |
| default: |
| unknown = append(unknown, test) |
| } |
| } |
| |
| if len(unknown) > 0 { |
| return fmt.Errorf("could not determine the runtime system for following tests %v", unknown) |
| } |
| |
| localTester := &SubprocessTester{ |
| wd: localWD, |
| } |
| if devCtx != nil { |
| localTester.env = []string{devCtx.EnvironEntry()} |
| } |
| |
| if err := runTests(linux, localTester.Test, output); err != nil { |
| return err |
| } |
| |
| if err := runTests(mac, localTester.Test, output); err != nil { |
| return err |
| } |
| |
| return runFuchsiaTests(fuchsia, output, devCtx) |
| } |
| |
| func sshIntoNode(nodename, privateKey string) (*ssh.Client, error) { |
| signer, err := ssh.ParsePrivateKey([]byte(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 runFuchsiaTests(tests []testsharder.Test, output *TestRunnerOutput, devCtx *botanist.DeviceContext) error { |
| if devCtx == nil { |
| return errors.New("no DeviceContext set; cannot execute fuchsia tests") |
| } |
| |
| tester, err := NewFuchsiaTester(*devCtx) |
| if err != nil { |
| return fmt.Errorf("failed to initialize fuchsia tester: %v", err) |
| } |
| defer tester.Close() |
| |
| // Ensure the syslog is always included in the output, even if tests fail. |
| // TODO(IN-824): Run fuchsia/linux/mac tests in go-routines and emit errors on channels. |
| defer func() { |
| if output.Tar == nil { |
| return |
| } |
| |
| stdout, stderr := tester.SyslogOutput() |
| stdoutBytes, err := ioutil.ReadAll(stdout) |
| if err != nil { |
| log.Println(err) |
| return |
| } |
| stderrBytes, err := ioutil.ReadAll(stderr) |
| if err != nil { |
| log.Println(err) |
| return |
| } |
| |
| if err := output.Tar.TarFile(stdoutBytes, syslogStdoutFilename); err != nil { |
| log.Println(err) |
| } |
| |
| if err := output.Tar.TarFile(stderrBytes, syslogStderrFilename); err != nil { |
| log.Println(err) |
| } |
| }() |
| |
| return runTests(tests, tester.Test, output) |
| } |
| |
| func runTests(tests []testsharder.Test, tester Tester, output *TestRunnerOutput) error { |
| for _, test := range tests { |
| result, err := runTest(context.Background(), test, tester) |
| if err != nil { |
| log.Println(err) |
| } |
| if result != nil { |
| output.Record(*result) |
| } |
| } |
| return nil |
| } |
| |
| func runTest(ctx context.Context, test testsharder.Test, tester Tester) (*testrunner.TestResult, error) { |
| result := runtests.TestSuccess |
| output := new(bytes.Buffer) |
| multistdout := io.MultiWriter(output, os.Stdout) |
| multistderr := io.MultiWriter(output, os.Stderr) |
| |
| startTime := time.Now() |
| |
| if err := tester(ctx, test, multistdout, multistderr); err != nil { |
| result = runtests.TestFailure |
| log.Println(err) |
| } |
| |
| endTime := time.Now() |
| |
| // Record the test details in the summary. |
| return &testrunner.TestResult{ |
| Name: test.Name, |
| Output: output, |
| Result: result, |
| StartTime: startTime, |
| EndTime: endTime, |
| }, nil |
| } |