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. // NewParser returns a new Go test output parser.
func NewParser(options ...Option) *Parser { func NewParser(options ...Option) *Parser {
p := &Parser{} p := &Parser{}
@ -68,7 +112,9 @@ func NewParser(options ...Option) *Parser {
// Parser is a Go test output Parser. // Parser is a Go test output Parser.
type Parser struct { type Parser struct {
packageName string packageName string
subtestMode SubtestMode
timestampFunc func() time.Time timestampFunc func() time.Time
events []Event events []Event
@ -89,6 +135,7 @@ func (p *Parser) Parse(r io.Reader) (gtr.Report, error) {
func (p *Parser) report(events []Event) gtr.Report { func (p *Parser) report(events []Event) gtr.Report {
rb := newReportBuilder() rb := newReportBuilder()
rb.packageName = p.packageName rb.packageName = p.packageName
rb.subtestMode = p.subtestMode
if p.timestampFunc != nil { if p.timestampFunc != nil {
rb.timestampFunc = p.timestampFunc rb.timestampFunc = p.timestampFunc
} }

View File

@ -12,7 +12,8 @@ import (
) )
var ( 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 { 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) got := parser.report(events)
if diff := cmp.Diff(want, got); diff != "" { if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("FromEvents report incorrect, diff (-want, +got):\n%v", 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 package gotest
import ( import (
"fmt"
"strings"
"time" "time"
"github.com/jstemmer/go-junit-report/v2/gtr" "github.com/jstemmer/go-junit-report/v2/gtr"
@ -22,13 +24,15 @@ type reportBuilder struct {
runErrors map[int]gtr.Error runErrors map[int]gtr.Error
// state // state
nextID int // next free unused id nextID int // next free unused id
lastID int // most recently created id lastID int // most recently created id
output []string // output that does not belong to any test output []string // output that does not belong to any test
coverage float64 // coverage percentage coverage float64 // coverage percentage
parentIDs map[int]struct{} // set of test id's that contain subtests
// options // options
packageName string packageName string
subtestMode SubtestMode
timestampFunc func() time.Time timestampFunc func() time.Time
} }
@ -40,6 +44,7 @@ func newReportBuilder() *reportBuilder {
buildErrors: make(map[int]gtr.Error), buildErrors: make(map[int]gtr.Error),
runErrors: make(map[int]gtr.Error), runErrors: make(map[int]gtr.Error),
nextID: 1, nextID: 1,
parentIDs: make(map[int]struct{}),
timestampFunc: time.Now, 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 // CreateTest adds a test with the given name to the report, and marks it as
// active. // active.
func (b *reportBuilder) CreateTest(name string) { 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} 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 var benchmarks []gtr.Benchmark
for id := 1; id < b.nextID; id++ { for id := 1; id < b.nextID; id++ {
if t, ok := b.tests[id]; ok { 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) tests = append(tests, t)
continue continue
} }
@ -255,6 +271,7 @@ func (b *reportBuilder) CreatePackage(name, result string, duration time.Duratio
b.coverage = 0 b.coverage = 0
b.tests = make(map[int]gtr.Test) b.tests = make(map[int]gtr.Test)
b.benchmarks = make(map[int]gtr.Benchmark) b.benchmarks = make(map[int]gtr.Benchmark)
b.parentIDs = make(map[int]struct{})
} }
// Coverage sets the code coverage percentage. // 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 { if t, ok := b.tests[b.lastID]; ok && t.Name == name {
return b.lastID, true return b.lastID, true
} }
for id := len(b.tests); id >= 0; id-- { for i := b.nextID; i >= 0; i-- {
if b.tests[id].Name == name { if test, ok := b.tests[i]; ok && test.Name == name {
return id, true return i, true
} }
} }
return 0, false 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 // findBenchmark returns the id of the most recently created benchmark with the
// given name if it exists. // given name if it exists.
func (b *reportBuilder) findBenchmark(name string) (int, bool) { func (b *reportBuilder) findBenchmark(name string) (int, bool) {