open-nomad/vendor/github.com/jstemmer/go-junit-report/parser/parser.go
Seth Hoenig 435c0d9fc8 deps: Switch to Go modules for dependency management
This PR switches the Nomad repository from using govendor to Go modules
for managing dependencies. Aspects of the Nomad workflow remain pretty
much the same. The usual Makefile targets should continue to work as
they always did. The API submodule simply defers to the parent Nomad
version on the repository, keeping the semantics of API versioning that
currently exists.
2020-06-02 14:30:36 -05:00

304 lines
8.1 KiB
Go

package parser
import (
"bufio"
"io"
"regexp"
"strconv"
"strings"
"time"
)
// Result represents a test result.
type Result int
// Test result constants
const (
PASS Result = iota
FAIL
SKIP
)
// Report is a collection of package tests.
type Report struct {
Packages []Package
}
// Package contains the test results of a single package.
type Package struct {
Name string
Duration time.Duration
Tests []*Test
Benchmarks []*Benchmark
CoveragePct string
// Time is deprecated, use Duration instead.
Time int // in milliseconds
}
// Test contains the results of a single test.
type Test struct {
Name string
Duration time.Duration
Result Result
Output []string
SubtestIndent string
// Time is deprecated, use Duration instead.
Time int // in milliseconds
}
// Benchmark contains the results of a single benchmark.
type Benchmark struct {
Name string
Duration time.Duration
// number of B/op
Bytes int
// number of allocs/op
Allocs int
}
var (
regexStatus = regexp.MustCompile(`--- (PASS|FAIL|SKIP): (.+) \((\d+\.\d+)(?: seconds|s)\)`)
regexIndent = regexp.MustCompile(`^([ \t]+)---`)
regexCoverage = regexp.MustCompile(`^coverage:\s+(\d+\.\d+)%\s+of\s+statements(?:\sin\s.+)?$`)
regexResult = regexp.MustCompile(`^(ok|FAIL)\s+([^ ]+)\s+(?:(\d+\.\d+)s|\(cached\)|(\[\w+ failed]))(?:\s+coverage:\s+(\d+\.\d+)%\sof\sstatements(?:\sin\s.+)?)?$`)
// regexBenchmark captures 3-5 groups: benchmark name, number of times ran, ns/op (with or without decimal), B/op (optional), and allocs/op (optional).
regexBenchmark = regexp.MustCompile(`^(Benchmark[^ -]+)(?:-\d+\s+|\s+)(\d+)\s+(\d+|\d+\.\d+)\sns/op(?:\s+(\d+)\sB/op)?(?:\s+(\d+)\sallocs/op)?`)
regexOutput = regexp.MustCompile(`( )*\t(.*)`)
regexSummary = regexp.MustCompile(`^(PASS|FAIL|SKIP)$`)
)
// Parse parses go test output from reader r and returns a report with the
// results. An optional pkgName can be given, which is used in case a package
// result line is missing.
func Parse(r io.Reader, pkgName string) (*Report, error) {
reader := bufio.NewReader(r)
report := &Report{make([]Package, 0)}
// keep track of tests we find
var tests []*Test
// keep track of benchmarks we find
var benchmarks []*Benchmark
// sum of tests' time, use this if current test has no result line (when it is compiled test)
var testsTime time.Duration
// current test
var cur string
// keep track if we've already seen a summary for the current test
var seenSummary bool
// coverage percentage report for current package
var coveragePct string
// stores mapping between package name and output of build failures
var packageCaptures = map[string][]string{}
// the name of the package which it's build failure output is being captured
var capturedPackage string
// capture any non-test output
var buffers = map[string][]string{}
// parse lines
for {
l, _, err := reader.ReadLine()
if err != nil && err == io.EOF {
break
} else if err != nil {
return nil, err
}
line := string(l)
if strings.HasPrefix(line, "=== RUN ") {
// new test
cur = strings.TrimSpace(line[8:])
tests = append(tests, &Test{
Name: cur,
Result: FAIL,
Output: make([]string, 0),
})
// clear the current build package, so output lines won't be added to that build
capturedPackage = ""
seenSummary = false
} else if matches := regexBenchmark.FindStringSubmatch(line); len(matches) == 6 {
bytes, _ := strconv.Atoi(matches[4])
allocs, _ := strconv.Atoi(matches[5])
benchmarks = append(benchmarks, &Benchmark{
Name: matches[1],
Duration: parseNanoseconds(matches[3]),
Bytes: bytes,
Allocs: allocs,
})
} else if strings.HasPrefix(line, "=== PAUSE ") {
continue
} else if strings.HasPrefix(line, "=== CONT ") {
cur = strings.TrimSpace(line[8:])
continue
} else if matches := regexResult.FindStringSubmatch(line); len(matches) == 6 {
if matches[5] != "" {
coveragePct = matches[5]
}
if strings.HasSuffix(matches[4], "failed]") {
// the build of the package failed, inject a dummy test into the package
// which indicate about the failure and contain the failure description.
tests = append(tests, &Test{
Name: matches[4],
Result: FAIL,
Output: packageCaptures[matches[2]],
})
} else if matches[1] == "FAIL" && len(tests) == 0 && len(buffers[cur]) > 0 {
// This package didn't have any tests, but it failed with some
// output. Create a dummy test with the output.
tests = append(tests, &Test{
Name: "Failure",
Result: FAIL,
Output: buffers[cur],
})
buffers[cur] = buffers[cur][0:0]
}
// all tests in this package are finished
report.Packages = append(report.Packages, Package{
Name: matches[2],
Duration: parseSeconds(matches[3]),
Tests: tests,
Benchmarks: benchmarks,
CoveragePct: coveragePct,
Time: int(parseSeconds(matches[3]) / time.Millisecond), // deprecated
})
buffers[cur] = buffers[cur][0:0]
tests = make([]*Test, 0)
benchmarks = make([]*Benchmark, 0)
coveragePct = ""
cur = ""
testsTime = 0
} else if matches := regexStatus.FindStringSubmatch(line); len(matches) == 4 {
cur = matches[2]
test := findTest(tests, cur)
if test == nil {
continue
}
// test status
if matches[1] == "PASS" {
test.Result = PASS
} else if matches[1] == "SKIP" {
test.Result = SKIP
} else {
test.Result = FAIL
}
if matches := regexIndent.FindStringSubmatch(line); len(matches) == 2 {
test.SubtestIndent = matches[1]
}
test.Output = buffers[cur]
test.Name = matches[2]
test.Duration = parseSeconds(matches[3])
testsTime += test.Duration
test.Time = int(test.Duration / time.Millisecond) // deprecated
} else if matches := regexCoverage.FindStringSubmatch(line); len(matches) == 2 {
coveragePct = matches[1]
} else if matches := regexOutput.FindStringSubmatch(line); capturedPackage == "" && len(matches) == 3 {
// Sub-tests start with one or more series of 4-space indents, followed by a hard tab,
// followed by the test output
// Top-level tests start with a hard tab.
test := findTest(tests, cur)
if test == nil {
continue
}
test.Output = append(test.Output, matches[2])
} else if strings.HasPrefix(line, "# ") {
// indicates a capture of build output of a package. set the current build package.
capturedPackage = line[2:]
} else if capturedPackage != "" {
// current line is build failure capture for the current built package
packageCaptures[capturedPackage] = append(packageCaptures[capturedPackage], line)
} else if regexSummary.MatchString(line) {
// don't store any output after the summary
seenSummary = true
} else if !seenSummary {
// buffer anything else that we didn't recognize
buffers[cur] = append(buffers[cur], line)
// if we have a current test, also append to its output
test := findTest(tests, cur)
if test != nil {
if strings.HasPrefix(line, test.SubtestIndent+" ") {
test.Output = append(test.Output, strings.TrimPrefix(line, test.SubtestIndent+" "))
}
}
}
}
if len(tests) > 0 {
// no result line found
report.Packages = append(report.Packages, Package{
Name: pkgName,
Duration: testsTime,
Time: int(testsTime / time.Millisecond),
Tests: tests,
Benchmarks: benchmarks,
CoveragePct: coveragePct,
})
}
return report, nil
}
func parseSeconds(t string) time.Duration {
if t == "" {
return time.Duration(0)
}
// ignore error
d, _ := time.ParseDuration(t + "s")
return d
}
func parseNanoseconds(t string) time.Duration {
// note: if input < 1 ns precision, result will be 0s.
if t == "" {
return time.Duration(0)
}
// ignore error
d, _ := time.ParseDuration(t + "ns")
return d
}
func findTest(tests []*Test, name string) *Test {
for i := len(tests) - 1; i >= 0; i-- {
if tests[i].Name == name {
return tests[i]
}
}
return nil
}
// Failures counts the number of failed tests in this report
func (r *Report) Failures() int {
count := 0
for _, p := range r.Packages {
for _, t := range p.Tests {
if t.Result == FAIL {
count++
}
}
}
return count
}