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()) +}