blob: c41ded0be8e2242d9c7ec2385cddf70003afeead [file] [edit]
// 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
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
gotesting "testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"chromiumos/tast/internal/bundle"
"chromiumos/tast/internal/control"
"chromiumos/tast/internal/jsonprotocol"
"chromiumos/tast/internal/testing"
"chromiumos/tast/testutil"
)
const (
// Prefix for bundles created by createBundleSymlinks.
bundlePrefix = "fake_bundle"
)
var (
// fakeFixture1 and fakeFixture2 are fake fixtures which fake bundles have.
fakeFixture1 = &testing.FixtureInstance{Name: "fake1"}
fakeFixture2 = &testing.FixtureInstance{Name: "fake2", Parent: "fake1"}
)
func init() {
// If the binary was executed via a symlink created by createBundleSymlinks,
// behave like a test bundle instead of running unit tests.
if strings.HasPrefix(filepath.Base(os.Args[0]), bundlePrefix) {
os.Exit(runFakeBundle())
}
}
// createBundleSymlinks creates a temporary directory and places symlinks within it
// pointing at the test binary that's currently running. Each symlink is given a name
// that is later parsed by runFakeBundle to determine the bundle's desired behavior.
// The temporary directory's path is returned.
func createBundleSymlinks(t *gotesting.T, bundleTestResults ...[]bool) (dir string) {
exec, err := os.Executable()
if err != nil {
t.Fatal(err)
}
dir = testutil.TempDir(t)
for bn, tests := range bundleTestResults {
var s string
for _, pass := range tests {
if pass {
s += "p"
} else {
s += "f"
}
}
// Symlinks take the form "<prefix>-<bundleNum>-<testResults>", where bundleNum
// is a 0-indexed integer and testResults is a string consisting of 'p' and 'f'
// runes indicating whether each successive test should pass or fail.
name := fmt.Sprintf("%s-%d-%s", bundlePrefix, bn, s)
if err = os.Symlink(exec, filepath.Join(dir, name)); err != nil {
t.Fatal(err)
}
}
return dir
}
// getTestName returns the name to use for the test with the 0-indexed testNum in the
// bundle with the 0-indexed bundleNum.
func getTestName(bundleNum, testNum int) string {
return fmt.Sprintf("pkg.Bundle%dTest%d", bundleNum, testNum)
}
// runFakeBundle is invoked when the test binary that's currently running is executed
// via a symlink previously created by createBundleSymlinks. The symlink name is parsed
// to get the desired behavior, and then tests are registered and executed via the
// bundle package's Local function.
func runFakeBundle() int {
bundleName := filepath.Base(os.Args[0])
parts := strings.Split(bundleName, "-")
if len(parts) != 3 {
log.Fatalf("Unparsable filename %q", os.Args[0])
}
bundleNum, err := strconv.Atoi(parts[1])
if err != nil {
log.Fatalf("Bad bundle number %q in filename %q", parts[1], os.Args[0])
}
reg := testing.NewRegistry(bundleName)
reg.AddFixtureInstance(fakeFixture1)
reg.AddFixtureInstance(fakeFixture2)
for i, res := range parts[2] {
var f testing.TestFunc
if res == 'p' {
f = func(context.Context, *testing.State) {}
} else if res == 'f' {
f = func(ctx context.Context, s *testing.State) { s.Fatal("Failed") }
} else {
log.Fatalf("Bad rune %v in result string %q", res, parts[2])
}
reg.AddTestInstance(&testing.TestInstance{
Name: getTestName(bundleNum, i),
Func: f,
})
}
return bundle.Local(os.Args[1:], os.Stdin, os.Stdout, os.Stderr, reg, bundle.Delegate{})
}
// newBufferWithArgs returns a buffer containing the JSON representation of args.
func newBufferWithArgs(t *gotesting.T, args *jsonprotocol.RunnerArgs) *bytes.Buffer {
b := &bytes.Buffer{}
if args != nil {
if err := json.NewEncoder(b).Encode(args); err != nil {
t.Fatal(err)
}
}
return b
}
// callRun calls Run using the supplied arguments and returns its output, status, and a signature that can
// be used in error messages.
func callRun(t *gotesting.T, clArgs []string, stdinArgs, defaultArgs *jsonprotocol.RunnerArgs, scfg *StaticConfig) (
status int, stdout, stderr *bytes.Buffer, sig string) {
if defaultArgs == nil {
defaultArgs = &jsonprotocol.RunnerArgs{}
}
sig = fmt.Sprintf("Run(%v, %+v)", clArgs, stdinArgs)
stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{}
return Run(clArgs, newBufferWithArgs(t, stdinArgs), stdout, stderr, defaultArgs, scfg), stdout, stderr, sig
}
// readAllMessages reads and returns a slice of pointers to control messages
// from r. It reports a fatal test error if any problems are encountered.
func readAllMessages(t *gotesting.T, r io.Reader) []interface{} {
msgs := make([]interface{}, 0)
mr := control.NewMessageReader(r)
for mr.More() {
msg, err := mr.ReadMessage()
if err != nil {
t.Fatalf("ReadMessage failed: %v", err)
}
msgs = append(msgs, msg)
}
return msgs
}
// gotRunError returns true if there is a control.RunError message in msgs,
// a slice of pointers to control messages.
func gotRunError(msgs []interface{}) bool {
for _, msg := range msgs {
if _, ok := msg.(*control.RunError); ok {
return true
}
}
return false
}
func TestRunListTests(t *gotesting.T) {
// Create one bundle with two tests, one with three, and one with just one.
dir := createBundleSymlinks(t, []bool{true, true}, []bool{true, true, true}, []bool{true})
defer os.RemoveAll(dir)
// All six tests should be printed.
args := jsonprotocol.RunnerArgs{
Mode: jsonprotocol.RunnerListTestsMode,
ListTests: &jsonprotocol.RunnerListTestsArgs{BundleGlob: filepath.Join(dir, "*")},
}
status, stdout, stderr, sig := callRun(t, nil, &args, nil, &StaticConfig{Type: LocalRunner})
if status != statusSuccess {
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
var tests []*jsonprotocol.EntityWithRunnabilityInfo
if err := json.Unmarshal(stdout.Bytes(), &tests); err != nil {
t.Fatalf("%s printed unparsable output %q", sig, stdout.String())
}
if len(tests) != 6 {
t.Errorf("%s printed %v test(s); want 6: %v", sig, len(tests), stdout.String())
}
if stderr.Len() != 0 {
t.Errorf("%s wrote to stderr: %q", sig, stderr.String())
}
bundleCounts := make(map[string]int)
for _, test := range tests {
bundleCounts[test.Bundle]++
}
var counts []int
for _, v := range bundleCounts {
counts = append(counts, v)
}
sort.Ints(counts)
if want := []int{1, 2, 3}; !reflect.DeepEqual(counts, want) {
t.Errorf("counts = %v, want %v", counts, want)
}
}
func TestRunListFixtures(t *gotesting.T) {
dir := createBundleSymlinks(t, []bool{true})
defer os.RemoveAll(dir)
args := jsonprotocol.RunnerArgs{
Mode: jsonprotocol.RunnerListFixturesMode,
ListFixtures: &jsonprotocol.RunnerListFixturesArgs{
BundleGlob: filepath.Join(dir, "*"),
},
}
status, stdout, _, sig := callRun(t, nil, &args, nil, &StaticConfig{Type: LocalRunner})
if status != statusSuccess {
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
var got jsonprotocol.RunnerListFixturesResult
if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
t.Fatalf("%s printed unparsable output %q", sig, stdout.String())
}
bundle := fmt.Sprintf("%s-0-p", bundlePrefix)
bundlePath := filepath.Join(dir, bundle)
want := jsonprotocol.RunnerListFixturesResult{Fixtures: map[string][]*jsonprotocol.EntityInfo{
bundlePath: {
{Name: "fake1", Type: jsonprotocol.EntityFixture, Bundle: bundle},
{Name: "fake2", Fixture: "fake1", Type: jsonprotocol.EntityFixture, Bundle: bundle},
},
}}
sortEntities := cmpopts.SortSlices(func(a, b *jsonprotocol.EntityInfo) bool { return a.Name < b.Name })
if diff := cmp.Diff(got, want, sortEntities); diff != "" {
t.Fatal("Result mismatch (-want +got): ", diff)
}
// Test errors
status, _, _, sig = callRun(t, nil, &jsonprotocol.RunnerArgs{Mode: jsonprotocol.RunnerListFixturesMode /* ListFixtures is nil */}, nil, &StaticConfig{Type: LocalRunner})
if status != statusBadArgs {
t.Fatalf("%s = %v; want %v", sig, status, statusBadArgs)
}
}
func TestRunListTestsNoBundles(t *gotesting.T) {
// Don't create any bundles; this should make the runner fail.
dir := createBundleSymlinks(t)
defer os.RemoveAll(dir)
args := jsonprotocol.RunnerArgs{
Mode: jsonprotocol.RunnerListTestsMode,
ListTests: &jsonprotocol.RunnerListTestsArgs{BundleGlob: filepath.Join(dir, "*")},
}
// The runner should only exit with 0 and report errors via control messages on stdout when it's
// performing an actual test run. Since we're only listing tests, it should instead exit with an
// error (and write the error to stderr, although that's not checked here).
status, stdout, stderr, sig := callRun(t, nil, &args, nil, &StaticConfig{Type: LocalRunner})
if status != statusNoBundles {
t.Errorf("%s = %v; want %v", sig, status, statusNoBundles)
}
if stdout.Len() != 0 {
t.Errorf("%s wrote %q to stdout; want nothing (error should go to stderr)", sig, stdout.String())
}
if stderr.Len() == 0 {
t.Errorf("%s didn't write error to stderr", sig)
}
}
func TestRunSysInfo(t *gotesting.T) {
td := testutil.TempDir(t)
defer os.RemoveAll(td)
const (
logsDir = "logs"
crashesDir = "crashes"
// Written before GetSysInfoState.
oldLogFile = "1.txt"
oldLogData = "first log"
oldCrashFile = "1.dmp"
oldCrashData = "first crash"
// Written after GetSysInfoState.
newLogFile = "2.txt"
newLogData = "second log"
newCrashFile = "2.dmp"
newCrashData = "second crash"
// Written by SystemInfoFunc.
customLogFile = "custom.txt"
customLogData = "custom data"
)
if err := testutil.WriteFiles(td, map[string]string{
filepath.Join(logsDir, oldLogFile): oldLogData,
filepath.Join(crashesDir, oldCrashFile): oldCrashData,
}); err != nil {
t.Fatal(err)
}
// Get the initial state.
scfg := StaticConfig{
Type: LocalRunner,
SystemLogDir: filepath.Join(td, logsDir),
SystemInfoFunc: func(ctx context.Context, dir string) error {
return ioutil.WriteFile(filepath.Join(dir, customLogFile), []byte(customLogData), 0644)
},
SystemCrashDirs: []string{filepath.Join(td, crashesDir)},
CleanupLogsPausedPath: filepath.Join(td, "cleanup_logs_paused"),
}
status, stdout, _, sig := callRun(t, nil, &jsonprotocol.RunnerArgs{Mode: jsonprotocol.RunnerGetSysInfoStateMode}, nil, &scfg)
if status != statusSuccess {
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
var getRes jsonprotocol.RunnerGetSysInfoStateResult
if err := json.NewDecoder(stdout).Decode(&getRes); err != nil {
t.Fatalf("%v gave bad output: %v", sig, err)
}
if len(getRes.Warnings) > 0 {
t.Errorf("%v produced warning(s): %v", sig, getRes.Warnings)
}
if _, err := os.Stat(scfg.CleanupLogsPausedPath); err != nil {
t.Errorf("Cleanup logs paused file not created: %v", err)
}
if err := testutil.WriteFiles(td, map[string]string{
filepath.Join(logsDir, newLogFile): newLogData,
filepath.Join(crashesDir, newCrashFile): newCrashData,
}); err != nil {
t.Fatal(err)
}
// Now collect system info.
args := jsonprotocol.RunnerArgs{
Mode: jsonprotocol.RunnerCollectSysInfoMode,
CollectSysInfo: &jsonprotocol.RunnerCollectSysInfoArgs{InitialState: getRes.State},
}
if status, stdout, _, sig = callRun(t, nil, &args, nil, &scfg); status != statusSuccess {
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
var collectRes jsonprotocol.RunnerCollectSysInfoResult
if err := json.NewDecoder(stdout).Decode(&collectRes); err != nil {
t.Fatalf("%v gave bad output: %v", sig, err)
}
if len(collectRes.Warnings) > 0 {
t.Errorf("%v produced warning(s): %v", sig, collectRes.Warnings)
}
defer os.RemoveAll(collectRes.LogDir)
defer os.RemoveAll(collectRes.CrashDir)
// The newly-written files should have been copied into the returned temp dirs.
if act, err := testutil.ReadFiles(collectRes.LogDir); err != nil {
t.Error(err)
} else {
if exp := map[string]string{
newLogFile: newLogData,
customLogFile: customLogData,
}; !reflect.DeepEqual(act, exp) {
t.Errorf("%v collected logs %v; want %v", sig, act, exp)
}
}
if act, err := testutil.ReadFiles(collectRes.CrashDir); err != nil {
t.Error(err)
} else {
if exp := map[string]string{newCrashFile: newCrashData}; !reflect.DeepEqual(act, exp) {
t.Errorf("%v collected crashes %v; want %v", sig, act, exp)
}
}
if _, err := os.Stat(scfg.CleanupLogsPausedPath); !os.IsNotExist(err) {
t.Errorf("Cleanup logs paused file not deleted: %v", err)
}
}
func TestRunTests(t *gotesting.T) {
dir := createBundleSymlinks(t, []bool{false, true}, []bool{true})
defer os.RemoveAll(dir)
// Run should execute multiple test bundles and merge their output correctly.
args := &jsonprotocol.RunnerArgs{RunTests: &jsonprotocol.RunnerRunTestsArgs{BundleGlob: filepath.Join(dir, "*")}}
status, stdout, stderr, sig := callRun(t, nil, args, nil, &StaticConfig{Type: LocalRunner})
if status != statusSuccess {
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
msgs := readAllMessages(t, stdout)
if rs, ok := msgs[0].(*control.RunStart); !ok {
t.Errorf("First message not RunStart: %v", msgs[0])
} else {
// Construct names for both tests in bundle 0 and the single test in bundle 1.
expNames := []string{getTestName(0, 0), getTestName(0, 1), getTestName(1, 0)}
if !reflect.DeepEqual(rs.TestNames, expNames) {
t.Errorf("RunStart included test names %v; want %v", rs.TestNames, expNames)
}
if rs.NumTests != 3 {
t.Errorf("RunStart reported %v test(s); want 3", rs.NumTests)
}
}
if re, ok := msgs[len(msgs)-1].(*control.RunEnd); !ok {
t.Errorf("Last message not RunEnd: %v", msgs[len(msgs)-1])
} else {
if fi, err := os.Stat(re.OutDir); os.IsNotExist(err) {
t.Errorf("RunEnd out dir %q doesn't exist", re.OutDir)
} else {
if mode := fi.Mode() & os.ModePerm; mode != 0755 {
t.Errorf("Out dir %v has mode 0%o; want 0%o", re.OutDir, mode, 0755)
}
os.RemoveAll(re.OutDir)
}
}
// Check that the right number of tests are reported as started and failed and that bundles and tests are sorted.
// (Bundle names are included in these test names, so we can verify sorting by just comparing names.)
tests := make(map[string]struct{})
failed := make(map[string]struct{})
var name string
for _, msg := range msgs {
if ts, ok := msg.(*control.EntityStart); ok {
if ts.Info.Name < name {
t.Errorf("Saw unsorted test %q after %q", ts.Info.Name, name)
}
name = ts.Info.Name
tests[name] = struct{}{}
} else if _, ok := msg.(*control.EntityError); ok {
failed[name] = struct{}{}
}
}
if len(tests) != 3 {
t.Errorf("Got EntityStart messages for %v test(s); want 3", len(tests))
}
if len(failed) != 1 {
t.Errorf("Got EntityError messages for %v test(s); want 1", len(failed))
}
if stderr.Len() != 0 {
t.Errorf("%s wrote %q to stderr", sig, stderr.String())
}
}
func TestInvalidFlag(t *gotesting.T) {
status, stdout, stderr, sig := callRun(t, []string{"-bogus"}, nil, nil, &StaticConfig{Type: LocalRunner})
if status != statusBadArgs {
t.Errorf("%s = %v; want %v", sig, status, statusBadArgs)
}
if stdout.Len() != 0 {
t.Errorf("%s wrote %q to stdout; want nothing (error should go to stderr)", sig, stdout.String())
}
if stderr.Len() == 0 {
t.Errorf("%s didn't write error to stderr", sig)
}
}
func TestRunNoTests(t *gotesting.T) {
dir := createBundleSymlinks(t, []bool{true})
defer os.RemoveAll(dir)
// RunTests should fail when run manually with a pattern is passed that doesn't match any tests.
clArgs := []string{"-bundles=" + filepath.Join(dir, "*"), "bogus.SomeTest"}
status, stdout, stderr, sig := callRun(t, clArgs, nil, nil, &StaticConfig{Type: LocalRunner})
if status == 0 {
t.Errorf("%s = %v; want non-zero", sig, status)
}
if stdout.Len() != 0 {
t.Errorf("%s wrote %q to stdout; want nothing (error should go to stderr)", sig, stdout.String())
}
if stderr.Len() == 0 {
t.Errorf("%s didn't write error to stderr", sig)
}
// If the command was run by the tast command, it should exit with success.
args := &jsonprotocol.RunnerArgs{
RunTests: &jsonprotocol.RunnerRunTestsArgs{
BundleArgs: jsonprotocol.BundleRunTestsArgs{Patterns: []string{"bogus.SomeTest"}},
BundleGlob: filepath.Join(dir, "*"),
},
}
scfg := &StaticConfig{Type: LocalRunner}
if status, stdout, stderr, sig = callRun(t, nil, args, nil, scfg); status != statusSuccess {
t.Errorf("%s = %v; want %v", sig, status, statusSuccess)
}
if gotRunError(readAllMessages(t, stdout)) {
t.Errorf("%s wrote RunError message", sig)
}
if stderr.Len() != 0 {
t.Errorf("%s wrote %q to stderr", sig, stderr.String())
}
}
func TestRunFailForTestErrorWhenRunManually(t *gotesting.T) {
dir := createBundleSymlinks(t, []bool{true, false, true})
defer os.RemoveAll(dir)
// RunTests should fail when a bundle reports failure while run manually.
clArgs := []string{"-bundles=" + filepath.Join(dir, "*")}
status, _, stderr, sig := callRun(t, clArgs, nil, nil, &StaticConfig{Type: LocalRunner})
if status != statusTestFailed {
t.Errorf("%s = %v; want %v", sig, status, statusTestFailed)
}
if stderr.Len() == 0 {
t.Errorf("%s didn't write error to stderr", sig)
}
}
func TestCheckDepsWhenRunManually(t *gotesting.T) {
bd := createBundleSymlinks(t, []bool{true})
defer os.RemoveAll(bd)
// Write a file containing a list of USE flags.
const useFlagsFile = "use_flags.txt"
td := testutil.TempDir(t)
defer os.RemoveAll(td)
if err := testutil.WriteFiles(td, map[string]string{
useFlagsFile: "flag1\nflag2\n",
}); err != nil {
t.Fatal(err)
}
args := jsonprotocol.RunnerArgs{RunTests: &jsonprotocol.RunnerRunTestsArgs{BundleGlob: filepath.Join(bd, "*")}}
scfg := StaticConfig{
Type: LocalRunner,
USEFlagsFile: filepath.Join(td, useFlagsFile),
SoftwareFeatureDefinitions: map[string]string{
"both": "flag1 && flag2",
"neither": "!flag1 && !flag2",
"third": "flag3",
},
}
status, _, stderr, sig := callRun(t, []string{"-extrauseflags=flag3,flag4", "(!foo)"}, nil, &args, &scfg)
if status != statusSuccess {
println(stderr.String())
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
// Use args.BundleArgs to determine what would be passed to test bundles.
bundleArgs, err := args.BundleArgs(jsonprotocol.BundleRunTestsMode)
if err != nil {
t.Fatal("BundleArgs failed: ", err)
}
if !bundleArgs.RunTests.CheckDeps {
t.Errorf("%s wouldn't request checking test deps", sig)
}
if exp := []string{"both", "third"}; !reflect.DeepEqual(bundleArgs.RunTests.AvailableSoftwareFeatures, exp) {
t.Errorf("%s would pass available features %v; want %v",
sig, bundleArgs.RunTests.AvailableSoftwareFeatures, exp)
}
if exp := []string{"neither"}; !reflect.DeepEqual(bundleArgs.RunTests.UnavailableSoftwareFeatures, exp) {
t.Errorf("%s would pass unavailable features %v; want %v",
sig, bundleArgs.RunTests.UnavailableSoftwareFeatures, exp)
}
}
func TestSkipBundlesWithoutMatchedTests(t *gotesting.T) {
dir := createBundleSymlinks(t, []bool{true}, []bool{true})
defer os.RemoveAll(dir)
// Bundles report failure if instructed to run using a pattern that doesn't
// match any tests in the bundle. Pass the name of the test in the first bundle and
// check that the run succeeds (i.e. the second bundle wasn't executed).
clArgs := []string{"-bundles=" + filepath.Join(dir, "*"), getTestName(0, 0)}
status, _, _, sig := callRun(t, clArgs, nil, nil, &StaticConfig{Type: LocalRunner})
if status != statusSuccess {
t.Errorf("%s = %v; want %v", sig, status, statusSuccess)
}
}
func TestRunTestsUseRequestedOutDir(t *gotesting.T) {
bundleDir := createBundleSymlinks(t, []bool{true})
defer os.RemoveAll(bundleDir)
outDir := testutil.TempDir(t)
defer os.RemoveAll(outDir)
status, stdout, _, sig := callRun(t, nil, &jsonprotocol.RunnerArgs{
RunTests: &jsonprotocol.RunnerRunTestsArgs{
BundleGlob: filepath.Join(bundleDir, "*"),
BundleArgs: jsonprotocol.BundleRunTestsArgs{OutDir: outDir},
},
}, nil, &StaticConfig{Type: LocalRunner})
if status != statusSuccess {
t.Fatalf("%s = %v; want %v", sig, status, statusSuccess)
}
msgs := readAllMessages(t, stdout)
if re, ok := msgs[len(msgs)-1].(*control.RunEnd); !ok {
t.Errorf("Last message not RunEnd: %v", msgs[len(msgs)-1])
} else {
// The RunEnd message should contain the out dir that we originally requested.
if re.OutDir != outDir {
t.Errorf("RunEnd.OutDir = %q; want %q", re.OutDir, outDir)
}
}
}