github.com/tiagovtristao/plz@v13.4.0+incompatible/src/core/test_results.go (about) 1 package core 2 3 import ( 4 "bytes" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/thought-machine/please/src/fs" 10 ) 11 12 // TestSuites describes a collection of test results for a set of targets. 13 type TestSuites struct { 14 TestSuites []TestSuite // The test results for each separate target 15 } 16 17 // TestSuite describes all the test results for a target. 18 type TestSuite struct { 19 Package string // The package name of the test suite (usually the first part of the target label). 20 Name string // The name of the test suite (usually the last part of the target label). 21 Cached bool // True if the test results were retrieved from cache. 22 Duration time.Duration // The length of time it took to run this target (may be different from the sum of times of test cases). 23 TimedOut bool // True if the test failed because we timed it out. 24 TestCases TestCases // The test cases that ran during execution of this target. 25 Properties map[string]string // The system properties at the time of the test. 26 Timestamp string // ISO8601 formatted datetime when the test ran. 27 } 28 29 // JavaStyleName pretends we are using a language that has package names and classnames etc. 30 func (testSuite TestSuite) JavaStyleName() string { 31 return fmt.Sprintf("%s.%s", testSuite.Package, testSuite.Name) 32 } 33 34 // Collapse adds the results of one test suite to the current one. 35 func (testSuite *TestSuite) Collapse(incoming TestSuite) { 36 testSuite.TestCases = append(testSuite.TestCases, incoming.TestCases...) 37 testSuite.Duration += incoming.Duration 38 testSuite.TimedOut = testSuite.TimedOut || incoming.TimedOut 39 if testSuite.Properties == nil { 40 testSuite.Properties = make(map[string]string) 41 } 42 testSuite.Properties = addAll(testSuite.Properties, incoming.Properties) 43 } 44 45 func addAll(map1 map[string]string, map2 map[string]string) map[string]string { 46 for k, v := range map2 { 47 map1[k] = v 48 } 49 return map1 50 } 51 52 // Tests returns the number of TestCases. 53 func (testSuite *TestSuite) Tests() int { 54 return len(testSuite.TestCases) 55 } 56 57 // FlakyPasses returns the number of TestCases which succeeded after some number of executions. 58 func (testSuite TestSuite) FlakyPasses() int { 59 flakyPasses := 0 60 61 for _, result := range testSuite.TestCases { 62 if result.Success() != nil && len(result.Executions) > 1 { 63 flakyPasses++ 64 } 65 } 66 67 return flakyPasses 68 } 69 70 // Passes returns the number of TestCases which succeeded (not skipped). 71 func (testSuite TestSuite) Passes() int { 72 passes := 0 73 74 for _, result := range testSuite.TestCases { 75 if len(result.Failures()) == 0 && len(result.Errors()) == 0 && result.Skip() == nil { 76 passes++ 77 } 78 } 79 80 return passes 81 } 82 83 // Errors returns the number of TestCases which did not succeed and returned some abnormal error. 84 func (testSuite *TestSuite) Errors() int { 85 errors := 0 86 87 for _, result := range testSuite.TestCases { 88 // No success result, not skipped, some errors (don't care about the presence of failures) 89 if result.Success() == nil && result.Skip() == nil && len(result.Errors()) > 0 { 90 errors++ 91 } 92 } 93 94 return errors 95 } 96 97 // Failures returns the number of TestCases which did not succeed and returned some failure. 98 func (testSuite *TestSuite) Failures() int { 99 failures := 0 100 101 for _, result := range testSuite.TestCases { 102 // No success result, not skipped, no errors, but some failures. 103 if result.Success() == nil && result.Skip() == nil && len(result.Errors()) == 0 && len(result.Failures()) > 0 { 104 failures++ 105 } 106 } 107 108 return failures 109 } 110 111 // Skips returns the number of TestCases that were skipped. 112 func (testSuite *TestSuite) Skips() int { 113 skips := 0 114 115 for _, result := range testSuite.TestCases { 116 if result.Skip() != nil { 117 skips++ 118 } 119 } 120 121 return skips 122 } 123 124 // Add puts test cases together if they have the same name and classname, allowing callers to treat 125 // multiple test cases as if they were merely multiple executions of the same test. 126 func (testSuite *TestSuite) Add(cases ...TestCase) { 127 for _, testCase := range cases { 128 idx := findMatchingTestCase(&testCase, &testSuite.TestCases) 129 130 if idx >= 0 { 131 testSuite.TestCases[idx].Executions = append(testSuite.TestCases[idx].Executions, testCase.Executions...) 132 } else { 133 testSuite.TestCases = append(testSuite.TestCases, testCase) 134 } 135 } 136 } 137 138 func findMatchingTestCase(testCase *TestCase, testCases *TestCases) int { 139 for idx := range *testCases { 140 originalTestCase := (*testCases)[idx] 141 if originalTestCase.Name == testCase.Name && originalTestCase.ClassName == testCase.ClassName { 142 return idx 143 } 144 } 145 return -1 146 } 147 148 // TestCase describes a set of test results for a test method. 149 type TestCase struct { 150 ClassName string // ClassName of test (optional, for languages that don't have classes) 151 Name string // Name of test 152 Executions []TestExecution // The results of executing the test, possibly multiple times 153 } 154 155 // Success returns either the successful execution of a test case, or nil if it was never successfully executed. 156 func (testCase *TestCase) Success() *TestExecution { 157 for _, execution := range testCase.Executions { 158 if execution.Failure == nil && 159 execution.Error == nil && 160 execution.Skip == nil { 161 return &execution 162 } 163 } 164 return nil 165 } 166 167 // Skip returns the either the skipped execution of a test case, or nil if it was never skipped. 168 func (testCase *TestCase) Skip() *TestExecution { 169 for _, execution := range testCase.Executions { 170 if execution.Skip != nil { 171 return &execution 172 } 173 } 174 return nil 175 } 176 177 // Failures returns all failing executions of a test case. 178 func (testCase *TestCase) Failures() []TestExecution { 179 failures := make([]TestExecution, 0) 180 for _, execution := range testCase.Executions { 181 if execution.Failure != nil { 182 failures = append(failures, execution) 183 } 184 } 185 return failures 186 } 187 188 // Errors returns all abnormal executions of a test case. 189 func (testCase *TestCase) Errors() []TestExecution { 190 errors := make([]TestExecution, 0) 191 for _, execution := range testCase.Executions { 192 if execution.Error != nil { 193 errors = append(errors, execution) 194 } 195 } 196 return errors 197 } 198 199 // Duration calculates how long the test case took to run to success or failure (or nil if skipped or abnormal exit). 200 func (testCase *TestCase) Duration() *time.Duration { 201 if testCase.Success() != nil { 202 return testCase.Success().Duration 203 } else if failures := testCase.Failures(); len(failures) > 0 { 204 return failures[0].Duration 205 } 206 // Unable to determine duration of this test case. 207 return nil 208 } 209 210 // TestCases is named so we can add a method to it. 211 type TestCases []TestCase 212 213 // AllSucceeded checks that every test case either passed or was skipped. 214 func (testCases TestCases) AllSucceeded() bool { 215 for _, testCase := range testCases { 216 if testCase.Success() == nil && testCase.Skip() == nil { 217 return false 218 } 219 } 220 return true 221 } 222 223 // TestExecution represents one execution of a test method. The absence of a Failure, Error or Skip implies the test 224 // executed successfully. 225 type TestExecution struct { 226 Failure *TestResultFailure // The failure, if any, running the test (usually an assertion that failed) 227 Error *TestResultFailure // The error, if any, running the test (usually some other abnormal exit) 228 Skip *TestResultSkip // The reason for skipping the test, if it was skipped 229 Stdout string // Standard output during test 230 Stderr string // Standard error during test 231 Duration *time.Duration // How long the test took (if it did not fail abnormally) 232 } 233 234 // TestResultFailure stores the information related to the failure - stack trace, exception type etc. 235 type TestResultFailure struct { 236 Type string // The type of error (e.g. "AssertionError") 237 Message string // The reason for error (e.g. "1 != 2") 238 Traceback string // The trace of the error (if known) 239 } 240 241 // TestResultSkip stores the reason for skipping a test. 242 type TestResultSkip struct { 243 Message string // The reason for skipping the test 244 } 245 246 // A LineCoverage represents a single line of coverage, which can be in one of several states. 247 // Note that Please doesn't support sub-line coverage at present. 248 type LineCoverage uint8 249 250 // Constants representing the states that a single line can be in for coverage. 251 const ( 252 NotExecutable LineCoverage = iota // Line isn't executable (eg. comment, blank) 253 Unreachable LineCoverage = iota // Line is executable but we've determined it can't be reached. So far not used. 254 Uncovered LineCoverage = iota // Line is executable but isn't covered. 255 Covered LineCoverage = iota // Line is executable and covered. 256 ) 257 258 var lineCoverageOutput = [...]rune{'N', 'X', 'U', 'C'} // Corresponds to ordering of enum. 259 260 // TestCoverage implements a pretty simple coverage format; we record one int for each line 261 // stating what its coverage is. 262 type TestCoverage struct { 263 Tests map[BuildLabel]map[string][]LineCoverage 264 Files map[string][]LineCoverage 265 } 266 267 // Aggregate aggregates results from that coverage object into this one. 268 func (coverage *TestCoverage) Aggregate(cov *TestCoverage) { 269 if coverage.Tests == nil { 270 coverage.Tests = map[BuildLabel]map[string][]LineCoverage{} 271 } 272 if coverage.Files == nil { 273 coverage.Files = map[string][]LineCoverage{} 274 } 275 276 // Assume that tests are independent (will currently always be the case). 277 for label, c := range cov.Tests { 278 coverage.Tests[label] = c 279 } 280 // Files are more complex since multiple tests can cover the same file. 281 // We take the best result for each line from each test. 282 for filename, c := range cov.Files { 283 coverage.Files[filename] = MergeCoverageLines(coverage.Files[filename], c) 284 } 285 } 286 287 // MergeCoverageLines merges two sets of coverage results together, taking 288 // the superset of the two results. 289 func MergeCoverageLines(existing, coverage []LineCoverage) []LineCoverage { 290 ret := make([]LineCoverage, len(existing)) 291 copy(ret, existing) 292 for i, line := range coverage { 293 if i >= len(ret) { 294 ret = append(ret, line) 295 } else if coverage[i] > ret[i] { 296 ret[i] = coverage[i] 297 } 298 } 299 return ret 300 } 301 302 // OrderedFiles returns an ordered slice of all the files we have coverage information for. 303 // Note that files are ordered non-trivially such that each directory remains together. 304 func (coverage *TestCoverage) OrderedFiles() []string { 305 files := make([]string, 0, len(coverage.Files)) 306 for file := range coverage.Files { 307 if strings.HasPrefix(file, RepoRoot) { 308 file = strings.TrimLeft(file[len(RepoRoot):], "/") 309 } 310 files = append(files, file) 311 } 312 fs.SortPaths(files) 313 return files 314 } 315 316 // NewTestCoverage constructs and returns a new TestCoverage instance. 317 func NewTestCoverage() TestCoverage { 318 return TestCoverage{ 319 Tests: map[BuildLabel]map[string][]LineCoverage{}, 320 Files: map[string][]LineCoverage{}, 321 } 322 } 323 324 // TestCoverageString produces a string representation of coverage for serialising to file so we don't 325 // expose the internal enum values (ordering is important so we may want to insert 326 // new ones later). This format happens to be the same as the one Phabricator uses, 327 // which is mildly useful to us since we want to integrate with it anyway. See 328 // https://secure.phabricator.com/book/phabricator/article/arcanist_coverage/ 329 // for more detail of how it works. 330 func TestCoverageString(lines []LineCoverage) string { 331 var buffer bytes.Buffer 332 for _, line := range lines { 333 buffer.WriteRune(lineCoverageOutput[line]) 334 } 335 return buffer.String() 336 }