github.com/tiagovtristao/plz@v13.4.0+incompatible/src/test/test_step.go (about) 1 package test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path" 10 "strings" 11 "time" 12 13 "github.com/pkg/xattr" 14 "gopkg.in/op/go-logging.v1" 15 16 "github.com/thought-machine/please/src/build" 17 "github.com/thought-machine/please/src/core" 18 "github.com/thought-machine/please/src/metrics" 19 "github.com/thought-machine/please/src/utils" 20 "github.com/thought-machine/please/src/worker" 21 ) 22 23 var log = logging.MustGetLogger("test") 24 25 const dummyOutput = "=== RUN DummyTest\n--- PASS: DummyTest (0.00s)\nPASS\n" 26 const dummyCoverage = "<?xml version=\"1.0\" ?><coverage></coverage>" 27 28 // Tag that we attach for xattrs to store hashes against files. 29 // Note that we are required to provide the user namespace; that seems to be set implicitly 30 // by the attr utility, but that is not done for us here. 31 const xattrName = "user.plz_test" 32 33 // Test runs the tests for a single target. 34 func Test(tid int, state *core.BuildState, label core.BuildLabel) { 35 state.LogBuildResult(tid, label, core.TargetTesting, "Testing...") 36 startTime := time.Now() 37 target := state.Graph.TargetOrDie(label) 38 test(tid, state.ForTarget(target), label, target) 39 metrics.Record(target, time.Since(startTime)) 40 } 41 42 func test(tid int, state *core.BuildState, label core.BuildLabel, target *core.BuildTarget) { 43 hash, err := build.RuntimeHash(state, target) 44 if err != nil { 45 state.LogBuildError(tid, label, core.TargetTestFailed, err, "Failed to calculate target hash") 46 return 47 } 48 // Check the cached output files if the target wasn't rebuilt. 49 hash = core.CollapseHash(hash) 50 cachedOutputFile := target.TestResultsFile() 51 cachedCoverageFile := target.CoverageFile() 52 resultsFileName := path.Base(cachedOutputFile) 53 coverageFileName := path.Base(cachedCoverageFile) 54 outputFile := path.Join(target.TestDir(), "test.results") 55 coverageFile := path.Join(target.TestDir(), "test.coverage") 56 needCoverage := state.NeedCoverage && !target.NoTestOutput && !target.HasAnyLabel(state.Config.Test.DisableCoverage) 57 58 // If the user passed --shell then just prepare the directory. 59 if state.PrepareShell { 60 if err := prepareTestDir(state.Graph, target); err != nil { 61 state.LogBuildError(tid, label, core.TargetTestFailed, err, "Failed to prepare test directory") 62 } else { 63 target.SetState(core.Stopped) 64 state.LogBuildResult(tid, label, core.TargetTestStopped, "Test stopped") 65 } 66 return 67 } 68 69 cachedTestResults := func() *core.TestSuite { 70 log.Debug("Not re-running test %s; got cached results.", label) 71 coverage := parseCoverageFile(target, cachedCoverageFile) 72 results, err := parseTestResults(cachedOutputFile) 73 results.Package = strings.Replace(target.Label.PackageName, "/", ".", -1) 74 results.Name = target.Label.Name 75 results.Cached = true 76 if err != nil { 77 state.LogBuildError(tid, label, core.TargetTestFailed, err, "Failed to parse cached test file %s", cachedOutputFile) 78 } else if results.Failures() > 0 { 79 log.Warning("Test results (for %s) with failures shouldn't be cached - ignoring.", label) 80 state.Cache.Clean(target) 81 return nil 82 } else { 83 logTestSuccess(state, tid, label, &results, &coverage) 84 } 85 return &results 86 } 87 88 moveAndCacheOutputFiles := func(results *core.TestSuite, coverage *core.TestCoverage) bool { 89 // Never cache test results when given arguments; the results may be incomplete. 90 if len(state.TestArgs) > 0 { 91 log.Debug("Not caching results for %s, we passed it arguments", label) 92 return true 93 } 94 // Never cache test results if there were failures (usually flaky tests). 95 if results.Failures() > 0 { 96 log.Debug("Not caching results for %s, test had failures", label) 97 return true 98 } 99 if err := moveAndCacheOutputFile(state, target, hash, outputFile, cachedOutputFile, resultsFileName, dummyOutput); err != nil { 100 state.LogTestResult(tid, label, core.TargetTestFailed, results, coverage, err, "Failed to move test output file") 101 return false 102 } 103 if needCoverage || core.PathExists(coverageFile) { 104 if err := moveAndCacheOutputFile(state, target, hash, coverageFile, cachedCoverageFile, coverageFileName, dummyCoverage); err != nil { 105 state.LogTestResult(tid, label, core.TargetTestFailed, results, coverage, err, "Failed to move test coverage file") 106 return false 107 } 108 } 109 for _, output := range target.TestOutputs { 110 tmpFile := path.Join(target.TestDir(), output) 111 outFile := path.Join(target.OutDir(), output) 112 if err := moveAndCacheOutputFile(state, target, hash, tmpFile, outFile, output, ""); err != nil { 113 state.LogTestResult(tid, label, core.TargetTestFailed, results, coverage, err, "Failed to move test output file") 114 return false 115 } 116 } 117 return true 118 } 119 120 needToRun := func() bool { 121 if target.State() == core.Unchanged && core.PathExists(cachedOutputFile) { 122 // Output file exists already and appears to be valid. We might still need to rerun though 123 // if the coverage files aren't available. 124 if needCoverage && !verifyHash(cachedCoverageFile, hash) { 125 log.Debug("Rerunning %s, coverage file doesn't exist or has wrong hash", target.Label) 126 return true 127 } else if !verifyHash(cachedOutputFile, hash) { 128 log.Debug("Rerunning %s, results file has incorrect hash", target.Label) 129 return true 130 } 131 return false 132 } 133 log.Debug("Output file %s does not exist for %s", cachedOutputFile, target.Label) 134 // Check the cache for these artifacts. 135 if state.Cache == nil { 136 return true 137 } 138 if !state.Cache.RetrieveExtra(target, hash, resultsFileName) { 139 return true 140 } 141 if needCoverage && !state.Cache.RetrieveExtra(target, hash, coverageFileName) { 142 return true 143 } 144 for _, output := range target.TestOutputs { 145 if !state.Cache.RetrieveExtra(target, hash, output) { 146 return true 147 } 148 } 149 return false 150 } 151 152 // Don't cache when doing multiple runs, presumably the user explicitly wants to check it. 153 if state.NumTestRuns == 1 && !needToRun() { 154 cachedResults := cachedTestResults() 155 if cachedResults != nil { 156 target.Results = *cachedResults 157 return 158 } 159 } 160 161 // Fresh set of results for this target. 162 target.Results = core.TestSuite{ 163 Package: strings.Replace(target.Label.PackageName, "/", ".", -1), 164 Name: target.Label.Name, 165 Timestamp: time.Now().Format(time.RFC3339), 166 } 167 168 // Remove any cached test result file. 169 if err := RemoveTestOutputs(target); err != nil { 170 state.LogBuildError(tid, label, core.TargetTestFailed, err, "Failed to remove test output files") 171 return 172 } 173 if worker, err := startTestWorkerIfNeeded(tid, state, target); err != nil { 174 state.LogBuildError(tid, label, core.TargetTestFailed, err, "Failed to start test worker") 175 testCase := core.TestCase{ 176 Name: worker, 177 Executions: []core.TestExecution{ 178 { 179 Failure: &core.TestResultFailure{ 180 Message: "Failed to start test worker", 181 Type: "WorkerFail", 182 Traceback: err.Error(), 183 }, 184 }, 185 }, 186 } 187 target.Results.TestCases = append(target.Results.TestCases, testCase) 188 return 189 } 190 191 var coverage core.TestCoverage 192 193 // Always run the test this number of times 194 for runs := 1; runs <= state.NumTestRuns; runs++ { 195 status := "Testing" 196 var runStatus string 197 if state.NumTestRuns > 1 { 198 runStatus = status + fmt.Sprintf(" (run %d of %d)", runs, state.NumTestRuns) 199 } else { 200 runStatus = status 201 } 202 // New group of test cases for each group of flaky runs 203 flakeResults := core.TestSuite{} 204 // Run tests at least once, but possibly more if it's flaky. 205 // Flakiness will be `3` if `flaky` is `True` in the build_def. 206 numFlakes := utils.Max(target.Flakiness, 1) 207 for flakes := 1; flakes <= numFlakes; flakes++ { 208 var flakeStatus string 209 if numFlakes > 1 { 210 flakeStatus = runStatus + fmt.Sprintf(" (flake %d of %d)", flakes, numFlakes) 211 } else { 212 flakeStatus = runStatus 213 } 214 state.LogBuildResult(tid, label, core.TargetTesting, fmt.Sprintf("%s...", flakeStatus)) 215 216 testSuite := doTest(tid, state, target, outputFile) 217 218 flakeResults.TimedOut = flakeResults.TimedOut || testSuite.TimedOut 219 flakeResults.Properties = testSuite.Properties 220 flakeResults.Duration += testSuite.Duration 221 // Each set of executions is treated as a group 222 // So if a test flakes three times, three executions will be part of one test case. 223 flakeResults.Add(testSuite.TestCases...) 224 225 // If execution succeeded, we can break out of the flake loop 226 if testSuite.TestCases.AllSucceeded() { 227 break 228 } 229 } 230 // Each set of executions is now treated separately 231 // So if you ask for 3 runs you get 3 separate `PASS`es. 232 target.Results.Collapse(flakeResults) 233 } 234 235 coverage = parseCoverageFile(target, coverageFile) 236 237 if target.Results.TestCases.AllSucceeded() { 238 // Success, clean things up 239 if moveAndCacheOutputFiles(&target.Results, &coverage) { 240 logTestSuccess(state, tid, label, &target.Results, &coverage) 241 } 242 // Clean up the test directory. 243 if state.CleanWorkdirs { 244 if err := os.RemoveAll(target.TestDir()); err != nil { 245 log.Warning("Failed to remove test directory for %s: %s", target.Label, err) 246 } 247 } 248 } else { 249 var resultErr error 250 var resultMsg string 251 if target.Results.Failures() > 0 { 252 resultMsg = "Tests failed" 253 for _, testCase := range target.Results.TestCases { 254 if len(testCase.Failures()) > 0 { 255 resultErr = fmt.Errorf(testCase.Failures()[0].Failure.Message) 256 } 257 } 258 } else if target.Results.Errors() > 0 { 259 resultMsg = "Tests errored" 260 for _, testCase := range target.Results.TestCases { 261 if len(testCase.Errors()) > 0 { 262 resultErr = fmt.Errorf(testCase.Errors()[0].Error.Message) 263 } 264 } 265 } else { 266 resultErr = fmt.Errorf("unknown error") 267 resultMsg = "Something went wrong" 268 } 269 state.LogTestResult(tid, label, core.TargetTestFailed, &target.Results, &coverage, resultErr, resultMsg) 270 } 271 } 272 273 func logTestSuccess(state *core.BuildState, tid int, label core.BuildLabel, results *core.TestSuite, coverage *core.TestCoverage) { 274 var description string 275 tests := pluralise("test", results.Tests()) 276 if results.Skips() != 0 { 277 description = fmt.Sprintf("%d %s passed. %d skipped", 278 results.Tests(), tests, results.Skips()) 279 } else { 280 description = fmt.Sprintf("%d %s passed.", len(results.TestCases), tests) 281 } 282 state.LogTestResult(tid, label, core.TargetTested, results, coverage, nil, description) 283 } 284 285 func pluralise(word string, quantity int) string { 286 if quantity == 1 { 287 return word 288 } 289 return word + "s" 290 } 291 292 func prepareTestDir(graph *core.BuildGraph, target *core.BuildTarget) error { 293 if err := os.RemoveAll(target.TestDir()); err != nil { 294 return err 295 } 296 if err := os.MkdirAll(target.TestDir(), core.DirPermissions); err != nil { 297 return err 298 } 299 for out := range core.IterRuntimeFiles(graph, target, true) { 300 if err := core.PrepareSourcePair(out); err != nil { 301 return err 302 } 303 } 304 return nil 305 } 306 307 // testCommandAndEnv returns the test command & environment for a target. 308 func testCommandAndEnv(state *core.BuildState, target *core.BuildTarget) (string, []string) { 309 replacedCmd := build.ReplaceTestSequences(state, target, target.GetTestCommand(state)) 310 env := core.TestEnvironment(state, target, path.Join(core.RepoRoot, target.TestDir())) 311 if len(state.TestArgs) > 0 { 312 args := strings.Join(state.TestArgs, " ") 313 replacedCmd += " " + args 314 env = append(env, "TESTS="+args) 315 } 316 return replacedCmd, env 317 } 318 319 func runTest(state *core.BuildState, target *core.BuildTarget) ([]byte, error) { 320 replacedCmd, env := testCommandAndEnv(state, target) 321 log.Debug("Running test %s\nENVIRONMENT:\n%s\n%s", target.Label, strings.Join(env, "\n"), replacedCmd) 322 _, stderr, err := core.ExecWithTimeoutShellStdStreams(state, target, target.TestDir(), env, target.TestTimeout, state.Config.Test.Timeout, state.ShowAllOutput, replacedCmd, target.TestSandbox, state.DebugTests, "testing") 323 return stderr, err 324 } 325 326 func doTest(tid int, state *core.BuildState, target *core.BuildTarget, outputFile string) core.TestSuite { 327 startTime := time.Now() 328 stdout, runError := prepareAndRunTest(tid, state, target) 329 duration := time.Since(startTime) 330 331 parsedSuite := parseTestOutput(stdout, "", runError, duration, target, outputFile) 332 333 return core.TestSuite{ 334 Package: strings.Replace(target.Label.PackageName, "/", ".", -1), 335 Name: target.Label.Name, 336 Duration: duration, 337 TimedOut: runError == context.DeadlineExceeded, 338 Properties: parsedSuite.Properties, 339 TestCases: parsedSuite.TestCases, 340 } 341 } 342 343 // prepareAndRunTest sets up a test directory and runs the test. 344 func prepareAndRunTest(tid int, state *core.BuildState, target *core.BuildTarget) (stdout []byte, err error) { 345 if err = prepareTestDir(state.Graph, target); err != nil { 346 state.LogBuildError(tid, target.Label, core.TargetTestFailed, err, "Failed to prepare test directory for %s: %s", target.Label, err) 347 return []byte{}, err 348 } 349 return runPossiblyContainerisedTest(tid, state, target) 350 } 351 352 func parseTestOutput(stdout []byte, stderr string, runError error, duration time.Duration, target *core.BuildTarget, outputFile string) core.TestSuite { 353 // This is all pretty involved; there are lots of different possibilities of what could happen. 354 // The contract is that the test must return zero on success or non-zero on failure (Unix FTW). 355 // If it's successful, it must produce a parseable file named "test.results" in its temp folder. 356 // (alternatively, this can be a directory containing parseable files). 357 // Tests can opt out of the file requirement individually, in which case they're judged only 358 // by their return value. 359 // But of course, we still have to consider all the alternatives here and handle them nicely. 360 361 // No output and no execution error and output not expected - OK 362 // No output and no execution error and output expected - SYNTHETIC ERROR - Missing Results 363 // No output and execution error - SYNTHETIC ERROR - Failed to Run 364 // Output and no execution error - PARSE OUTPUT - Ignore noTestOutput 365 // Output and execution error - PARSE OUTPUT + SYNTHETIC ERROR - Incomplete Run 366 if !core.PathExists(outputFile) { 367 if runError == nil && target.NoTestOutput { 368 return core.TestSuite{ 369 TestCases: []core.TestCase{ 370 { 371 // Need a name so that multiple runs get collated correctly. 372 Name: target.Results.Name, 373 Executions: []core.TestExecution{ 374 { 375 Duration: &duration, 376 Stdout: string(stdout), 377 Stderr: stderr, 378 }, 379 }, 380 }, 381 }, 382 } 383 } else if runError == nil { 384 return core.TestSuite{ 385 TestCases: []core.TestCase{ 386 { 387 Name: target.Results.Name, 388 Executions: []core.TestExecution{ 389 { 390 Duration: &duration, 391 Stdout: string(stdout), 392 Stderr: stderr, 393 Error: &core.TestResultFailure{ 394 Message: "Test failed to produce output results file", 395 Type: "MissingResults", 396 }, 397 }, 398 }, 399 }, 400 }, 401 } 402 } else if target.NoTestOutput { 403 return core.TestSuite{ 404 TestCases: []core.TestCase{ 405 { 406 Name: target.Results.Name, 407 Executions: []core.TestExecution{ 408 { 409 Duration: &duration, 410 Stdout: string(stdout), 411 Stderr: stderr, 412 Failure: &core.TestResultFailure{ 413 Message: "Test failed: " + runError.Error(), 414 Type: "TestFailed", 415 }, 416 }, 417 }, 418 }, 419 }, 420 } 421 } 422 423 return core.TestSuite{ 424 TestCases: []core.TestCase{ 425 { 426 Name: target.Results.Name, 427 Executions: []core.TestExecution{ 428 { 429 Duration: &duration, 430 Stdout: string(stdout), 431 Stderr: stderr, 432 Error: &core.TestResultFailure{ 433 Message: "Test failed with no results", 434 Type: "NoResults", 435 Traceback: runError.Error(), 436 }, 437 }, 438 }, 439 }, 440 }, 441 } 442 } 443 444 results, parseError := parseTestResults(outputFile) 445 if parseError != nil { 446 if runError != nil { 447 return core.TestSuite{ 448 TestCases: []core.TestCase{ 449 { 450 Name: target.Results.Name, 451 Executions: []core.TestExecution{ 452 { 453 Duration: &duration, 454 Stdout: string(stdout), 455 Stderr: stderr, 456 Error: &core.TestResultFailure{ 457 Message: "Test failed with no results", 458 Type: "NoResults", 459 Traceback: runError.Error(), 460 }, 461 }, 462 }, 463 }, 464 }, 465 } 466 } 467 468 return core.TestSuite{ 469 TestCases: []core.TestCase{ 470 { 471 Name: "Unknown", 472 Executions: []core.TestExecution{ 473 { 474 Duration: &duration, 475 Stdout: string(stdout), 476 Stderr: stderr, 477 Error: &core.TestResultFailure{ 478 Message: "Couldn't parse test output file", 479 Type: "NoResults", 480 Traceback: parseError.Error(), 481 }, 482 }, 483 }, 484 }, 485 }, 486 } 487 } 488 489 if runError != nil && results.Failures() == 0 { 490 // Add a failure result to the test so it shows up in the final aggregation. 491 results.TestCases = append(results.TestCases, core.TestCase{ 492 // We don't know the type of test we ran :( 493 Name: target.Results.Name, 494 Executions: []core.TestExecution{ 495 { 496 Duration: &duration, 497 Stdout: string(stdout), 498 Stderr: stderr, 499 Error: &core.TestResultFailure{ 500 Type: "ReturnValue", 501 Message: "Test returned nonzero but reported no errors", 502 Traceback: runError.Error(), 503 }, 504 }, 505 }, 506 }) 507 } else if runError == nil && results.Failures() != 0 { 508 results.TestCases = append(results.TestCases, core.TestCase{ 509 // We don't know the type of test we ran :( 510 Name: target.Results.Name, 511 Executions: []core.TestExecution{ 512 { 513 Duration: &duration, 514 Stdout: string(stdout), 515 Stderr: stderr, 516 Failure: &core.TestResultFailure{ 517 Type: "ReturnValue", 518 Message: "Test returned 0 but still reported failures", 519 }, 520 }, 521 }, 522 }) 523 } 524 525 return results 526 } 527 528 // Parses the coverage output for a single target. 529 func parseCoverageFile(target *core.BuildTarget, coverageFile string) core.TestCoverage { 530 coverage, err := parseTestCoverage(target, coverageFile) 531 if err != nil { 532 log.Errorf("Failed to parse coverage file for %s: %s", target.Label, err) 533 } 534 return coverage 535 } 536 537 // RemoveTestOutputs removes any cached test or coverage result files for a target. 538 func RemoveTestOutputs(target *core.BuildTarget) error { 539 if err := os.RemoveAll(target.TestResultsFile()); err != nil { 540 return err 541 } else if err := os.RemoveAll(target.CoverageFile()); err != nil { 542 return err 543 } 544 for _, output := range target.TestOutputs { 545 if err := os.RemoveAll(path.Join(target.OutDir(), output)); err != nil { 546 return err 547 } 548 } 549 return nil 550 } 551 552 // Attempt to write a dummy coverage file to record that it's been done for a test. 553 func moveAndCacheOutputFile(state *core.BuildState, target *core.BuildTarget, hash []byte, from, to, filename, dummy string) error { 554 if !core.PathExists(from) { 555 if dummy == "" { 556 return nil 557 } 558 if err := ioutil.WriteFile(to, []byte(dummy), 0644); err != nil { 559 return err 560 } 561 } else if err := os.Rename(from, to); err != nil { 562 return err 563 } 564 if state.Cache != nil { 565 state.Cache.StoreExtra(target, hash, filename) 566 } 567 // Set the hash on the new destination file 568 return xattr.LSet(to, xattrName, hash) 569 } 570 571 // startTestWorkerIfNeeded starts a worker server if the test needs one. 572 func startTestWorkerIfNeeded(tid int, state *core.BuildState, target *core.BuildTarget) (string, error) { 573 workerCmd, _, _ := build.TestWorkerCommand(state, target) 574 if workerCmd == "" { 575 return "", nil 576 } 577 state.LogBuildResult(tid, target.Label, core.TargetTesting, "Starting test worker...") 578 err := worker.EnsureWorkerStarted(state, workerCmd, target) 579 if err == nil { 580 state.LogBuildResult(tid, target.Label, core.TargetTesting, "Testing...") 581 } 582 return workerCmd, err 583 } 584 585 // verifyHash verifies that the hash on a test file matches the one for the current test. 586 func verifyHash(filename string, hash []byte) bool { 587 attr, err := xattr.LGet(filename, xattrName) 588 return err == nil && bytes.Equal(attr, hash) 589 }