in dev-tools/mage/gotest.go [215:356]
func GoTest(ctx context.Context, params GoTestArgs) error {
mg.Deps(InstallGoTestTools)
fmt.Println(">> go test:", params.LogName, "Testing")
// We use gotestsum to drive the tests and produce a junit report.
// The tool runs `go test -json` in order to produce a structured log which makes it easier
// to parse the actual test output.
// Of OutputFile is given the original JSON file will be written as well.
//
// The runner needs to set CLI flags for gotestsum and for "go test". We track the different
// CLI flags in the gotestsumArgs and testArgs variables, such that we can finally produce command like:
// $ gotestsum <gotestsum args> -- <go test args>
//
// The additional arguments given via GoTestArgs are applied to `go test` only. Callers can not
// modify any of the gotestsum arguments.
gotestsumArgs := []string{"--no-color"}
if mg.Verbose() {
gotestsumArgs = append(gotestsumArgs, "-f", "standard-verbose")
} else {
gotestsumArgs = append(gotestsumArgs, "-f", "standard-quiet")
}
if params.JUnitReportFile != "" {
CreateDir(params.JUnitReportFile)
gotestsumArgs = append(gotestsumArgs, "--junitfile", params.JUnitReportFile)
}
if params.OutputFile != "" {
CreateDir(params.OutputFile)
gotestsumArgs = append(gotestsumArgs, "--jsonfile", params.OutputFile+".json")
}
var testArgs []string
if params.Race {
testArgs = append(testArgs, "-race")
}
if len(params.Tags) > 0 {
params := strings.Join(params.Tags, " ")
if params != "" {
testArgs = append(testArgs, "-tags", params)
}
}
if params.CoverageProfileFile != "" {
params.CoverageProfileFile = createDir(filepath.Clean(params.CoverageProfileFile))
testArgs = append(testArgs,
"-covermode=atomic",
"-coverprofile="+params.CoverageProfileFile,
)
}
// Pass the go test extra flags BEFORE the RunExpr.
// TL;DR: This is needed to make sure that a -test.run flag specified in the GOTEST_FLAGS environment variable does
// not interfere with the batching done by the framework.
//
// Full explanation:
// The integration test framework runs the tests twice:
// - the first time we pass a special tag that make all the define statements in the tests skip the test and dump the requirements.
// This output is processed by the integration test framework to discover the tests and the set of environments/machines
// we will need to spawn and allocate the tests to the various machines. (see batch.go for details)
// - the second time we run the tests (here) the integration test framework adds a -test.run flag when launching go test
// on the remote machine to make sure that only the tests corresponding to that batch are executed.
//
// By specifying the extra flags before the -test.run for the batch we make sure that the last flag definition "wins"
// (have a look at the unit test in batch_test.go), so that whatever run constraint is specified in GOTEST_FLAGS
// participates in the discovery and batching (1st go test execution) but doesn't override the actual execution on
// the remote machine (2nd go test execution).
testArgs = append(testArgs, params.ExtraFlags...)
if params.RunExpr != "" {
testArgs = append(testArgs, "-run", params.RunExpr)
}
testArgs = append(testArgs, params.Packages...)
args := append(gotestsumArgs, append([]string{"--"}, testArgs...)...)
fmt.Println(">> ARGS:", params.LogName, "Command:", "gotestsum", strings.Join(args, " "))
goTest := makeCommand(ctx, params.Env, "gotestsum", args...)
// Wire up the outputs.
var outputs []io.Writer
if params.Output != nil {
outputs = append(outputs, params.Output)
}
if params.OutputFile != "" {
fileOutput, err := os.Create(createDir(params.OutputFile))
if err != nil {
return fmt.Errorf("failed to create go test output file: %w", err)
}
defer fileOutput.Close()
outputs = append(outputs, fileOutput)
}
output := io.MultiWriter(outputs...)
if params.Output == nil {
goTest.Stdout = io.MultiWriter(output, os.Stdout)
goTest.Stderr = io.MultiWriter(output, os.Stderr)
} else {
goTest.Stdout = output
goTest.Stderr = output
}
err := goTest.Run()
var goTestErr *exec.ExitError
if err != nil {
// Command ran.
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
return fmt.Errorf("failed to execute go: %w", err)
}
// Command ran but failed. Process the output.
goTestErr = exitErr
}
if goTestErr != nil {
// No packages were tested. Probably the code didn't compile.
return fmt.Errorf("go test returned a non-zero value: %w", goTestErr)
}
// Generate a HTML code coverage report.
var htmlCoverReport string
if params.CoverageProfileFile != "" {
htmlCoverReport = strings.TrimSuffix(params.CoverageProfileFile,
filepath.Ext(params.CoverageProfileFile)) + ".html"
coverToHTML := sh.RunCmd("go", "tool", "cover",
"-html="+params.CoverageProfileFile,
"-o", htmlCoverReport)
if err = coverToHTML(); err != nil {
return fmt.Errorf("failed to write HTML code coverage report: %w", err)
}
}
// Return an error indicating that testing failed.
if goTestErr != nil {
fmt.Println(">> go test:", params.LogName, "Test Failed")
return fmt.Errorf("go test returned a non-zero value: %w", goTestErr)
}
fmt.Println(">> go test:", params.LogName, "Test Passed")
return nil
}