Files
kubernetes/vendor/gotest.tools/gotestsum/testjson/execution.go
2019-07-31 17:43:02 -04:00

380 lines
9.4 KiB
Go

package testjson
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"time"
"github.com/jonboulle/clockwork"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)
// Action of TestEvent
type Action string
// nolint: unused
const (
ActionRun Action = "run"
ActionPause Action = "pause"
ActionCont Action = "cont"
ActionPass Action = "pass"
ActionBench Action = "bench"
ActionFail Action = "fail"
ActionOutput Action = "output"
ActionSkip Action = "skip"
)
// TestEvent is a structure output by go tool test2json and go test -json.
type TestEvent struct {
// Time encoded as an RFC3339-format string
Time time.Time
Action Action
Package string
Test string
// Elapsed time in seconds
Elapsed float64
// Output of test or benchmark
Output string
// raw is the raw JSON bytes of the event
raw []byte
}
// PackageEvent returns true if the event is a package start or end event
func (e TestEvent) PackageEvent() bool {
return e.Test == ""
}
// ElapsedFormatted returns Elapsed formatted in the go test format, ex (0.00s).
func (e TestEvent) ElapsedFormatted() string {
return fmt.Sprintf("(%.2fs)", e.Elapsed)
}
// Bytes returns the serialized JSON bytes that were parsed to create the event.
func (e TestEvent) Bytes() []byte {
return e.raw
}
// Package is the set of TestEvents for a single go package
type Package struct {
// TODO: this could be Total()
Total int
Failed []TestCase
Skipped []TestCase
Passed []TestCase
output map[string][]string
// coverage stores the code coverage output for the package without the
// trailing newline (ex: coverage: 91.1% of statements).
coverage string
// action identifies if the package passed or failed. A package may fail
// with no test failures if an init() or TestMain exits non-zero.
// skip indicates there were no tests.
action Action
}
// Result returns if the package passed, failed, or was skipped because there
// were no tests.
func (p Package) Result() Action {
return p.action
}
// Elapsed returns the sum of the elapsed time for all tests in the package.
func (p Package) Elapsed() time.Duration {
elapsed := time.Duration(0)
for _, testcase := range p.TestCases() {
elapsed += testcase.Elapsed
}
return elapsed
}
// TestCases returns all the test cases.
func (p Package) TestCases() []TestCase {
return append(append(p.Passed, p.Failed...), p.Skipped...)
}
// Output returns the full test output for a test.
func (p Package) Output(test string) string {
return strings.Join(p.output[test], "")
}
// TestMainFailed returns true if the package failed, but there were no tests.
// This may occur if the package init() or TestMain exited non-zero.
func (p Package) TestMainFailed() bool {
return p.action == ActionFail && len(p.Failed) == 0
}
// TestCase stores the name and elapsed time for a test case.
type TestCase struct {
Package string
Test string
Elapsed time.Duration
}
func newPackage() *Package {
return &Package{output: make(map[string][]string)}
}
// Execution of one or more test packages
type Execution struct {
started time.Time
packages map[string]*Package
errors []string
}
func (e *Execution) add(event TestEvent) {
pkg, ok := e.packages[event.Package]
if !ok {
pkg = newPackage()
e.packages[event.Package] = pkg
}
if event.PackageEvent() {
e.addPackageEvent(pkg, event)
return
}
e.addTestEvent(pkg, event)
}
func (e *Execution) addPackageEvent(pkg *Package, event TestEvent) {
switch event.Action {
case ActionPass, ActionFail:
pkg.action = event.Action
case ActionOutput:
if isCoverageOutput(event.Output) {
pkg.coverage = strings.TrimRight(event.Output, "\n")
}
pkg.output[""] = append(pkg.output[""], event.Output)
}
}
func (e *Execution) addTestEvent(pkg *Package, event TestEvent) {
switch event.Action {
case ActionRun:
pkg.Total++
case ActionFail:
pkg.Failed = append(pkg.Failed, TestCase{
Package: event.Package,
Test: event.Test,
Elapsed: elapsedDuration(event.Elapsed),
})
case ActionSkip:
pkg.Skipped = append(pkg.Skipped, TestCase{
Package: event.Package,
Test: event.Test,
Elapsed: elapsedDuration(event.Elapsed),
})
case ActionOutput, ActionBench:
// TODO: limit size of buffered test output
pkg.output[event.Test] = append(pkg.output[event.Test], event.Output)
case ActionPass:
pkg.Passed = append(pkg.Passed, TestCase{
Package: event.Package,
Test: event.Test,
Elapsed: elapsedDuration(event.Elapsed),
})
// Remove test output once a test passes, it wont be used
delete(pkg.output, event.Test)
}
}
func elapsedDuration(elapsed float64) time.Duration {
return time.Duration(elapsed*1000) * time.Millisecond
}
func isCoverageOutput(output string) bool {
return all(
strings.HasPrefix(output, "coverage:"),
strings.HasSuffix(output, "% of statements\n"))
}
// Output returns the full test output for a test.
func (e *Execution) Output(pkg, test string) string {
return strings.Join(e.packages[pkg].output[test], "")
}
// OutputLines returns the full test output for a test as an array of lines.
func (e *Execution) OutputLines(pkg, test string) []string {
return e.packages[pkg].output[test]
}
// Package returns the Package by name.
func (e *Execution) Package(name string) *Package {
return e.packages[name]
}
// Packages returns a sorted list of all package names.
func (e *Execution) Packages() []string {
return sortedKeys(e.packages)
}
var clock = clockwork.NewRealClock()
// Elapsed returns the time elapsed since the execution started.
func (e *Execution) Elapsed() time.Duration {
return clock.Now().Sub(e.started)
}
// Failed returns a list of all the failed test cases.
func (e *Execution) Failed() []TestCase {
var failed []TestCase
for _, name := range sortedKeys(e.packages) {
pkg := e.packages[name]
// Add package-level failure output if there were no failed tests.
if pkg.TestMainFailed() {
failed = append(failed, TestCase{Package: name})
} else {
failed = append(failed, pkg.Failed...)
}
}
return failed
}
func sortedKeys(pkgs map[string]*Package) []string {
keys := make([]string, 0, len(pkgs))
for key := range pkgs {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
// Skipped returns a list of all the skipped test cases.
func (e *Execution) Skipped() []TestCase {
skipped := make([]TestCase, 0, len(e.packages))
for _, pkg := range sortedKeys(e.packages) {
skipped = append(skipped, e.packages[pkg].Skipped...)
}
return skipped
}
// Total returns a count of all test cases.
func (e *Execution) Total() int {
total := 0
for _, pkg := range e.packages {
total += pkg.Total
}
return total
}
func (e *Execution) addError(err string) {
// Build errors start with a header
if strings.HasPrefix(err, "# ") {
return
}
// TODO: may need locking, or use a channel
e.errors = append(e.errors, err)
}
// Errors returns a list of all the errors.
func (e *Execution) Errors() []string {
return e.errors
}
// NewExecution returns a new Execution and records the current time as the
// time the test execution started.
func NewExecution() *Execution {
return &Execution{
started: time.Now(),
packages: make(map[string]*Package),
}
}
// ScanConfig used by ScanTestOutput
type ScanConfig struct {
Stdout io.Reader
Stderr io.Reader
Handler EventHandler
}
// EventHandler is called by ScanTestOutput for each event and write to stderr.
type EventHandler interface {
Event(event TestEvent, execution *Execution) error
Err(text string) error
}
// ScanTestOutput reads lines from stdout and stderr, creates an Execution,
// calls the Handler for each event, and returns the Execution.
func ScanTestOutput(config ScanConfig) (*Execution, error) {
execution := NewExecution()
var group errgroup.Group
group.Go(func() error {
return readStdout(config, execution)
})
group.Go(func() error {
return readStderr(config, execution)
})
return execution, group.Wait()
}
func readStdout(config ScanConfig, execution *Execution) error {
scanner := bufio.NewScanner(config.Stdout)
for scanner.Scan() {
raw := scanner.Bytes()
event, err := parseEvent(raw)
switch {
case err == errBadEvent:
// nolint: errcheck
config.Handler.Err(errBadEvent.Error() + ": " + scanner.Text())
continue
case err != nil:
return errors.Wrapf(err, "failed to parse test output: %s", string(raw))
}
execution.add(event)
if err := config.Handler.Event(event, execution); err != nil {
return err
}
}
return errors.Wrap(scanner.Err(), "failed to scan test output")
}
func readStderr(config ScanConfig, execution *Execution) error {
scanner := bufio.NewScanner(config.Stderr)
for scanner.Scan() {
line := scanner.Text()
config.Handler.Err(line) // nolint: errcheck
if isGoModuleOutput(line) {
continue
}
execution.addError(line)
}
return errors.Wrap(scanner.Err(), "failed to scan test stderr")
}
func isGoModuleOutput(scannerText string) bool {
prefixes := []string{
"go: copying",
"go: creating",
"go: downloading",
"go: extracting",
"go: finding",
}
for _, prefix := range prefixes {
if strings.HasPrefix(scannerText, prefix) {
return true
}
}
return false
}
func parseEvent(raw []byte) (TestEvent, error) {
// TODO: this seems to be a bug in the `go test -json` output
if bytes.HasPrefix(raw, []byte("FAIL")) {
logrus.Warn(string(raw))
return TestEvent{}, errBadEvent
}
event := TestEvent{}
err := json.Unmarshal(raw, &event)
event.raw = raw
return event, err
}
var errBadEvent = errors.New("bad output from test2json")