blob: 0bdd1859f60b950c43d383f05d55d0b61d610f4e [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 run
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
gotesting "testing"
"time"
"chromiumos/tast/cmd/logging"
"chromiumos/tast/common/control"
"chromiumos/tast/common/testing"
"chromiumos/tast/common/testutil"
)
// noOpCopyAndRemove can be passed to readTestOutput by tests.
func noOpCopyAndRemove(src, dst string) error { return nil }
func TestReadTestOutput(t *gotesting.T) {
const (
test1Name = "foo.FirstTest"
test1Desc = "First description"
test1LogText = "Here's a log message"
test2Name = "foo.SecondTest"
test2Desc = "Second description"
test2ErrorReason = "Everything is broken :-("
test2ErrorFile = "some_test.go"
test2ErrorLine = 123
test2ErrorStack = "[stack trace]"
test2OutFile = "data.txt"
test2OutData = "Here's some data created by the test."
)
runStartTime := time.Unix(1, 0)
test1StartTime := time.Unix(2, 0)
test1LogTime := time.Unix(3, 0)
test1EndTime := time.Unix(4, 0)
test2StartTime := time.Unix(5, 0)
test2ErrorTime := time.Unix(7, 0)
test2EndTime := time.Unix(9, 0)
runEndTime := time.Unix(10, 0)
tempDir := testutil.TempDir(t, "results_test.")
defer os.RemoveAll(tempDir)
outDir := filepath.Join(tempDir, "out")
if err := testutil.WriteFiles(outDir, map[string]string{
filepath.Join(test2Name, test2OutFile): test2OutData,
}); err != nil {
t.Fatal(err)
}
b := bytes.Buffer{}
mw := control.NewMessageWriter(&b)
mw.WriteMessage(&control.RunStart{runStartTime, 2})
mw.WriteMessage(&control.TestStart{test1StartTime, test1Name,
testing.Test{Name: test1Name, Desc: test1Desc}})
mw.WriteMessage(&control.TestLog{test1LogTime, test1LogText})
mw.WriteMessage(&control.TestEnd{test1EndTime, test1Name})
mw.WriteMessage(&control.TestStart{test2StartTime, test2Name,
testing.Test{Name: test2Name, Desc: test2Desc}})
mw.WriteMessage(&control.TestError{test2ErrorTime,
testing.Error{test2ErrorReason, test2ErrorFile, test2ErrorLine, test2ErrorStack}})
mw.WriteMessage(&control.TestEnd{test2EndTime, test2Name})
mw.WriteMessage(&control.RunEnd{runEndTime, "", outDir})
cfg := Config{
Logger: logging.NewSimple(&bytes.Buffer{}, 0, false),
ResDir: filepath.Join(tempDir, "results"),
}
crf := func(src, dst string) error { return os.Rename(src, dst) }
if err := readTestOutput(context.Background(), &cfg, &b, crf); err != nil {
t.Fatal(err)
}
files, err := testutil.ReadFiles(cfg.ResDir)
if err != nil {
t.Fatal(err)
}
expRes, err := json.MarshalIndent([]testResult{
{
Test: testing.Test{Name: test1Name, Desc: test1Desc},
Start: test1StartTime,
End: test1EndTime,
OutDir: filepath.Join(cfg.ResDir, testLogsDir, test1Name),
},
{
Test: testing.Test{Name: test2Name, Desc: test2Desc},
Errors: []testing.Error{
testing.Error{
Reason: test2ErrorReason,
File: test2ErrorFile,
Line: test2ErrorLine,
Stack: test2ErrorStack,
},
},
Start: test2StartTime,
End: test2EndTime,
OutDir: filepath.Join(cfg.ResDir, testLogsDir, test2Name),
},
}, "", " ")
if err != nil {
t.Fatal(err)
}
expRes = append(expRes, '\n')
if files[resultsFilename] != string(expRes) {
t.Errorf("%s contains %q; want %q", resultsFilename, files[resultsFilename], string(expRes))
}
outPath := filepath.Join(testLogsDir, test2Name, test2OutFile)
if files[outPath] != test2OutData {
t.Errorf("%s contains %q; want %q", outPath, files[outPath], test2OutData)
}
// TODO(derat): Check more output, including run errors.
}
func TestReadTestOutputTimeout(t *gotesting.T) {
tempDir := testutil.TempDir(t, "results_test.")
defer os.RemoveAll(tempDir)
// Create a pipe, but don't write to it or close it during the test.
// readTestOutput should time out and report an error.
pr, pw := io.Pipe()
defer pw.Close()
cfg := Config{
Logger: logging.NewSimple(&bytes.Buffer{}, 0, false),
ResDir: tempDir,
msgTimeout: time.Millisecond,
}
if err := readTestOutput(context.Background(), &cfg, pr, noOpCopyAndRemove); err == nil {
t.Error("readTestOutput didn't return error for timeout")
}
}
func TestNextMessageTimeout(t *gotesting.T) {
now := time.Unix(60, 0)
for _, tc := range []struct {
now time.Time
msgTimeout time.Duration
ctxTimeout time.Duration
testStart time.Time
testTimeout time.Duration
exp time.Duration
}{
{
// Outside a test, and without a custom or context timeout, use the default.
exp: defaultMsgTimeout,
},
{
// If a message timeout is supplied, use it instead of default.
msgTimeout: 5 * time.Second,
exp: 5 * time.Second,
},
{
// Mid-test, use the test's remaining time plus the normal message timeout.
msgTimeout: 10 * time.Second,
testStart: now.Add(-1 * time.Second),
testTimeout: 5 * time.Second,
exp: 14 * time.Second,
},
{
// A context timeout should cap whatever timeout would be used otherwise.
msgTimeout: 20 * time.Second,
ctxTimeout: 11 * time.Second,
exp: 11 * time.Second,
},
} {
ctx := context.Background()
var cancel context.CancelFunc
if tc.ctxTimeout != 0 {
ctx, cancel = context.WithDeadline(ctx, now.Add(tc.ctxTimeout))
}
h := resultsHandler{
ctx: ctx,
cfg: &Config{msgTimeout: tc.msgTimeout},
}
if !tc.testStart.IsZero() {
h.res = &testResult{
Test: testing.Test{Timeout: tc.testTimeout},
testStartMsgTime: tc.testStart,
}
}
// Avoid printing ugly negative numbers for unset testStart fields.
var testStartUnix int64
if !tc.testStart.IsZero() {
testStartUnix = tc.testStart.Unix()
}
if act := h.nextMessageTimeout(now); act != tc.exp {
t.Errorf("nextMessageTimeout(%v) (msgTimeout=%v, ctxTimeout=%v testStart=%v, testTimeout=%v) = %v; want %v",
now.Unix(), tc.msgTimeout, tc.ctxTimeout, testStartUnix, tc.testTimeout, act, tc.exp)
}
if cancel != nil {
cancel()
}
}
}