github.com/saucelabs/saucectl@v0.175.1/internal/junit/junit.go (about) 1 package junit 2 3 import ( 4 "encoding/xml" 5 "fmt" 6 7 "golang.org/x/exp/maps" 8 ) 9 10 // FileName is the name of the JUnit report. 11 const FileName = "junit.xml" 12 13 // Property maps to a <property> element that's part of <properties>. 14 type Property struct { 15 Name string `xml:"name,attr"` 16 Value string `xml:"value,attr"` 17 } 18 19 // TestCase maps to <testcase> element 20 type TestCase struct { 21 Name string `xml:"name,attr"` 22 // Assertions is the number of assertions in the test case. 23 Assertions string `xml:"assertions,attr,omitempty"` 24 // Time in seconds it took to run the test case. 25 Time string `xml:"time,attr"` 26 // Timestamp as specified by ISO 8601 (2014-01-21T16:17:18). 27 // Timezone is optional. 28 Timestamp string `xml:"timestamp,attr"` 29 ClassName string `xml:"classname,attr"` 30 // Status indicates success or failure of the test. May be used instead of 31 // Error, Failure or Skipped or in addition to them. 32 Status string `xml:"status,attr,omitempty"` 33 File string `xml:"file,attr,omitempty"` 34 SystemErr string `xml:"system-err,omitempty"` 35 SystemOut string `xml:"system-out,omitempty"` 36 Error *Error `xml:"error,omitempty"` 37 Failure *Failure `xml:"failure,omitempty"` 38 Skipped *Skipped `xml:"skipped,omitempty"` 39 } 40 41 // IsError returns true if the test case errored. Multiple fields are taken 42 // into account to determine this. 43 func (tc TestCase) IsError() bool { 44 return tc.Error != nil || tc.Status == "error" 45 } 46 47 // IsFailure returns true if the test case failed. Multiple fields are taken 48 // into account to determine this. 49 func (tc TestCase) IsFailure() bool { 50 return tc.Failure != nil || tc.Status == "failure" || tc.Status == "failed" 51 } 52 53 // IsSkipped returns true if the test case was skipped. Multiple fields are 54 // taken into account to determine this. 55 func (tc TestCase) IsSkipped() bool { 56 return tc.Skipped != nil || tc.Status == "skipped" 57 } 58 59 // Failure maps to either a <failure> or <error> element. It usually indicates 60 // assertion failures. Depending on the framework, this may also indicate an 61 // unexpected error, much like Error does. Some frameworks use Error or the 62 // 'status' attribute on TestCase instead. 63 type Failure struct { 64 // Message is a short description of the failure. 65 Message string `xml:"message,attr"` 66 // Type is the type of failure, e.g. "java.lang.AssertionError". 67 Type string `xml:"type,attr"` 68 // Text is a failure description or stack trace. 69 Text string `xml:",chardata"` 70 } 71 72 // Error maps to <error> element. It usually indicates unexpected errors. 73 // Some frameworks use Failure or the 'status' attribute on TestCase instead. 74 type Error struct { 75 // Message is a short description of the error. 76 Message string `xml:"message,attr"` 77 // Type is the type of error, e.g. "java.lang.NullPointerException". 78 Type string `xml:"type,attr"` 79 // Text is an error description or stack trace. 80 Text string `xml:",chardata"` 81 } 82 83 // Skipped maps to <skipped> element. Indicates a skipped test. Some frameworks 84 // use the 'status' attribute on TestCase instead. 85 type Skipped struct { 86 // Message is a short description that explains why the test was skipped. 87 Message string `xml:"message,attr"` 88 } 89 90 // TestSuite maps to <testsuite> element 91 type TestSuite struct { 92 Name string `xml:"name,attr"` 93 Tests int `xml:"tests,attr"` 94 Properties []Property `xml:"properties>property"` 95 Errors int `xml:"errors,attr,omitempty"` 96 Failures int `xml:"failures,attr,omitempty"` 97 // Disabled is the number of disabled or skipped tests. Some frameworks use Skipped instead. 98 Disabled int `xml:"disabled,attr,omitempty"` 99 // Skipped is the number of skipped or disabled tests. Some frameworks use Disabled instead. 100 Skipped int `xml:"skipped,attr,omitempty"` 101 // Time in seconds it took to run the test suite. 102 Time string `xml:"time,attr,omitempty"` 103 // Timestamp as specified by ISO 8601 (2014-01-21T16:17:18). Timezone may not be specified. 104 Timestamp string `xml:"timestamp,attr,omitempty"` 105 Package string `xml:"package,attr,omitempty"` 106 File string `xml:"file,attr,omitempty"` 107 TestCases []TestCase `xml:"testcase"` 108 SystemErr string `xml:"system-err,omitempty"` 109 SystemOut string `xml:"system-out,omitempty"` 110 } 111 112 // AddTestCases adds test cases to the test suite. If unique is true, existing 113 // test cases with the same name will be replaced. 114 func (ts *TestSuite) AddTestCases(unique bool, tcs ...TestCase) { 115 if !unique { 116 ts.TestCases = append(ts.TestCases, tcs...) 117 return 118 } 119 120 // index existing test cases by name 121 testMap := make(map[string]TestCase) 122 for _, tc := range ts.TestCases { 123 key := fmt.Sprintf("%s.%s", tc.ClassName, tc.Name) 124 testMap[key] = tc 125 } 126 127 // add new test cases 128 for _, tc := range tcs { 129 key := fmt.Sprintf("%s.%s", tc.ClassName, tc.Name) 130 testMap[key] = tc 131 } 132 133 // convert map back to slice 134 ts.TestCases = maps.Values(testMap) 135 } 136 137 // Compute updates some statistics for the test suite based on the test cases it 138 // contains. 139 // 140 // Updates the following fields: 141 // - Tests 142 // - Errors 143 // - Failures 144 // - Skipped/Disabled 145 func (ts *TestSuite) Compute() { 146 ts.Tests = len(ts.TestCases) 147 ts.Errors = 0 148 ts.Failures = 0 149 ts.Disabled = 0 150 ts.Skipped = 0 151 152 for _, tc := range ts.TestCases { 153 if tc.IsError() { 154 ts.Errors++ 155 } else if tc.IsFailure() { 156 ts.Failures++ 157 } else if tc.IsSkipped() { 158 ts.Skipped++ 159 } 160 // we favor skipped over disabled, so ignore disabled 161 } 162 } 163 164 // TestSuites maps to root junit <testsuites> element 165 type TestSuites struct { 166 XMLName xml.Name `xml:"testsuites"` 167 TestSuites []TestSuite `xml:"testsuite"` 168 Name string `xml:"name,attr,omitempty"` 169 // Time in seconds it took to run all the test suites. 170 Time string `xml:"time,attr,omitempty"` 171 Tests int `xml:"tests,attr,omitempty"` 172 // Disabled is the number of disabled or skipped tests. Some frameworks use Skipped instead. 173 Disabled int `xml:"disabled,attr,omitempty"` 174 // Skipped is the number of skipped or disabled tests. Some frameworks use Disabled instead. 175 Skipped int `xml:"skipped,attr,omitempty"` 176 Failures int `xml:"failures,attr,omitempty"` 177 Errors int `xml:"errors,attr,omitempty"` 178 } 179 180 // Compute updates _some_ statistics for the entire report based on the test 181 // cases it contains. This is an expensive and destructive operation. 182 // Use judiciously. 183 // 184 // Updates the following fields: 185 // - Tests 186 // - Errors 187 // - Failures 188 // - Skipped/Disabled 189 func (ts *TestSuites) Compute() { 190 ts.Tests = 0 191 ts.Errors = 0 192 ts.Failures = 0 193 ts.Disabled = 0 194 ts.Skipped = 0 195 196 for i := range ts.TestSuites { 197 suite := &ts.TestSuites[i] 198 suite.Compute() 199 200 ts.Tests += suite.Tests 201 ts.Errors += suite.Errors 202 ts.Failures += suite.Failures 203 ts.Disabled += suite.Disabled 204 ts.Skipped += suite.Skipped 205 } 206 } 207 208 // TestCases returns all test cases from all test suites. 209 func (ts *TestSuites) TestCases() []TestCase { 210 var tcs []TestCase 211 for _, ts := range ts.TestSuites { 212 tcs = append(tcs, ts.TestCases...) 213 } 214 215 return tcs 216 } 217 218 // Parse a junit report from an XML encoded byte string. The root <testsuites> 219 // element is optional if there's only one <testsuite> element. In that case, 220 // Parse will parse the <testsuite> and wrap it in a TestSuites struct. 221 func Parse(data []byte) (TestSuites, error) { 222 var tss TestSuites 223 err := xml.Unmarshal(data, &tss) 224 if err != nil { 225 // root <testsuites> is optional 226 // parse a <testsuite> element instead 227 var ts TestSuite 228 if err = xml.Unmarshal(data, &ts); err != nil { 229 return tss, err 230 } 231 232 tss.TestSuites = []TestSuite{ 233 ts, 234 } 235 } 236 237 return tss, err 238 } 239 240 // MergeReports merges multiple junit reports into a single report. 241 func MergeReports(reports ...TestSuites) TestSuites { 242 suites := make(map[string]TestSuite) 243 244 for _, rep := range reports { 245 for _, suite := range rep.TestSuites { 246 indexedSuite, ok := suites[suite.Name] 247 if !ok { 248 suites[suite.Name] = suite 249 continue 250 } 251 252 indexedSuite.AddTestCases(true, suite.TestCases...) 253 suites[suite.Name] = indexedSuite 254 } 255 } 256 257 return TestSuites{ 258 TestSuites: maps.Values(suites), 259 } 260 }