mirror of
https://github.com/jstemmer/go-junit-report.git
synced 2025-05-20 19:04:29 -05:00
parser/gotest: add SubtestMode to configure how to deal with subtests
This commit is contained in:
parent
6c038bc425
commit
1b7027fde7
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user