diff --git a/pkg/build/util/util.go b/pkg/build/util/util.go
index 7f4ca0ff315d..53a18829b619 100644
--- a/pkg/build/util/util.go
+++ b/pkg/build/util/util.go
@@ -8,7 +8,6 @@
package util
import (
- "cmp"
"encoding/xml"
"fmt"
"io"
@@ -30,8 +29,13 @@ type TestSuites struct {
type testSuite struct {
XMLName xml.Name `xml:"testsuite"`
TestCases []*testCase `xml:"testcase"`
- Attrs []xml.Attr `xml:",any,attr"`
+ Errors int `xml:"errors,attr"`
+ Failures int `xml:"failures,attr"`
+ Skipped int `xml:"skipped,attr"`
+ Tests int `xml:"tests,attr"`
+ Time float64 `xml:"time,attr"`
Name string `xml:"name,attr"`
+ Timestamp string `xml:"timestamp,attr"`
}
type testCase struct {
@@ -41,11 +45,12 @@ type testCase struct {
// this isn't Java so there isn't a classname) and excluding it causes
// the TeamCity UI to display the same data in a slightly more coherent
// and usable way.
- Name string `xml:"name,attr"`
- Time string `xml:"time,attr"`
- Failure *XMLMessage `xml:"failure,omitempty"`
- Error *XMLMessage `xml:"error,omitempty"`
- Skipped *XMLMessage `xml:"skipped,omitempty"`
+ Classname string `xml:"classname,attr"`
+ Name string `xml:"name,attr"`
+ Time string `xml:"time,attr"`
+ Failure *XMLMessage `xml:"failure,omitempty"`
+ Error *XMLMessage `xml:"error,omitempty"`
+ Skipped *XMLMessage `xml:"skipped,omitempty"`
}
// XMLMessage is a catch-all structure containing details about a test
@@ -122,7 +127,6 @@ func OutputsOfGenrule(target, xmlQueryOutput string) ([]string, error) {
// it to the output file. TeamCity kind of knows how to interpret the schema,
// but the schema isn't *exactly* what it's expecting. By munging the XML's
// here we ensure that the TC test view is as useful as possible.
-// Helper function meant to be used with maybeStageArtifact.
func MungeTestXML(srcContent []byte, outFile io.Writer) error {
// Parse the XML into a TestSuites struct.
suites := TestSuites{}
@@ -135,59 +139,102 @@ func MungeTestXML(srcContent []byte, outFile io.Writer) error {
if err != nil {
return err
}
- // If test.xml is empty, this will be an empty object. We do want to
- // emit something, however.
- if len(suites.Suites) == 0 {
- return writeToFile(&testSuite{}, outFile)
+ var res testSuite
+ for _, suite := range suites.Suites {
+ res.XMLName = suite.XMLName
+ res.TestCases = append(res.TestCases, suite.TestCases...)
+ if res.Name == "" {
+ i := strings.LastIndexByte(suite.Name, '.')
+ if i < 0 {
+ i = len(suite.Name)
+ }
+ res.Name = suite.Name[0:i]
+ }
+ res.Errors += suite.Errors
+ res.Failures += suite.Failures
+ res.Skipped += suite.Skipped
+ res.Tests += suite.Tests
+ res.Time += suite.Time
+ if res.Timestamp == "" {
+ res.Timestamp = suite.Timestamp
+ } else {
+ res.Timestamp = min(res.Timestamp, suite.Timestamp)
+ }
}
- // We only want the first test suite in the list of suites.
- return writeToFile(&suites.Suites[0], outFile)
+ return writeToFile(res, outFile)
}
-// MergeTestXMLs merges the given list of test suites into a single test suite,
-// then writes the serialized XML to the given outFile. The prefix is passed
-// to xml.Unmarshal. Note that this function might modify the passed-in
-// TestSuites in-place.
+// MergeTestXMLs merges the given list of test suites into a single object,
+// then writes the serialized XML to the given outFile.
func MergeTestXMLs(suitesToMerge []TestSuites, outFile io.Writer) error {
if len(suitesToMerge) == 0 {
return fmt.Errorf("expected at least one test suite")
}
- var resultSuites TestSuites
- resultSuites.Suites = append(resultSuites.Suites, testSuite{})
- resultSuite := &resultSuites.Suites[0]
- resultSuite.Name = suitesToMerge[0].Suites[0].Name
- resultSuite.Attrs = suitesToMerge[0].Suites[0].Attrs
- cases := make(map[string]*testCase)
+ type suiteAndCaseMap struct {
+ suite testSuite
+ cases map[string]*testCase
+ }
+ suitesMap := make(map[string]*suiteAndCaseMap)
for _, suites := range suitesToMerge {
- for _, testCase := range suites.Suites[0].TestCases {
- oldCase, ok := cases[testCase.Name]
+ for _, suite := range suites.Suites {
+ oldSuite, ok := suitesMap[suite.Name]
if !ok {
- cases[testCase.Name] = testCase
+ cases := make(map[string]*testCase)
+ for _, testCase := range suite.TestCases {
+ cases[testCase.Name] = testCase
+ }
+ suitesMap[suite.Name] = &suiteAndCaseMap{
+ suite: suite,
+ cases: cases,
+ }
continue
}
- if testCase.Failure != nil {
- if oldCase.Failure == nil {
- oldCase.Failure = testCase.Failure
- } else {
- oldCase.Failure.Contents = oldCase.Failure.Contents + "\n" + testCase.Failure.Contents
+ for _, testCase := range suite.TestCases {
+ oldCase, ok := oldSuite.cases[testCase.Name]
+ if !ok {
+ oldSuite.cases[testCase.Name] = testCase
+ continue
}
- }
- if testCase.Error != nil {
- if oldCase.Error == nil {
- oldCase.Error = testCase.Error
- } else {
- oldCase.Error.Contents = oldCase.Error.Contents + "\n" + testCase.Error.Contents
+ if testCase.Failure != nil {
+ if oldCase.Failure == nil {
+ oldCase.Failure = testCase.Failure
+ oldSuite.suite.Failures += 1
+ } else {
+ oldCase.Failure.Contents = oldCase.Failure.Contents + "\n" + testCase.Failure.Contents
+ }
+ }
+ if testCase.Error != nil {
+ if oldCase.Error == nil {
+ oldCase.Error = testCase.Error
+ oldSuite.suite.Errors += 1
+ } else {
+ oldCase.Error.Contents = oldCase.Error.Contents + "\n" + testCase.Error.Contents
+ }
}
}
}
}
- for _, testCase := range cases {
- resultSuite.TestCases = append(resultSuite.TestCases, testCase)
+ suitesSlice := make([]string, 0, len(suitesMap))
+ for suiteName := range suitesMap {
+ suitesSlice = append(suitesSlice, suiteName)
+ }
+ slices.Sort(suitesSlice)
+ var res TestSuites
+ for _, suiteName := range suitesSlice {
+ suiteAndCases := suitesMap[suiteName]
+ suite := suiteAndCases.suite
+ suite.TestCases = []*testCase{}
+ casesSlice := make([]string, 0, len(suiteAndCases.cases))
+ for caseName := range suiteAndCases.cases {
+ casesSlice = append(casesSlice, caseName)
+ }
+ slices.Sort(casesSlice)
+ for _, caseName := range casesSlice {
+ suite.TestCases = append(suite.TestCases, suiteAndCases.cases[caseName])
+ }
+ res.Suites = append(res.Suites, suite)
}
- slices.SortFunc(resultSuite.TestCases, func(a, b *testCase) int {
- return cmp.Compare(a.Name, b.Name)
- })
- return writeToFile(&resultSuites, outFile)
+ return writeToFile(&res, outFile)
}
func writeToFile(suite interface{}, outFile io.Writer) error {
diff --git a/pkg/build/util/util_test.go b/pkg/build/util/util_test.go
index 0c2c1cee98ec..9388d4bf56e6 100644
--- a/pkg/build/util/util_test.go
+++ b/pkg/build/util/util_test.go
@@ -75,74 +75,80 @@ func TestOutputsOfGenrule(t *testing.T) {
func TestMergeXml(t *testing.T) {
const xml1 = `
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- FAILED :(
+
+
+
+
+
+ FAILED
+
+
+
+
+
+
+
+
+
+
+
+
+
`
const xml2 = `
-
+
-
-
-
-
-
- ALSO FAILED :(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FAILED ALSO
-
-
-
-
-
-
-
-
-
-
-
+
+
+
`
const expected = `
-
-
-
-
-
-
-
- ALSO FAILED :(
+
+
+
+
+
+
+
+ FAILED
-
-
-
-
-
-
-
-
-
-
-
- FAILED :(
+
+
+
+
+
+
+
+
+
+
+ FAILED ALSO
+
+
+
`
@@ -154,3 +160,54 @@ func TestMergeXml(t *testing.T) {
require.NoError(t, MergeTestXMLs([]TestSuites{suite1, suite2}, &buf))
require.Equal(t, expected, buf.String())
}
+
+func TestMungeTestXML(t *testing.T) {
+ beforeXml := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+ expected := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ var buf bytes.Buffer
+ require.NoError(t, MungeTestXML([]byte(beforeXml), &buf))
+ require.Equal(t, expected, buf.String())
+}