Skip to content

Commit 29e3973

Browse files
authored
Grouping subtests using test suites in test.xml (#4275)
**What type of PR is this?** Feature **What does this PR do? Why is it needed?** This PR makes every test function a `<testsuite>` and every subtest a `<testcase>`. This allow us to record the start time of every test function, because only `<testsuite>` has a timestamp attribute. This approach is also consistent with [testify suite](https://pkg.go.dev/github.com/stretchr/testify/suite), each of which becomes a `<testsuite>`. Note that in the test.xml, a test function is both a `<testsuite>` and a `<testcase>`. It has to be a `<testcase>` because the test function itself has its own duration that is independent of its subtests. Also, it can have assertions outside all subtests, it can fail even all its subtests succeed. If no subtest is used, every `<testsuite>` only has one `<testcase>`, which is more verbose than before. Consumers of test.xml that only read `<testcase>` are not affected by this change. but those assuming only one `<testsuite>` will need to be updated. We also lose track of the duration of the whole package, but that can be obtained from Bazel build events.
1 parent bd074fa commit 29e3973

File tree

5 files changed

+93
-49
lines changed

5 files changed

+93
-49
lines changed
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
<testsuites>
2-
<testsuite errors="0" failures="0" skipped="0" tests="0" time="" name="pkg/testing"></testsuite>
3-
</testsuites>
1+
<testsuites></testsuites>

go/tools/bzltestutil/testdata/report.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
<testsuites>
2-
<testsuite errors="0" failures="3" skipped="1" tests="7" time="0.030" name="pkg/testing">
2+
<testsuite errors="0" failures="1" skipped="0" tests="1" time="0.000" name="pkg/testing.TestFail">
33
<testcase classname="testing" name="TestFail" time="0.000">
44
<failure message="Failed" type="">=== RUN TestFail&#xA;--- FAIL: TestFail (0.00s)&#xA; test_test.go:23: Not working&#xA;</failure>
55
</testcase>
6+
</testsuite>
7+
<testsuite errors="0" failures="0" skipped="0" tests="1" time="0.000" name="pkg/testing.TestPass">
68
<testcase classname="testing" name="TestPass" time="0.000"></testcase>
9+
</testsuite>
10+
<testsuite errors="0" failures="0" skipped="0" tests="1" time="0.000" name="pkg/testing.TestPassLog">
711
<testcase classname="testing" name="TestPassLog" time="0.000"></testcase>
12+
</testsuite>
13+
<testsuite errors="0" failures="2" skipped="1" tests="4" time="0.020" name="pkg/testing.TestSubtests">
814
<testcase classname="testing" name="TestSubtests" time="0.020">
915
<failure message="Failed" type="">=== RUN TestSubtests&#xA;--- FAIL: TestSubtests (0.02s)&#xA;</failure>
1016
</testcase>

go/tools/bzltestutil/testdata/timeout.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<testsuites>
2-
<testsuite errors="2" failures="0" skipped="0" tests="5" time="8.555" name="pkg/testing" timestamp="2025-02-07T17:15:47.557Z">
2+
<testsuite errors="2" failures="0" skipped="0" tests="5" time="8.000" name="pkg/testing.TestReport" timestamp="2025-02-07T17:15:48.107Z">
33
<testcase classname="testing" name="TestReport" time="8.000">
44
<error message="Interrupted" type="">=== RUN TestReport&#xA;&#x9;&#x9;TestReport (8s)&#xA;</error>
55
</testcase>

go/tools/bzltestutil/xml.go

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type testCase struct {
7272
state string
7373
output strings.Builder
7474
duration *float64
75+
timestamp *time.Time
7576
}
7677

7778
const (
@@ -81,10 +82,6 @@ const (
8182
// json2xml converts test2json's output into an xml output readable by Bazel.
8283
// http://windyroad.com.au/dl/Open%20Source/JUnit.xsd
8384
func json2xml(r io.Reader, pkgName string) ([]byte, error) {
84-
var (
85-
pkgDuration *float64
86-
pkgTimestamp *time.Time
87-
)
8885
testcases := make(map[string]*testCase)
8986
testCaseByName := func(name string) *testCase {
9087
if name == "" {
@@ -105,24 +102,16 @@ func json2xml(r io.Reader, pkgName string) ([]byte, error) {
105102
} else if err != nil {
106103
return nil, fmt.Errorf("error decoding test2json output: %s", err)
107104
}
108-
if pkgTimestamp == nil || (e.Time != nil && e.Time.Unix() < pkgTimestamp.Unix()) {
109-
pkgTimestamp = e.Time
110-
}
111105
switch s := e.Action; s {
112106
case "run":
113107
if c := testCaseByName(e.Test); c != nil {
114108
c.state = s
109+
c.timestamp = e.Time
115110
}
116111
case "output":
117112
trimmedOutput := strings.TrimSpace(e.Output)
118113
if strings.HasPrefix(trimmedOutput, timeoutPanicPrefix) {
119114
inTimeoutSection = true
120-
// the final "fail" action with "Elapsed" may not appear. If not, using the timeout setting as
121-
// pkgDuration; if it appears, it will override pkgDuration.
122-
if duration, err := time.ParseDuration(strings.TrimSpace(trimmedOutput[len(timeoutPanicPrefix):])); err == nil {
123-
seconds := duration.Seconds()
124-
pkgDuration = &seconds
125-
}
126115
continue
127116
}
128117
if inTimeoutSection && strings.HasPrefix(trimmedOutput, "running tests:") {
@@ -159,39 +148,48 @@ func json2xml(r io.Reader, pkgName string) ([]byte, error) {
159148
if c := testCaseByName(e.Test); c != nil {
160149
c.state = s
161150
c.duration = e.Elapsed
162-
} else {
163-
pkgDuration = e.Elapsed
164151
}
165152
case "pass":
166153
if c := testCaseByName(e.Test); c != nil {
167154
c.duration = e.Elapsed
168155
c.state = s
169-
} else {
170-
pkgDuration = e.Elapsed
171156
}
172157
}
173158
}
174159

175-
return xml.MarshalIndent(toXML(pkgName, pkgDuration, pkgTimestamp, testcases), "", "\t")
160+
return xml.MarshalIndent(toXML(pkgName, testcases), "", "\t")
176161
}
177162

178-
func toXML(pkgName string, pkgDuration *float64, pkgTimestamp *time.Time, testcases map[string]*testCase) *xmlTestSuites {
163+
func toXML(pkgName string, testcases map[string]*testCase) *xmlTestSuites {
179164
cases := make([]string, 0, len(testcases))
180165
for k := range testcases {
181166
cases = append(cases, k)
182167
}
183168
sort.Strings(cases)
184-
suite := xmlTestSuite{
185-
Name: pkgName,
186-
}
187-
if pkgDuration != nil {
188-
suite.Time = fmt.Sprintf("%.3f", *pkgDuration)
189-
}
190-
if pkgTimestamp != nil {
191-
suite.Timestamp = pkgTimestamp.Format("2006-01-02T15:04:05.000Z")
192-
}
169+
170+
suiteByName := make(map[string]*xmlTestSuite)
171+
var suiteNames []string
172+
193173
for _, name := range cases {
174+
suiteName := strings.SplitN(name, "/", 2)[0]
175+
var suite *xmlTestSuite
176+
suite, ok := suiteByName[suiteName]
177+
if !ok {
178+
suite = &xmlTestSuite{
179+
Name: pkgName + "." + suiteName,
180+
}
181+
suiteByName[suiteName] = suite
182+
suiteNames = append(suiteNames, suiteName)
183+
}
194184
c := testcases[name]
185+
if name == suiteName {
186+
if c.duration != nil {
187+
suite.Time = fmt.Sprintf("%.3f", *c.duration)
188+
}
189+
if c.timestamp != nil {
190+
suite.Timestamp = c.timestamp.Format("2006-01-02T15:04:05.000Z")
191+
}
192+
}
195193
suite.Tests++
196194
newCase := xmlTestCase{
197195
Name: name,
@@ -230,5 +228,10 @@ func toXML(pkgName string, pkgDuration *float64, pkgTimestamp *time.Time, testca
230228
}
231229
suite.TestCases = append(suite.TestCases, newCase)
232230
}
233-
return &xmlTestSuites{Suites: []xmlTestSuite{suite}}
231+
var suites xmlTestSuites
232+
// because test cases are sorted by name, the suite names are also sorted.
233+
for _, name := range suiteNames {
234+
suites.Suites = append(suites.Suites, *suiteByName[name])
235+
}
236+
return &suites
234237
}

tests/core/go_test/xmlreport_test.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,28 +104,65 @@ func Test(t *testing.T) {
104104
args: []string{"test", "//:xml_test"},
105105
expected: xmlTestSuites{
106106
XMLName: xml.Name{Local: "testsuites"},
107-
Suites: []xmlTestSuite{{
108-
XMLName: xml.Name{Local: "testsuite"},
109-
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test",
110-
Errors: 0,
111-
Failures: 3,
112-
Tests: 3,
113-
}},
107+
Suites: []xmlTestSuite{
108+
{
109+
XMLName: xml.Name{Local: "testsuite"},
110+
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test.TestFail",
111+
Errors: 0,
112+
Failures: 1,
113+
Skipped: 0,
114+
Tests: 1,
115+
},
116+
{
117+
XMLName: xml.Name{Local: "testsuite"},
118+
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test.TestSubtests",
119+
Errors: 0,
120+
Failures: 2,
121+
Skipped: 0,
122+
Tests: 2,
123+
},
124+
},
114125
},
115126
},
116127
{
117128
name: "verbose",
118129
args: []string{"test", "--test_env=GO_TEST_WRAP_TESTV=1", "//:xml_test"},
119130
expected: xmlTestSuites{
120131
XMLName: xml.Name{Local: "testsuites"},
121-
Suites: []xmlTestSuite{{
122-
XMLName: xml.Name{Local: "testsuite"},
123-
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test",
124-
Errors: 0,
125-
Failures: 3,
126-
Skipped: 1,
127-
Tests: 7,
128-
}},
132+
Suites: []xmlTestSuite{
133+
{
134+
XMLName: xml.Name{Local: "testsuite"},
135+
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test.TestFail",
136+
Errors: 0,
137+
Failures: 1,
138+
Skipped: 0,
139+
Tests: 1,
140+
},
141+
{
142+
XMLName: xml.Name{Local: "testsuite"},
143+
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test.TestPass",
144+
Errors: 0,
145+
Failures: 0,
146+
Skipped: 0,
147+
Tests: 1,
148+
},
149+
{
150+
XMLName: xml.Name{Local: "testsuite"},
151+
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test.TestPassLog",
152+
Errors: 0,
153+
Failures: 0,
154+
Skipped: 0,
155+
Tests: 1,
156+
},
157+
{
158+
XMLName: xml.Name{Local: "testsuite"},
159+
Name: "github.com/bazelbuild/rules_go/tests/core/go_test/xml_test.TestSubtests",
160+
Errors: 0,
161+
Failures: 2,
162+
Skipped: 1,
163+
Tests: 4,
164+
},
165+
},
129166
},
130167
},
131168
}

0 commit comments

Comments
 (0)