mirror of
https://github.com/jstemmer/go-junit-report.git
synced 2025-04-10 15:48:08 -05:00
parser/gotest: Add support for processing events for different packages
The reportBuilder assumed we were always processing events for a single package at a time. This is not true however when running `go test -json -race` for example. In order to properly support processing events from different packages we now have packageBuilders per package name. Fixes #134
This commit is contained in:
parent
85f2715ac9
commit
9357c180d4
parser/gotest
@ -2,6 +2,7 @@ package gotest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -15,21 +16,20 @@ const (
|
|||||||
|
|
||||||
// reportBuilder helps build a test Report from a collection of events.
|
// reportBuilder helps build a test Report from a collection of events.
|
||||||
//
|
//
|
||||||
// The reportBuilder keeps track of the active context whenever a test or build
|
// The reportBuilder delegates to the packageBuilder for creating packages from
|
||||||
// error is created. This is necessary because the test parser do not contain
|
// basic test events, but keeps track of build errors itself. The reportBuilder
|
||||||
// any state themselves and simply just emit an event for every line that is
|
// is also responsible for generating unique test id's.
|
||||||
// read. By tracking the active context, any output that is appended to the
|
//
|
||||||
// reportBuilder gets attributed to the correct test or build error.
|
// Test output is collected by the output collector, which also keeps track of
|
||||||
|
// the currently active test so output is automatically associated with the
|
||||||
|
// correct test.
|
||||||
type reportBuilder struct {
|
type reportBuilder struct {
|
||||||
packages []gtr.Package
|
packageBuilders map[string]*packageBuilder
|
||||||
tests map[int]gtr.Test
|
|
||||||
buildErrors map[int]gtr.Error
|
buildErrors map[int]gtr.Error
|
||||||
|
|
||||||
// state
|
|
||||||
nextID int // next free unused id
|
nextID int // next free unused id
|
||||||
output *collector.Output // output collected for each id
|
output *collector.Output // output collected for each id
|
||||||
coverage float64 // coverage percentage
|
packages []gtr.Package // completed packages
|
||||||
parentIDs map[int]struct{} // set of test id's that contain subtests
|
|
||||||
|
|
||||||
// options
|
// options
|
||||||
packageName string
|
packageName string
|
||||||
@ -40,296 +40,188 @@ type reportBuilder struct {
|
|||||||
// newReportBuilder creates a new reportBuilder.
|
// newReportBuilder creates a new reportBuilder.
|
||||||
func newReportBuilder() *reportBuilder {
|
func newReportBuilder() *reportBuilder {
|
||||||
return &reportBuilder{
|
return &reportBuilder{
|
||||||
tests: make(map[int]gtr.Test),
|
packageBuilders: make(map[string]*packageBuilder),
|
||||||
buildErrors: make(map[int]gtr.Error),
|
buildErrors: make(map[int]gtr.Error),
|
||||||
nextID: 1,
|
nextID: 1,
|
||||||
output: collector.New(),
|
output: collector.New(),
|
||||||
parentIDs: make(map[int]struct{}),
|
|
||||||
timestampFunc: time.Now,
|
timestampFunc: time.Now,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessEvent gives an event to this reportBuilder to be processed for this
|
// getPackageBuilder returns the packageBuilder for the given packageName. If
|
||||||
// report.
|
// no packageBuilder exists for the given package, a new one is created.
|
||||||
|
func (b *reportBuilder) getPackageBuilder(packageName string) *packageBuilder {
|
||||||
|
pb, ok := b.packageBuilders[packageName]
|
||||||
|
if !ok {
|
||||||
|
output := b.output
|
||||||
|
if packageName != "" {
|
||||||
|
output = collector.New()
|
||||||
|
}
|
||||||
|
pb = newPackageBuilder(b.generateID, output)
|
||||||
|
b.packageBuilders[packageName] = pb
|
||||||
|
}
|
||||||
|
return pb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessEvent takes a test event and adds it to the report.
|
||||||
func (b *reportBuilder) ProcessEvent(ev Event) {
|
func (b *reportBuilder) ProcessEvent(ev Event) {
|
||||||
switch ev.Type {
|
switch ev.Type {
|
||||||
case "run_test":
|
case "run_test":
|
||||||
b.CreateTest(ev.Name)
|
b.getPackageBuilder(ev.Package).CreateTest(ev.Name)
|
||||||
case "pause_test":
|
case "pause_test":
|
||||||
b.PauseTest(ev.Name)
|
b.getPackageBuilder(ev.Package).PauseTest(ev.Name)
|
||||||
case "cont_test":
|
case "cont_test":
|
||||||
b.ContinueTest(ev.Name)
|
b.getPackageBuilder(ev.Package).ContinueTest(ev.Name)
|
||||||
case "end_test":
|
case "end_test":
|
||||||
b.EndTest(ev.Name, ev.Result, ev.Duration, ev.Indent)
|
b.getPackageBuilder(ev.Package).EndTest(ev.Name, ev.Result, ev.Duration, ev.Indent)
|
||||||
case "run_benchmark":
|
case "run_benchmark":
|
||||||
b.CreateTest(ev.Name)
|
b.getPackageBuilder(ev.Package).CreateTest(ev.Name)
|
||||||
case "benchmark":
|
case "benchmark":
|
||||||
b.BenchmarkResult(ev.Name, ev.Iterations, ev.NsPerOp, ev.MBPerSec, ev.BytesPerOp, ev.AllocsPerOp)
|
b.getPackageBuilder(ev.Package).BenchmarkResult(ev.Name, ev.Iterations, ev.NsPerOp, ev.MBPerSec, ev.BytesPerOp, ev.AllocsPerOp)
|
||||||
case "end_benchmark":
|
case "end_benchmark":
|
||||||
b.EndTest(ev.Name, ev.Result, 0, 0)
|
b.getPackageBuilder(ev.Package).EndTest(ev.Name, ev.Result, 0, 0)
|
||||||
case "status":
|
case "status":
|
||||||
b.End()
|
b.getPackageBuilder(ev.Package).End()
|
||||||
case "summary":
|
case "summary":
|
||||||
b.CreatePackage(ev.Name, ev.Result, ev.Duration, ev.Data)
|
// The summary marks the end of a package. We can now create the actual
|
||||||
|
// package from all the events we've processed so far for this package.
|
||||||
|
b.packages = append(b.packages, b.CreatePackage(ev.Package, ev.Name, ev.Result, ev.Duration, ev.Data))
|
||||||
case "coverage":
|
case "coverage":
|
||||||
b.Coverage(ev.CovPct, ev.CovPackages)
|
b.getPackageBuilder(ev.Package).Coverage(ev.CovPct, ev.CovPackages)
|
||||||
case "build_output":
|
case "build_output":
|
||||||
b.CreateBuildError(ev.Name)
|
b.CreateBuildError(ev.Name)
|
||||||
case "output":
|
case "output":
|
||||||
b.AppendOutput(ev.Data)
|
if ev.Package != "" {
|
||||||
|
b.getPackageBuilder(ev.Package).Output(ev.Data)
|
||||||
|
} else {
|
||||||
|
b.output.Append(ev.Data)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
// This shouldn't happen, but just in case print a warning and ignore
|
||||||
|
// this event.
|
||||||
fmt.Printf("reportBuilder: unhandled event type: %v\n", ev.Type)
|
fmt.Printf("reportBuilder: unhandled event type: %v\n", ev.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newID returns a new unique id and sets the active context this id.
|
// newID returns a new unique id.
|
||||||
func (b *reportBuilder) newID() int {
|
func (b *reportBuilder) generateID() int {
|
||||||
id := b.nextID
|
id := b.nextID
|
||||||
b.nextID++
|
b.nextID++
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush creates a new package in this report containing any tests we've
|
// Build returns the new Report containing all the tests, build errors and
|
||||||
// collected so far. This is necessary when a test did not end with a summary.
|
// their output created from the processed events.
|
||||||
func (b *reportBuilder) flush() {
|
|
||||||
if len(b.tests) > 0 {
|
|
||||||
b.CreatePackage(b.packageName, "", 0, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build returns the new Report containing all the tests and output created so
|
|
||||||
// far.
|
|
||||||
func (b *reportBuilder) Build() gtr.Report {
|
func (b *reportBuilder) Build() gtr.Report {
|
||||||
b.flush()
|
// Create packages for any leftover package builders.
|
||||||
|
for name, pb := range b.packageBuilders {
|
||||||
|
if pb.IsEmpty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.packages = append(b.packages, b.CreatePackage(name, b.packageName, "", 0, ""))
|
||||||
|
}
|
||||||
return gtr.Report{Packages: b.packages}
|
return gtr.Report{Packages: b.packages}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTest adds a test with the given name to the report, and marks it as
|
|
||||||
// active.
|
|
||||||
func (b *reportBuilder) CreateTest(name string) int {
|
|
||||||
if parentID, ok := b.findTestParentID(name); ok {
|
|
||||||
b.parentIDs[parentID] = struct{}{}
|
|
||||||
}
|
|
||||||
id := b.newID()
|
|
||||||
b.output.SetActiveID(id)
|
|
||||||
b.tests[id] = gtr.NewTest(id, name)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// PauseTest marks the active context as no longer active. Any results or
|
|
||||||
// output added to the report after calling PauseTest will no longer be assumed
|
|
||||||
// to belong to this test.
|
|
||||||
func (b *reportBuilder) PauseTest(name string) {
|
|
||||||
b.output.SetActiveID(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContinueTest finds the test with the given name and marks it as active. If
|
|
||||||
// more than one test exist with this name, the most recently created test will
|
|
||||||
// be used.
|
|
||||||
func (b *reportBuilder) ContinueTest(name string) {
|
|
||||||
id, _ := b.findTest(name)
|
|
||||||
b.output.SetActiveID(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndTest finds the test with the given name, sets the result, duration and
|
|
||||||
// level. If more than one test exists with this name, the most recently
|
|
||||||
// created test will be used. If no test exists with this name, a new test is
|
|
||||||
// created.
|
|
||||||
func (b *reportBuilder) EndTest(name, result string, duration time.Duration, level int) {
|
|
||||||
id, ok := b.findTest(name)
|
|
||||||
if !ok {
|
|
||||||
// test did not exist, create one
|
|
||||||
// TODO: Likely reason is that the user ran go test without the -v
|
|
||||||
// flag, should we report this somewhere?
|
|
||||||
id = b.CreateTest(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := b.tests[id]
|
|
||||||
t.Result = parseResult(result)
|
|
||||||
t.Duration = duration
|
|
||||||
t.Level = level
|
|
||||||
b.tests[id] = t
|
|
||||||
b.output.SetActiveID(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// End marks the active context as no longer active.
|
|
||||||
func (b *reportBuilder) End() {
|
|
||||||
b.output.SetActiveID(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkResult updates an existing or adds a new test with the given
|
|
||||||
// results and marks it as active. If an existing test with this name exists
|
|
||||||
// but without result, then that one is updated. Otherwise a new one is added
|
|
||||||
// to the report.
|
|
||||||
func (b *reportBuilder) BenchmarkResult(name string, iterations int64, nsPerOp, mbPerSec float64, bytesPerOp, allocsPerOp int64) {
|
|
||||||
id, ok := b.findTest(name)
|
|
||||||
if !ok || b.tests[id].Result != gtr.Unknown {
|
|
||||||
id = b.CreateTest(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmark := Benchmark{iterations, nsPerOp, mbPerSec, bytesPerOp, allocsPerOp}
|
|
||||||
test := gtr.NewTest(id, name)
|
|
||||||
test.Result = gtr.Pass
|
|
||||||
test.Duration = benchmark.ApproximateDuration()
|
|
||||||
SetBenchmarkData(&test, benchmark)
|
|
||||||
b.tests[id] = test
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateBuildError creates a new build error and marks it as active.
|
// CreateBuildError creates a new build error and marks it as active.
|
||||||
func (b *reportBuilder) CreateBuildError(packageName string) {
|
func (b *reportBuilder) CreateBuildError(packageName string) {
|
||||||
id := b.newID()
|
id := b.generateID()
|
||||||
b.output.SetActiveID(id)
|
b.output.SetActiveID(id)
|
||||||
b.buildErrors[id] = gtr.Error{ID: id, Name: packageName}
|
b.buildErrors[id] = gtr.Error{ID: id, Name: packageName}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePackage adds a new package with the given name to the Report. This
|
// CreatePackage returns a new package containing all the build errors, output,
|
||||||
// package contains all the build errors, output, tests and benchmarks created
|
// tests and benchmarks created so far. The optional packageName is used to
|
||||||
// so far. Afterwards all state is reset.
|
// find the correct reportBuilder. The newPackageName is the actual package
|
||||||
func (b *reportBuilder) CreatePackage(name, result string, duration time.Duration, data string) {
|
// name that will be given to the returned package, which should be used in
|
||||||
|
// case the packageName was unknown until this point.
|
||||||
|
func (b *reportBuilder) CreatePackage(packageName, newPackageName, result string, duration time.Duration, data string) gtr.Package {
|
||||||
pkg := gtr.Package{
|
pkg := gtr.Package{
|
||||||
Name: name,
|
Name: newPackageName,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
Timestamp: b.timestampFunc(),
|
Timestamp: b.timestampFunc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build errors are treated somewhat differently. Rather than having a
|
// First check if this package contained a build error. If that's the case,
|
||||||
// single package with all build errors collected so far, we only care
|
// we won't find any tests in this package.
|
||||||
// about the build errors for this particular package.
|
|
||||||
for id, buildErr := range b.buildErrors {
|
for id, buildErr := range b.buildErrors {
|
||||||
if buildErr.Name == name {
|
if buildErr.Name == newPackageName {
|
||||||
if len(b.tests) > 0 {
|
|
||||||
panic("unexpected tests found in build error package")
|
|
||||||
}
|
|
||||||
buildErr.ID = id
|
|
||||||
buildErr.Duration = duration
|
|
||||||
buildErr.Cause = data
|
|
||||||
buildErr.Output = b.output.Get(id)
|
|
||||||
|
|
||||||
pkg.BuildError = buildErr
|
pkg.BuildError = buildErr
|
||||||
b.packages = append(b.packages, pkg)
|
pkg.BuildError.ID = id
|
||||||
|
pkg.BuildError.Duration = duration
|
||||||
|
pkg.BuildError.Cause = data
|
||||||
|
pkg.BuildError.Output = b.output.Get(id)
|
||||||
|
|
||||||
delete(b.buildErrors, id)
|
delete(b.buildErrors, id)
|
||||||
// TODO: reset state
|
b.output.SetActiveID(0)
|
||||||
// TODO: buildErrors shouldn't reset/use nextID/lastID, they're more like a global cache
|
return pkg
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've collected output, but there were no tests then either there
|
// Get the packageBuilder for this package and make sure it's deleted, so
|
||||||
// actually were no tests, or there was some other non-build error.
|
// future events for this package will use a new packageBuilder.
|
||||||
if b.output.Contains(globalID) && len(b.tests) == 0 {
|
pb := b.getPackageBuilder(packageName)
|
||||||
|
delete(b.packageBuilders, packageName)
|
||||||
|
pb.output.SetActiveID(0)
|
||||||
|
|
||||||
|
if pb.IsEmpty() {
|
||||||
|
return pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've collected output, but there were no tests, then this package
|
||||||
|
// had a runtime error or it simply didn't have any tests.
|
||||||
|
if pb.output.Contains(globalID) && len(pb.tests) == 0 {
|
||||||
if parseResult(result) == gtr.Fail {
|
if parseResult(result) == gtr.Fail {
|
||||||
pkg.RunError = gtr.Error{
|
pkg.RunError = gtr.Error{
|
||||||
Name: name,
|
Name: newPackageName,
|
||||||
Output: b.output.Get(globalID),
|
Output: pb.output.Get(globalID),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pkg.Output = b.output.Get(globalID)
|
pkg.Output = pb.output.Get(globalID)
|
||||||
}
|
}
|
||||||
b.packages = append(b.packages, pkg)
|
pb.output.Clear(globalID)
|
||||||
b.output.Clear(globalID)
|
return pkg
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the summary result says we failed, but there were no failing tests
|
// If the summary result says we failed, but there were no failing tests
|
||||||
// then something else must have failed.
|
// then something else must have failed.
|
||||||
if parseResult(result) == gtr.Fail && len(b.tests) > 0 && !b.containsFailures() {
|
if parseResult(result) == gtr.Fail && len(pb.tests) > 0 && !pb.containsFailures() {
|
||||||
pkg.RunError = gtr.Error{
|
pkg.RunError = gtr.Error{
|
||||||
Name: name,
|
Name: newPackageName,
|
||||||
Output: b.output.Get(globalID),
|
Output: pb.output.Get(globalID),
|
||||||
}
|
}
|
||||||
b.output.Clear(globalID)
|
pb.output.Clear(globalID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect tests for this package, maintaining insertion order.
|
// Collect tests for this package
|
||||||
var tests []gtr.Test
|
var tests []gtr.Test
|
||||||
for id := 1; id < b.nextID; id++ {
|
for id, t := range pb.tests {
|
||||||
if t, ok := b.tests[id]; ok {
|
if pb.isParent(id) {
|
||||||
if b.isParent(id) {
|
|
||||||
if b.subtestMode == IgnoreParentResults {
|
if b.subtestMode == IgnoreParentResults {
|
||||||
t.Result = gtr.Pass
|
t.Result = gtr.Pass
|
||||||
} else if b.subtestMode == ExcludeParents {
|
} else if b.subtestMode == ExcludeParents {
|
||||||
b.output.Merge(id, globalID)
|
pb.output.Merge(id, globalID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Output = b.output.Get(id)
|
t.Output = pb.output.Get(id)
|
||||||
tests = append(tests, t)
|
tests = append(tests, t)
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tests = groupBenchmarksByName(tests, b.output)
|
tests = groupBenchmarksByName(tests, b.output)
|
||||||
|
|
||||||
pkg.Coverage = b.coverage
|
// Sort packages by id to ensure we maintain insertion order.
|
||||||
pkg.Output = b.output.Get(globalID)
|
sort.Slice(tests, func(i, j int) bool {
|
||||||
pkg.Tests = tests
|
return tests[i].ID < tests[j].ID
|
||||||
b.packages = append(b.packages, pkg)
|
})
|
||||||
|
|
||||||
// reset state, except for nextID to ensure all id's are unique.
|
pkg.Tests = groupBenchmarksByName(tests, pb.output)
|
||||||
b.output.SetActiveID(0)
|
pkg.Coverage = pb.coverage
|
||||||
b.output.Clear(globalID)
|
pkg.Output = pb.output.Get(globalID)
|
||||||
b.coverage = 0
|
pb.output.Clear(globalID)
|
||||||
b.tests = make(map[int]gtr.Test)
|
return pkg
|
||||||
b.parentIDs = make(map[int]struct{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coverage sets the code coverage percentage.
|
// parseResult returns a gtr.Result for the given result string r.
|
||||||
func (b *reportBuilder) Coverage(pct float64, packages []string) {
|
|
||||||
b.coverage = pct
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendOutput appends the given text to the currently active context. If no
|
|
||||||
// active context exists, the output is assumed to belong to the package.
|
|
||||||
func (b *reportBuilder) AppendOutput(text string) {
|
|
||||||
b.output.Append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findTest returns the id of the most recently created test with the given
|
|
||||||
// name if it exists.
|
|
||||||
func (b *reportBuilder) findTest(name string) (int, bool) {
|
|
||||||
for i := b.nextID; i > 0; i-- {
|
|
||||||
if test, ok := b.tests[i]; ok && test.Name == name {
|
|
||||||
return i, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *reportBuilder) findTestParentID(name string) (int, bool) {
|
|
||||||
parent := dropLastSegment(name)
|
|
||||||
for parent != "" {
|
|
||||||
if id, ok := b.findTest(parent); ok {
|
|
||||||
return id, true
|
|
||||||
}
|
|
||||||
parent = dropLastSegment(parent)
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *reportBuilder) isParent(id int) bool {
|
|
||||||
_, ok := b.parentIDs[id]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func dropLastSegment(name string) string {
|
|
||||||
if idx := strings.LastIndexByte(name, '/'); idx >= 0 {
|
|
||||||
return name[:idx]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsFailures return true if the current list of tests contains at least
|
|
||||||
// one failing test or an unknown result.
|
|
||||||
func (b *reportBuilder) containsFailures() bool {
|
|
||||||
for _, test := range b.tests {
|
|
||||||
if test.Result == gtr.Fail || test.Result == gtr.Unknown {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseResult returns a Result for the given string r.
|
|
||||||
func parseResult(r string) gtr.Result {
|
func parseResult(r string) gtr.Result {
|
||||||
switch r {
|
switch r {
|
||||||
case "PASS":
|
case "PASS":
|
||||||
@ -345,6 +237,8 @@ func parseResult(r string) gtr.Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// groupBenchmarksByName groups tests with the Benchmark prefix if they have
|
||||||
|
// the same name and combines their output.
|
||||||
func groupBenchmarksByName(tests []gtr.Test, output *collector.Output) []gtr.Test {
|
func groupBenchmarksByName(tests []gtr.Test, output *collector.Output) []gtr.Test {
|
||||||
if len(tests) == 0 {
|
if len(tests) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -406,6 +300,7 @@ func groupBenchmarksByName(tests []gtr.Test, output *collector.Output) []gtr.Tes
|
|||||||
return grouped
|
return grouped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// combinedDuration returns the sum of the durations of the given tests.
|
||||||
func combinedDuration(tests []gtr.Test) time.Duration {
|
func combinedDuration(tests []gtr.Test) time.Duration {
|
||||||
var total time.Duration
|
var total time.Duration
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -414,6 +309,7 @@ func combinedDuration(tests []gtr.Test) time.Duration {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// groupResults returns the result we should use for a collection of tests.
|
||||||
func groupResults(tests []gtr.Test) gtr.Result {
|
func groupResults(tests []gtr.Test) gtr.Result {
|
||||||
var result gtr.Result
|
var result gtr.Result
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -426,3 +322,164 @@ func groupResults(tests []gtr.Test) gtr.Result {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// packageBuilder helps build a gtr.Package from a collection of test events.
|
||||||
|
type packageBuilder struct {
|
||||||
|
generateID func() int
|
||||||
|
output *collector.Output
|
||||||
|
|
||||||
|
tests map[int]gtr.Test
|
||||||
|
parentIDs map[int]struct{} // set of test id's that contain subtests
|
||||||
|
coverage float64 // coverage percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPackageBuilder creates a new packageBuilder. New tests will be assigned
|
||||||
|
// an ID returned by the generateID function. The activeIDSetter is called to
|
||||||
|
// set or reset the active test id.
|
||||||
|
func newPackageBuilder(generateID func() int, output *collector.Output) *packageBuilder {
|
||||||
|
return &packageBuilder{
|
||||||
|
generateID: generateID,
|
||||||
|
output: output,
|
||||||
|
tests: make(map[int]gtr.Test),
|
||||||
|
parentIDs: make(map[int]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if this package builder does not have any tests and has
|
||||||
|
// not collected any global output.
|
||||||
|
func (b packageBuilder) IsEmpty() bool {
|
||||||
|
return len(b.tests) == 0 && !b.output.Contains(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTest adds a test with the given name to the package, marks it as
|
||||||
|
// active and returns its generated id.
|
||||||
|
func (b *packageBuilder) CreateTest(name string) int {
|
||||||
|
if parentID, ok := b.findTestParentID(name); ok {
|
||||||
|
b.parentIDs[parentID] = struct{}{}
|
||||||
|
}
|
||||||
|
id := b.generateID()
|
||||||
|
b.output.SetActiveID(id)
|
||||||
|
b.tests[id] = gtr.NewTest(id, name)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// PauseTest marks the test with the given name no longer active. Any results
|
||||||
|
// or output added to the package after calling PauseTest will no longer be
|
||||||
|
// associated with this test.
|
||||||
|
func (b *packageBuilder) PauseTest(name string) {
|
||||||
|
b.output.SetActiveID(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContinueTest finds the test with the given name and marks it as active. If
|
||||||
|
// more than one test exist with this name, the most recently created test will
|
||||||
|
// be used.
|
||||||
|
func (b *packageBuilder) ContinueTest(name string) {
|
||||||
|
id, _ := b.findTest(name)
|
||||||
|
b.output.SetActiveID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndTest finds the test with the given name, sets the result, duration and
|
||||||
|
// level. If more than one test exists with this name, the most recently
|
||||||
|
// created test will be used. If no test exists with this name, a new test is
|
||||||
|
// created. The test is then marked as no longer active.
|
||||||
|
func (b *packageBuilder) EndTest(name, result string, duration time.Duration, level int) {
|
||||||
|
id, ok := b.findTest(name)
|
||||||
|
if !ok {
|
||||||
|
// test did not exist, create one
|
||||||
|
// TODO: Likely reason is that the user ran go test without the -v
|
||||||
|
// flag, should we report this somewhere?
|
||||||
|
id = b.CreateTest(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := b.tests[id]
|
||||||
|
t.Result = parseResult(result)
|
||||||
|
t.Duration = duration
|
||||||
|
t.Level = level
|
||||||
|
b.tests[id] = t
|
||||||
|
b.output.SetActiveID(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// End resets the active test.
|
||||||
|
func (b *packageBuilder) End() {
|
||||||
|
b.output.SetActiveID(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkResult updates an existing or adds a new test with the given
|
||||||
|
// results and marks it as active. If an existing test with this name exists
|
||||||
|
// but without result, then that one is updated. Otherwise a new one is added
|
||||||
|
// to the report.
|
||||||
|
func (b *packageBuilder) BenchmarkResult(name string, iterations int64, nsPerOp, mbPerSec float64, bytesPerOp, allocsPerOp int64) {
|
||||||
|
id, ok := b.findTest(name)
|
||||||
|
if !ok || b.tests[id].Result != gtr.Unknown {
|
||||||
|
id = b.CreateTest(name)
|
||||||
|
}
|
||||||
|
b.output.SetActiveID(id)
|
||||||
|
|
||||||
|
benchmark := Benchmark{iterations, nsPerOp, mbPerSec, bytesPerOp, allocsPerOp}
|
||||||
|
test := gtr.NewTest(id, name)
|
||||||
|
test.Result = gtr.Pass
|
||||||
|
test.Duration = benchmark.ApproximateDuration()
|
||||||
|
SetBenchmarkData(&test, benchmark)
|
||||||
|
b.tests[id] = test
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coverage sets the code coverage percentage.
|
||||||
|
func (b *packageBuilder) Coverage(pct float64, packages []string) {
|
||||||
|
b.coverage = pct
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output appends data to the output of this package.
|
||||||
|
func (b *packageBuilder) Output(data string) {
|
||||||
|
b.output.Append(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findTest returns the id of the most recently created test with the given
|
||||||
|
// name if it exists.
|
||||||
|
func (b *packageBuilder) findTest(name string) (int, bool) {
|
||||||
|
var maxid int
|
||||||
|
for id, test := range b.tests {
|
||||||
|
if maxid < id && test.Name == name {
|
||||||
|
maxid = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxid, maxid > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// findTestParentID searches the existing tests in this package for a parent of
|
||||||
|
// the test with the given name, and returns its id if one is found.
|
||||||
|
func (b *packageBuilder) findTestParentID(name string) (int, bool) {
|
||||||
|
parent := dropLastSegment(name)
|
||||||
|
for parent != "" {
|
||||||
|
if id, ok := b.findTest(parent); ok {
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
parent = dropLastSegment(parent)
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isParent returns true if the test with the given id has sub tests.
|
||||||
|
func (b *packageBuilder) isParent(id int) bool {
|
||||||
|
_, ok := b.parentIDs[id]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropLastSegment strips the last `/` and everything following it from the
|
||||||
|
// given name. If no `/` was found, the empty string is returned.
|
||||||
|
func dropLastSegment(name string) string {
|
||||||
|
if idx := strings.LastIndexByte(name, '/'); idx >= 0 {
|
||||||
|
return name[:idx]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsFailures return true if this package contains at least one failing
|
||||||
|
// test or a test with an unknown result.
|
||||||
|
func (b *packageBuilder) containsFailures() bool {
|
||||||
|
for _, test := range b.tests {
|
||||||
|
if test.Result == gtr.Fail || test.Result == gtr.Unknown {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -125,6 +125,66 @@ func TestReport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildReportMultiplePackages(t *testing.T) {
|
||||||
|
events := []Event{
|
||||||
|
{Package: "package/name1", Type: "run_test", Name: "TestOne"},
|
||||||
|
{Package: "package/name2", Type: "run_test", Name: "TestOne"},
|
||||||
|
{Package: "package/name1", Type: "output", Data: "\tHello"},
|
||||||
|
{Package: "package/name1", Type: "end_test", Name: "TestOne", Result: "PASS", Duration: 1 * time.Millisecond},
|
||||||
|
{Package: "package/name2", Type: "output", Data: "\tfile_test.go:10: error"},
|
||||||
|
{Package: "package/name2", Type: "end_test", Name: "TestOne", Result: "FAIL", Duration: 1 * time.Millisecond},
|
||||||
|
{Package: "package/name2", Type: "status", Result: "FAIL"},
|
||||||
|
{Package: "package/name2", Type: "summary", Result: "FAIL", Name: "package/name2", Duration: 1 * time.Millisecond},
|
||||||
|
{Package: "package/name1", Type: "status", Result: "PASS"},
|
||||||
|
{Package: "package/name1", Type: "summary", Result: "ok", Name: "package/name1", Duration: 1 * time.Millisecond},
|
||||||
|
}
|
||||||
|
|
||||||
|
want := gtr.Report{
|
||||||
|
Packages: []gtr.Package{
|
||||||
|
{
|
||||||
|
Name: "package/name2",
|
||||||
|
Duration: 1 * time.Millisecond,
|
||||||
|
Timestamp: testTimestamp,
|
||||||
|
Tests: []gtr.Test{
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "TestOne",
|
||||||
|
Duration: 1 * time.Millisecond,
|
||||||
|
Result: gtr.Fail,
|
||||||
|
Output: []string{"\tfile_test.go:10: error"},
|
||||||
|
Data: make(map[string]interface{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "package/name1",
|
||||||
|
Duration: 1 * time.Millisecond,
|
||||||
|
Timestamp: testTimestamp,
|
||||||
|
Tests: []gtr.Test{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "TestOne",
|
||||||
|
Duration: 1 * time.Millisecond,
|
||||||
|
Result: gtr.Pass,
|
||||||
|
Output: []string{"\tHello"},
|
||||||
|
Data: make(map[string]interface{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rb := newReportBuilder()
|
||||||
|
rb.timestampFunc = testTimestampFunc
|
||||||
|
for _, ev := range events {
|
||||||
|
rb.ProcessEvent(ev)
|
||||||
|
}
|
||||||
|
got := rb.Build()
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("FromEvents report incorrect, diff (-want, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubtestModes(t *testing.T) {
|
func TestSubtestModes(t *testing.T) {
|
||||||
events := []Event{
|
events := []Event{
|
||||||
{Type: "run_test", Name: "TestParent"},
|
{Type: "run_test", Name: "TestParent"},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user