88package util
99
1010import (
11- "cmp"
1211 "encoding/xml"
1312 "fmt"
1413 "io"
@@ -30,8 +29,13 @@ type TestSuites struct {
3029type testSuite struct {
3130 XMLName xml.Name `xml:"testsuite"`
3231 TestCases []* testCase `xml:"testcase"`
33- Attrs []xml.Attr `xml:",any,attr"`
32+ Errors int `xml:"errors,attr"`
33+ Failures int `xml:"failures,attr"`
34+ Skipped int `xml:"skipped,attr"`
35+ Tests int `xml:"tests,attr"`
36+ Time float64 `xml:"time,attr"`
3437 Name string `xml:"name,attr"`
38+ Timestamp string `xml:"timestamp,attr"`
3539}
3640
3741type testCase struct {
@@ -41,11 +45,12 @@ type testCase struct {
4145 // this isn't Java so there isn't a classname) and excluding it causes
4246 // the TeamCity UI to display the same data in a slightly more coherent
4347 // and usable way.
44- Name string `xml:"name,attr"`
45- Time string `xml:"time,attr"`
46- Failure * XMLMessage `xml:"failure,omitempty"`
47- Error * XMLMessage `xml:"error,omitempty"`
48- Skipped * XMLMessage `xml:"skipped,omitempty"`
48+ Classname string `xml:"classname,attr"`
49+ Name string `xml:"name,attr"`
50+ Time string `xml:"time,attr"`
51+ Failure * XMLMessage `xml:"failure,omitempty"`
52+ Error * XMLMessage `xml:"error,omitempty"`
53+ Skipped * XMLMessage `xml:"skipped,omitempty"`
4954}
5055
5156// XMLMessage is a catch-all structure containing details about a test
@@ -122,7 +127,6 @@ func OutputsOfGenrule(target, xmlQueryOutput string) ([]string, error) {
122127// it to the output file. TeamCity kind of knows how to interpret the schema,
123128// but the schema isn't *exactly* what it's expecting. By munging the XML's
124129// here we ensure that the TC test view is as useful as possible.
125- // Helper function meant to be used with maybeStageArtifact.
126130func MungeTestXML (srcContent []byte , outFile io.Writer ) error {
127131 // Parse the XML into a TestSuites struct.
128132 suites := TestSuites {}
@@ -135,59 +139,102 @@ func MungeTestXML(srcContent []byte, outFile io.Writer) error {
135139 if err != nil {
136140 return err
137141 }
138- // If test.xml is empty, this will be an empty object. We do want to
139- // emit something, however.
140- if len (suites .Suites ) == 0 {
141- return writeToFile (& testSuite {}, outFile )
142+ var res testSuite
143+ for _ , suite := range suites .Suites {
144+ res .XMLName = suite .XMLName
145+ res .TestCases = append (res .TestCases , suite .TestCases ... )
146+ if res .Name == "" {
147+ i := strings .LastIndexByte (suite .Name , '.' )
148+ if i < 0 {
149+ i = len (suite .Name )
150+ }
151+ res .Name = suite .Name [0 :i ]
152+ }
153+ res .Errors += suite .Errors
154+ res .Failures += suite .Failures
155+ res .Skipped += suite .Skipped
156+ res .Tests += suite .Tests
157+ res .Time += suite .Time
158+ if res .Timestamp == "" {
159+ res .Timestamp = suite .Timestamp
160+ } else {
161+ res .Timestamp = min (res .Timestamp , suite .Timestamp )
162+ }
142163 }
143- // We only want the first test suite in the list of suites.
144- return writeToFile (& suites .Suites [0 ], outFile )
164+ return writeToFile (res , outFile )
145165}
146166
147- // MergeTestXMLs merges the given list of test suites into a single test suite,
148- // then writes the serialized XML to the given outFile. The prefix is passed
149- // to xml.Unmarshal. Note that this function might modify the passed-in
150- // TestSuites in-place.
167+ // MergeTestXMLs merges the given list of test suites into a single object,
168+ // then writes the serialized XML to the given outFile.
151169func MergeTestXMLs (suitesToMerge []TestSuites , outFile io.Writer ) error {
152170 if len (suitesToMerge ) == 0 {
153171 return fmt .Errorf ("expected at least one test suite" )
154172 }
155- var resultSuites TestSuites
156- resultSuites .Suites = append (resultSuites .Suites , testSuite {})
157- resultSuite := & resultSuites .Suites [0 ]
158- resultSuite .Name = suitesToMerge [0 ].Suites [0 ].Name
159- resultSuite .Attrs = suitesToMerge [0 ].Suites [0 ].Attrs
160- cases := make (map [string ]* testCase )
173+ type suiteAndCaseMap struct {
174+ suite testSuite
175+ cases map [string ]* testCase
176+ }
177+ suitesMap := make (map [string ]* suiteAndCaseMap )
161178 for _ , suites := range suitesToMerge {
162- for _ , testCase := range suites .Suites [ 0 ]. TestCases {
163- oldCase , ok := cases [ testCase .Name ]
179+ for _ , suite := range suites .Suites {
180+ oldSuite , ok := suitesMap [ suite .Name ]
164181 if ! ok {
165- cases [testCase .Name ] = testCase
182+ cases := make (map [string ]* testCase )
183+ for _ , testCase := range suite .TestCases {
184+ cases [testCase .Name ] = testCase
185+ }
186+ suitesMap [suite .Name ] = & suiteAndCaseMap {
187+ suite : suite ,
188+ cases : cases ,
189+ }
166190 continue
167191 }
168- if testCase . Failure != nil {
169- if oldCase . Failure == nil {
170- oldCase . Failure = testCase . Failure
171- } else {
172- oldCase . Failure . Contents = oldCase . Failure . Contents + " \n " + testCase . Failure . Contents
192+ for _ , testCase := range suite . TestCases {
193+ oldCase , ok := oldSuite . cases [ testCase . Name ]
194+ if ! ok {
195+ oldSuite . cases [ testCase . Name ] = testCase
196+ continue
173197 }
174- }
175- if testCase .Error != nil {
176- if oldCase .Error == nil {
177- oldCase .Error = testCase .Error
178- } else {
179- oldCase .Error .Contents = oldCase .Error .Contents + "\n " + testCase .Error .Contents
198+ if testCase .Failure != nil {
199+ if oldCase .Failure == nil {
200+ oldCase .Failure = testCase .Failure
201+ oldSuite .suite .Failures += 1
202+ } else {
203+ oldCase .Failure .Contents = oldCase .Failure .Contents + "\n " + testCase .Failure .Contents
204+ }
205+ }
206+ if testCase .Error != nil {
207+ if oldCase .Error == nil {
208+ oldCase .Error = testCase .Error
209+ oldSuite .suite .Errors += 1
210+ } else {
211+ oldCase .Error .Contents = oldCase .Error .Contents + "\n " + testCase .Error .Contents
212+ }
180213 }
181214 }
182215 }
183216 }
184- for _ , testCase := range cases {
185- resultSuite .TestCases = append (resultSuite .TestCases , testCase )
217+ suitesSlice := make ([]string , 0 , len (suitesMap ))
218+ for suiteName := range suitesMap {
219+ suitesSlice = append (suitesSlice , suiteName )
220+ }
221+ slices .Sort (suitesSlice )
222+ var res TestSuites
223+ for _ , suiteName := range suitesSlice {
224+ suiteAndCases := suitesMap [suiteName ]
225+ suite := suiteAndCases .suite
226+ suite .TestCases = []* testCase {}
227+ casesSlice := make ([]string , 0 , len (suiteAndCases .cases ))
228+ for caseName := range suiteAndCases .cases {
229+ casesSlice = append (casesSlice , caseName )
230+ }
231+ slices .Sort (casesSlice )
232+ for _ , caseName := range casesSlice {
233+ suite .TestCases = append (suite .TestCases , suiteAndCases .cases [caseName ])
234+ }
235+ res .Suites = append (res .Suites , suite )
186236 }
187- slices .SortFunc (resultSuite .TestCases , func (a , b * testCase ) int {
188- return cmp .Compare (a .Name , b .Name )
189- })
190- return writeToFile (& resultSuites , outFile )
237+ return writeToFile (& res , outFile )
191238}
192239
193240func writeToFile (suite interface {}, outFile io.Writer ) error {
0 commit comments