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  }