blob: a58a37dfb5520ba8f324ee37c6f46a5103e9b486 [file] [log] [blame]
// Copyright 2017 The Chromium OS 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 runner provides functionality shared by test executables.
package runner
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"time"
"chromiumos/tast/common/control"
"chromiumos/tast/common/testing"
)
const (
// Number of characters in prefixes from the log package, e.g. "2017/08/17 09:29:54 ".
logPrefixLen = 20
)
// TestsToRun returns tests to run for a command invoked with args.
//
// If no arguments are supplied, all registered tests are returned.
// If a single argument is supplied and it is surrounded by parentheses,
// it is treated as a boolean expression specifying test attributes.
// Otherwise, argument(s) are interpreted as wildcard patterns matching test names.
//
// If any error was encountered while registering tests, it is returned instead.
func TestsToRun(args []string) ([]*testing.Test, error) {
if errs := testing.RegistrationErrors(); len(errs) > 0 {
es := make([]string, len(errs))
for i, err := range errs {
es[i] = err.Error()
}
return nil, errors.New(strings.Join(es, "\n"))
}
if len(args) == 0 {
return testing.GlobalRegistry().AllTests(), nil
}
if len(args) == 1 && strings.HasPrefix(args[0], "(") && strings.HasSuffix(args[0], ")") {
return testing.GlobalRegistry().TestsForAttrExpr(args[0][1 : len(args[0])-1])
}
// Print a helpful error message if it looks like the user wanted an attribute expression.
if len(args) == 1 && (strings.Contains(args[0], "&&") || strings.Contains(args[0], "||")) {
return nil, fmt.Errorf("attr expr %q must be within parentheses", args[0])
}
return testing.GlobalRegistry().TestsForPatterns(args)
}
// Log writes a RunLog control message to mw (if non-nil) or stdout.
func Log(mw *control.MessageWriter, msg string) {
if mw != nil {
mw.WriteMessage(&control.RunLog{time.Now(), msg})
} else {
log.Print(msg)
}
}
// Abort writes a RunError control message to mw (if non-nil) and logs
// a fatal error.
func Abort(mw *control.MessageWriter, msg string) {
if mw != nil {
_, fn, ln, _ := runtime.Caller(1)
mw.WriteMessage(&control.RunError{time.Now(), testing.Error{
Reason: msg,
File: fn,
Line: ln,
Stack: string(debug.Stack()),
}})
}
log.Fatal(msg)
}
// indent indents each line of s using prefix.
func indent(s, prefix string) string {
lines := strings.Split(s, "\n")
for i := range lines {
lines[i] = prefix + lines[i]
}
return strings.Join(lines, "\n")
}
// copyTestOutput reads test output from ch and writes it to mw.
// If mw is nil, the output is just logged to os.Stdout.
// true is returned if the test suceeded.
func copyTestOutput(ch chan testing.Output, mw *control.MessageWriter) (succeeded bool) {
succeeded = true
for o := range ch {
if o.Err != nil {
succeeded = false
if mw != nil {
mw.WriteMessage(&control.TestError{o.T, *o.Err})
} else {
stack := indent(strings.TrimSpace(o.Err.Stack), strings.Repeat(" ", logPrefixLen))
log.Printf("Error: [%s:%d] %v\n%s", o.Err.File, o.Err.Line, o.Err.Reason, stack)
}
} else {
if mw != nil {
mw.WriteMessage(&control.TestLog{o.T, o.Msg})
} else {
log.Print(o.Msg)
}
}
}
return succeeded
}
// RunConfig contains a test-running configuration to be passed to RunTests.
type RunConfig struct {
// Ctx is the context to be passed to tests.
Ctx context.Context
// Tests contains tests to run.
Tests []*testing.Test
// MessageWriter is used to send messages to the controlling process.
// If nil, output is logged to stdout instead.
MessageWriter *control.MessageWriter
// SetupFunc is run before every test if non-nil.
SetupFunc func() error
// BaseOutDir contains the base directory under which test output will be written.
BaseOutDir string
// DataDir contains the base directory under which test data files are located.
DataDir string
// DefaultTestTimeout contains the default maximum time allotted to each test.
// This is only used if testing.Test.Timeout is unset.
DefaultTestTimeout time.Duration
}
// RunTests runs tests as dictated by cfg. The number of failing tests is returned.
// If an error is encountered in the test harness (as opposed to in a test),
// it is returned immediately.
func RunTests(cfg RunConfig) (numFailed int, err error) {
for _, t := range cfg.Tests {
// Make a copy of the test with the default timeout if none was specified.
test := *t
if test.Timeout == 0 {
test.Timeout = cfg.DefaultTestTimeout
}
if cfg.MessageWriter != nil {
cfg.MessageWriter.WriteMessage(&control.TestStart{time.Now(), test.Name, test})
} else {
log.Print("Running ", test.Name)
}
outDir := filepath.Join(cfg.BaseOutDir, test.Name)
if err := os.MkdirAll(outDir, 0755); err != nil {
return 0, err
}
if cfg.SetupFunc != nil {
cfg.SetupFunc()
}
ch := make(chan testing.Output)
s := testing.NewState(cfg.Ctx, ch, filepath.Join(cfg.DataDir, test.DataDir()), outDir, test.Timeout)
done := make(chan bool, 1)
go func() {
if succeeded := copyTestOutput(ch, cfg.MessageWriter); !succeeded {
numFailed++
}
done <- true
}()
test.Run(s)
close(ch)
<-done
if cfg.MessageWriter != nil {
cfg.MessageWriter.WriteMessage(&control.TestEnd{time.Now(), test.Name})
} else {
log.Printf("Finished %s", test.Name)
}
}
return numFailed, nil
}