435c0d9fc8
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.
304 lines
8.1 KiB
Go
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
|
|
}
|