Skip to content

Commit d0f1d21

Browse files
authored
Merge pull request #18 from jaypipes/json-xml-out
add XUnit and JSON output for run results
2 parents 7c05a0e + 49cf338 commit d0f1d21

4 files changed

Lines changed: 219 additions & 61 deletions

File tree

api/xunit.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Use and distribution licensed under the Apache license version 2.
2+
//
3+
// See the COPYING file in the root project directory for full text.
4+
5+
package api
6+
7+
import (
8+
"encoding/xml"
9+
"time"
10+
)
11+
12+
// XUnitResults is a wrapper struct allowing the output of well-formed
13+
// JUnit/XUnit XML and JSON serialized scenario results
14+
type XUnitResults struct {
15+
XMLName xml.Name `json:"-" xml:"testsuites"`
16+
TestSuites []XUnitTestSuite `json:"testsuites" xml:"testsuite"`
17+
}
18+
19+
type XUnitTestSuite struct {
20+
Name string `json:"name" xml:"name,attr"`
21+
Time string `json:"time,omitempty" xml:"time,attr"`
22+
Skipped bool `json:"skipped,omitempty" xml:"skipped,omitempty"`
23+
Failures int `json:"failures" xml:"failures,attr"`
24+
Errors int `json:"errors" xml:"errors,attr"`
25+
Tests int `json:"tests" xml:"tests,attr"`
26+
Timestamp time.Time `json:"timestamp" xml:"timestamp,attr"`
27+
Properties []XUnitProperty `json:"properties,omitempty" xml:"properties,omitempty"`
28+
TestCases []XUnitTestCase `json:"testcases,omitempty" xml:"testcases,omitempty"`
29+
}
30+
31+
type XUnitProperty struct {
32+
Name string `json:"name" xml:"name,attr"`
33+
Value string `json:"value" xml:"value,attr"`
34+
}
35+
36+
type XUnitTestCase struct {
37+
Name string `json:"name" xml:"name,attr"`
38+
Time string `json:"time,omitempty" xml:"time,attr"`
39+
Status string `json:"status" xml:"status,attr"`
40+
Assertions int `json:"assertions" xml:"assertions,attr,omitempty"`
41+
Skipped bool `json:"skipped,omitempty" xml:"skipped,omitempty"`
42+
Failure *XUnitMessage `json:"failure,omitempty" xml:"failure,omitempty"`
43+
Error *XUnitMessage `json:"error,omitempty" xml:"error,omitempty"`
44+
SystemOut *XUnitTextBlock `json:"system-out,omitempty" xml:"system-out,omitempty"`
45+
SystemErr *XUnitTextBlock `json:"system-err,omitempty" xml:"system-err,omitempty"`
46+
}
47+
48+
type XUnitTextBlock struct {
49+
Text string `xml:",cdata"`
50+
}
51+
52+
type XUnitMessage struct {
53+
Message string `json:"message,omitempty" xml:"message,attr"`
54+
Type string `json:"type,omitempty" xml:"type,attr"`
55+
}

run/new.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
package run
66

7+
import "github.com/gdt-dev/core/testunit"
8+
79
type Option func(*Run)
810

