parser/gotest: add SubtestMode to configure how to deal with subtests

This commit is contained in:
Joël Stemmer 2022-05-22 00:32:17 +01:00
parent 6c038bc425
commit 1b7027fde7
3 changed files with 190 additions and 10 deletions

View File

@ -57,6 +57,50 @@ func TimestampFunc(f func() time.Time) Option {
}
}
// SubtestMode configures how Go subtests should be handled by the parser.
type SubtestMode string
const (
// SubtestModeDefault is the default subtest mode. It treats tests with
// subtests as any other tests.
SubtestModeDefault SubtestMode = ""
// IgnoreParentResults ignores test results for tests with subtests. Use
// this mode if you use subtest parents for common setup/teardown, but are
// not interested in counting them as failed tests. Ignoring their results
// still preserves these tests and their captured output in the report.
IgnoreParentResults SubtestMode = "ignore-parent-results"
// ExcludeParents excludes tests that contain subtests from the report.
// Note that the subtests themselves are not removed. Use this mode if you
// use subtest parents for common setup/teardown, but are not actually
// interested in their presence in the created report. If output was
// captured for tests that are removed, the output is preserved in the
// global report output.
ExcludeParents SubtestMode = "exclude-parents"
)
// ParseSubtestMode returns a SubtestMode for the given string.
func ParseSubtestMode(in string) (SubtestMode, error) {
switch in {
case string(IgnoreParentResults):
return IgnoreParentResults, nil
case string(ExcludeParents):
return ExcludeParents, nil
default:
return SubtestModeDefault, fmt.Errorf("unknown subtest mode: %v", in)
}
}
// SetSubtestMode is an Option to change how the parser handles tests with
// subtests. See the documentation for the individual SubtestModes for more
// information.
func SetSubtestMode(mode SubtestMode) Option {
return func(p *Parser) {
p.subtestMode = mode
}
}
// NewParser returns a new Go test output parser.
func NewParser(options ...Option) *Parser {
p := &Parser{}
@ -68,7 +112,9 @@ func NewParser(options ...Option) *Parser {
// Parser is a Go test output Parser.
type Parser struct {
packageName string
packageName string
subtestMode SubtestMode
timestampFunc func() time.Time
events []Event
@ -89,6 +135,7 @@ func (p *Parser) Parse(r io.Reader) (gtr.Report, error) {
func (p *Parser) report(events []Event) gtr.Report {
rb := newReportBuilder()
rb.packageName = p.packageName
rb.subtestMode = p.subtestMode
if p.timestampFunc != nil {
rb.timestampFunc = p.timestampFunc
}

View File

@ -12,7 +12,8 @@ import (
)
var (
testTimestamp = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
testTimestamp = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
testTimestampFunc = func() time.Time { return testTimestamp }
)
type parseLineTest struct {
@ -308,9 +309,101 @@ func TestReport(t *testing.T) {
},
}
parser := NewParser(TimestampFunc(func() time.Time { return testTimestamp }))
parser := NewParser(TimestampFunc(testTimestampFunc))
got := parser.report(events)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("FromEvents report incorrect, diff (-want, +got):\n%v", diff)
}
}
func TestSubtestModes(t *testing.T) {
events := []Event{
{Type: "run_test", Name: "TestParent"},
{Type: "output", Data: "TestParent output"},
{Type: "run_test", Name: "TestParent/Subtest#1"},
{Type: "output", Data: "Subtest#1 output"},
{Type: "run_test", Name: "TestParent/Subtest#2"},
{Type: "output", Data: "Subtest#2 output"},
{Type: "end_test", Name: "TestParent", Result: "PASS", Duration: 1 * time.Millisecond},
{Type: "end_test", Name: "TestParent/Subtest#1", Result: "FAIL", Duration: 2 * time.Millisecond},
{Type: "end_test", Name: "TestParent/Subtest#2", Result: "PASS", Duration: 3 * time.Millisecond},
{Type: "summary", Result: "FAIL", Name: "package/name", Duration: 1 * time.Millisecond},
}
tests := []struct {
name string
mode SubtestMode
want gtr.Report
}{
{
name: "ignore subtest parent results",
mode: IgnoreParentResults,
want: gtr.Report{
Packages: []gtr.Package{
{
Name: "package/name",
Duration: 1 * time.Millisecond,
Timestamp: testTimestamp,
Tests: []gtr.Test{
{
Name: "TestParent",
Duration: 1 * time.Millisecond,
Result: gtr.Pass,
Output: []string{"TestParent output"},
},
{
Name: "TestParent/Subtest#1",
Duration: 2 * time.Millisecond,
Result: gtr.Fail,
Output: []string{"Subtest#1 output"},
},
{
Name: "TestParent/Subtest#2",
Duration: 3 * time.Millisecond,
Result: gtr.Pass,
Output: []string{"Subtest#2 output"},
},
},
},
},
},
},
{
name: "exclude subtest parents",
mode: ExcludeParents,
want: gtr.Report{
Packages: []gtr.Package{
{
Name: "package/name",
Duration: 1 * time.Millisecond,
Timestamp: testTimestamp,
Tests: []gtr.Test{
{
Name: "TestParent/Subtest#1",
Duration: 2 * time.Millisecond,
Result: gtr.Fail,
Output: []string{"Subtest#1 output"},
},
{
Name: "TestParent/Subtest#2",
Duration: 3 * time.Millisecond,
Result: gtr.Pass,
Output: []string{"Subtest#2 output"},
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
parser := NewParser(TimestampFunc(testTimestampFunc), SetSubtestMode(test.mode))
got := parser.report(events)
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Invalid report created from events, diff (-want, +got):\n%v", diff)
}
})
}
}

View File

@ -1,6 +1,8 @@
package gotest
import (
"fmt"
"strings"
"time"
"github.com/jstemmer/go-junit-report/v2/gtr"
@ -22,13 +24,15 @@ type reportBuilder struct {
runErrors map[int]gtr.Error
// state
nextID int // next free unused id
lastID int // most recently created id
output []string // output that does not belong to any test
coverage float64 // coverage percentage
nextID int // next free unused id
lastID int // most recently created id
output []string // output that does not belong to any test
coverage float64 // coverage percentage
parentIDs map[int]struct{} // set of test id's that contain subtests
// options
packageName string
subtestMode SubtestMode
timestampFunc func() time.Time
}
@ -40,6 +44,7 @@ func newReportBuilder() *reportBuilder {
buildErrors: make(map[int]gtr.Error),
runErrors: make(map[int]gtr.Error),
nextID: 1,
parentIDs: make(map[int]struct{}),
timestampFunc: time.Now,
}
}
@ -71,6 +76,9 @@ func (b *reportBuilder) Build() gtr.Report {
// CreateTest adds a test with the given name to the report, and marks it as
// active.
func (b *reportBuilder) CreateTest(name string) {
if parentID, ok := b.findTestParentID(name); ok {
b.parentIDs[parentID] = struct{}{}
}
b.tests[b.newID()] = gtr.Test{Name: name}
}
@ -233,6 +241,14 @@ func (b *reportBuilder) CreatePackage(name, result string, duration time.Duratio
var benchmarks []gtr.Benchmark
for id := 1; id < b.nextID; id++ {
if t, ok := b.tests[id]; ok {
if b.isParent(id) {
if b.subtestMode == IgnoreParentResults {
t.Result = gtr.Pass
} else if b.subtestMode == ExcludeParents {
fmt.Printf("excluding test %v\n", t.Name)
continue
}
}
tests = append(tests, t)
continue
}
@ -255,6 +271,7 @@ func (b *reportBuilder) CreatePackage(name, result string, duration time.Duratio
b.coverage = 0
b.tests = make(map[int]gtr.Test)
b.benchmarks = make(map[int]gtr.Benchmark)
b.parentIDs = make(map[int]struct{})
}
// Coverage sets the code coverage percentage.
@ -291,14 +308,37 @@ func (b *reportBuilder) findTest(name string) (int, bool) {
if t, ok := b.tests[b.lastID]; ok && t.Name == name {
return b.lastID, true
}
for id := len(b.tests); id >= 0; id-- {
if b.tests[id].Name == name {
return id, true
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 ""
}
// findBenchmark returns the id of the most recently created benchmark with the
// given name if it exists.
func (b *reportBuilder) findBenchmark(name string) (int, bool) {