911
// New returns a new Run object that stores test run state.
1012
func New(opts ...Option) *Run {
1113
r := &Run{
12-
scenarioResults: map[string][]TestUnitResult{},
14+
scenarioResults: map[string][]testunit.Result{},
1315
}
1416
for _, opt := range opts {
1517
opt(r)

run/run.go

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
package run
22

33
import (
4+
"path/filepath"
45
"slices"
56
"time"
67

8+
"github.com/samber/lo"
9+
710
"github.com/gdt-dev/core/api"
811
"github.com/gdt-dev/core/testunit"
9-
"github.com/samber/lo"
1012
)
1113

1214
// Run stores state of a test run when tests are executed with the `gdt` CLI
1315
// tool.
1416
type Run struct {
1517
// scenarioResults is a map, keyed by the Scenario path, of slices of
16-
// TestUnitResult structs corresponding to the test specs in the scenario.
17-
// There is guaranteed to be exactly the same number of TestUnitResults in
18+
// testunit.Result structs corresponding to the test specs in the scenario.
19+
// There is guaranteed to be exactly the same number of testunit.Results in
1820
// the slice as scenarios in the scenario.
19-
scenarioResults map[string][]TestUnitResult
21+
scenarioResults map[string][]testunit.Result
2022
}
2123

2224
// OK returns true if all Scenarios in the Run had all successful test units.
2325
func (r *Run) OK() bool {
24-
return lo.EveryBy(lo.Values(r.scenarioResults), func(results []TestUnitResult) bool {
25-
return lo.EveryBy(results, func(tur TestUnitResult) bool {
26-
return tur.OK()
27-
})
28-
})
26+
return lo.EveryBy(
27+
lo.Values(r.scenarioResults),
28+
func(results []testunit.Result) bool {
29+
return lo.EveryBy(results, func(tur testunit.Result) bool {
30+
return tur.OK()
31+
})
32+
},
33+
)
2934
}
3035

3136
// ScenarioPaths returns a sorted list of Scenario Paths.
@@ -35,9 +40,9 @@ func (r *Run) ScenarioPaths() []string {
3540
return paths
3641
}
3742

38-
// ScenarioResults returns the set of TestUnitResults for a Scenario with the
43+
// ScenarioResults returns the set of testunit.Results for a Scenario with the
3944
// supplied path.
40-
func (r *Run) ScenarioResults(path string) []TestUnitResult {
45+
func (r *Run) ScenarioResults(path string) []testunit.Result {
4146
return r.scenarioResults[path]
4247
}
4348

@@ -49,63 +54,52 @@ func (r *Run) StoreResult(
4954
res *api.Result,
5055
) {
5156
if _, ok := r.scenarioResults[path]; !ok {
52-
r.scenarioResults[path] = []TestUnitResult{}
57+
r.scenarioResults[path] = []testunit.Result{}
5358
}
5459
r.scenarioResults[path] = append(
5560
r.scenarioResults[path],
56-
TestUnitResult{
57-
index: index,
58-
name: tu.Name(),
59-
elapsed: tu.Elapsed(),
60-
skipped: tu.Skipped(),
61-
failures: res.Failures(),
62-
detail: tu.Detail(),
63-
},
61+
testunit.NewResult(index, tu, res),
6462
)
6563
}
6664

67-
// TestUnitResult stores a summary of the test execution of a single test unit.
68-
type TestUnitResult struct {
69-
// index is the 0-based index of the test unit within the test scenario.
70-
index int
71-
// name is the short name of the test unit
72-
name string
73-
// skipped is true if the test unit was skipped
74-
skipped bool
75-
// failures is the collection of assertion failures for the test spec that
76-
// occurred during the run. this will NOT include RuntimeErrors.
77-
failures []error
78-
// elapsed is the time take to execute the test unit
79-
elapsed time.Duration
80-
// detail is a buffer holding any log entries made during the run of the
81-
// test spec.
82-
detail string
83-
}
65+
// XUnit returns the Run's scenario results as a slice of structs that can be
66+
// serialized to either XML (JUnit/XUnit-style) or JSON.
67+
func (r *Run) XUnit() []api.XUnitTestSuite {
68+
suites := []api.XUnitTestSuite{}
69+
paths := r.ScenarioPaths()
70+
for _, path := range paths {
71+
shortPath := filepath.Base(path)
72+
suite := api.XUnitTestSuite{
73+
Name: shortPath,
74+
Properties: []api.XUnitProperty{
75+
{
76+
Name: "path",
77+
Value: path,
78+
},
79+
},
80+
Timestamp: time.Now(),
81+
}
8482

85-
func (u TestUnitResult) OK() bool {
86-
return len(u.failures) == 0
87-
}
83+
var scenElapsed time.Duration
8884

89-
func (u TestUnitResult) Name() string {
90-
return u.name
91-
}
85+
unitResults := r.ScenarioResults(path)
9286

93-
func (u TestUnitResult) Index() int {
94-
return u.index
95-
}
96-
97-
func (u TestUnitResult) Failures() []error {
98-
return u.failures
99-
}
100-
101-
func (u TestUnitResult) Skipped() bool {
102-
return u.skipped
103-
}
104-
105-
func (u TestUnitResult) Detail() string {
106-
return u.detail
107-
}
87+
testcases := make([]api.XUnitTestCase, len(unitResults))
88+
tcFails := 0
10889

109-
func (u TestUnitResult) Elapsed() time.Duration {
110-
return u.elapsed
90+
for x, res := range unitResults {
91+
tc := res.XUnit()
92+
if res.Failed() {
93+
tcFails++
94+
}
95+
scenElapsed += res.Elapsed()
96+
testcases[x] = tc
97+
}
98+
suite.Failures = tcFails
99+
suite.Tests = len(testcases)
100+
suite.Time = scenElapsed.String()
101+
suite.TestCases = testcases
102+
suites = append(suites, suite)
103+
}
104+
return suites
111105
}

testunit/result.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Use and distribution licensed under the Apache license version 2.
2+
//
3+
// See the COPYING file in the root project directory for full text.
4+
5+
package testunit
6+
7+
import (
8+
"time"
9+
10+
"github.com/gdt-dev/core/api"
11+
)
12+
13+
func NewResult(
14+
index int,
15+
tu *TestUnit,
16+
res *api.Result,
17+
) Result {
18+
return Result{
19+
index: index,
20+
name: tu.Name(),
21+
elapsed: tu.Elapsed(),
22+
skipped: tu.Skipped(),
23+
failures: res.Failures(),
24+
detail: tu.Detail(),
25+
}
26+
}
27+
28+
// Result stores a summary of the test execution of a single test unit.
29+
type Result struct {
30+
// index is the 0-based index of the test unit within the test scenario.
31+
index int
32+
// name is the short name of the test unit
33+
name string
34+
// skipped is true if the test unit was skipped
35+
skipped bool
36+
// failures is the collection of assertion failures for the test spec that
37+
// occurred during the run. this will NOT include RuntimeErrors.
38+
failures []error
39+
// elapsed is the time take to execute the test unit
40+
elapsed time.Duration
41+
// detail is a buffer holding any log entries made during the run of the
42+
// test spec.
43+
detail string
44+
}
45+
46+
// OK returns whether the test unit executed without any failed assertions.
47+
func (r Result) OK() bool {
48+
return len(r.failures) == 0
49+
}
50+
51+
// Name returns the name of the test unit.
52+
func (r Result) Name() string {
53+
return r.name
54+
}
55+
56+
// Index returns the 0-based index of the test unit within its containing test
57+
// scenario.
58+
func (r Result) Index() int {
59+
return r.index
60+
}
61+
62+
// Failures returns a slice of error messages indicating the test unit
63+
// assertion failures.
64+
func (r Result) Failures() []error {
65+
return r.failures
66+
}
67+
68+
// Failed returns whether the test unit failed any test assertions.
69+
func (r Result) Failed() bool {
70+
return !r.skipped && len(r.failures) > 0
71+
}
72+
73+
// Skipped returns whether the test unit was skipped during execution.
74+
func (r Result) Skipped() bool {
75+
return r.skipped
76+
}
77+
78+
// Detail returns the collected details/output of the test unit.
79+
func (r Result) Detail() string {
80+
return r.detail
81+
}
82+
83+
// Elapsed returns the elapsed time of the test unit.
84+
func (r Result) Elapsed() time.Duration {
85+
return r.elapsed
86+
}
87+
88+
// XUnit returns the testunit.Result as a struct that can be serialized to
89+
// JUnit/XUnit XML or JSON.
90+
func (r Result) XUnit() api.XUnitTestCase {
91+
tc := api.XUnitTestCase{
92+
Name: r.Name(),
93+
}
94+
if r.Skipped() {
95+
tc.Skipped = true
96+
tc.Status = "skip"
97+
} else if r.OK() {
98+
tc.Status = "ok"
99+
} else {
100+
tc.Status = "fail"
101+
}
102+
tc.Time = r.Elapsed().String()
103+
tc.SystemOut = &api.XUnitTextBlock{
104+
Text: r.Detail(),
105+
}
106+
return tc
107+
}

0 commit comments

Comments
 (0)