356 lines
14 KiB
Go
356 lines
14 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/onsi/ginkgo/v2/formatter"
|
|
"github.com/onsi/ginkgo/v2/ginkgo/command"
|
|
"github.com/onsi/ginkgo/v2/internal/parallel_support"
|
|
"github.com/onsi/ginkgo/v2/reporters"
|
|
"github.com/onsi/ginkgo/v2/types"
|
|
)
|
|
|
|
func RunCompiledSuite(suite TestSuite, ginkgoConfig types.SuiteConfig, reporterConfig types.ReporterConfig, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig, additionalArgs []string) TestSuite {
|
|
suite.State = TestSuiteStateFailed
|
|
suite.HasProgrammaticFocus = false
|
|
|
|
if suite.PathToCompiledTest == "" {
|
|
return suite
|
|
}
|
|
|
|
if suite.IsGinkgo && cliConfig.ComputedProcs() > 1 {
|
|
suite = runParallel(suite, ginkgoConfig, reporterConfig, cliConfig, goFlagsConfig, additionalArgs)
|
|
} else if suite.IsGinkgo {
|
|
suite = runSerial(suite, ginkgoConfig, reporterConfig, cliConfig, goFlagsConfig, additionalArgs)
|
|
} else {
|
|
suite = runGoTest(suite, cliConfig, goFlagsConfig)
|
|
}
|
|
runAfterRunHook(cliConfig.AfterRunHook, reporterConfig.NoColor, suite)
|
|
return suite
|
|
}
|
|
|
|
func buildAndStartCommand(suite TestSuite, args []string, pipeToStdout bool) (*exec.Cmd, *bytes.Buffer) {
|
|
buf := &bytes.Buffer{}
|
|
cmd := exec.Command(suite.PathToCompiledTest, args...)
|
|
cmd.Dir = suite.Path
|
|
if pipeToStdout {
|
|
cmd.Stderr = io.MultiWriter(os.Stdout, buf)
|
|
cmd.Stdout = os.Stdout
|
|
} else {
|
|
cmd.Stderr = buf
|
|
cmd.Stdout = buf
|
|
}
|
|
err := cmd.Start()
|
|
command.AbortIfError("Failed to start test suite", err)
|
|
|
|
return cmd, buf
|
|
}
|
|
|
|
func checkForNoTestsWarning(buf *bytes.Buffer) bool {
|
|
if strings.Contains(buf.String(), "warning: no tests to run") {
|
|
fmt.Fprintf(os.Stderr, `Found no test suites, did you forget to run "ginkgo bootstrap"?`)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func runGoTest(suite TestSuite, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig) TestSuite {
|
|
// As we run the go test from the suite directory, make sure the cover profile is absolute
|
|
// and placed into the expected output directory when one is configured.
|
|
if goFlagsConfig.Cover && !filepath.IsAbs(goFlagsConfig.CoverProfile) {
|
|
goFlagsConfig.CoverProfile = AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0)
|
|
}
|
|
|
|
args, err := types.GenerateGoTestRunArgs(goFlagsConfig)
|
|
command.AbortIfError("Failed to generate test run arguments", err)
|
|
cmd, buf := buildAndStartCommand(suite, args, true)
|
|
|
|
cmd.Wait()
|
|
|
|
exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
|
|
passed := (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE)
|
|
passed = !(checkForNoTestsWarning(buf) && cliConfig.RequireSuite) && passed
|
|
if passed {
|
|
suite.State = TestSuiteStatePassed
|
|
} else {
|
|
suite.State = TestSuiteStateFailed
|
|
}
|
|
|
|
return suite
|
|
}
|
|
|
|
func runSerial(suite TestSuite, ginkgoConfig types.SuiteConfig, reporterConfig types.ReporterConfig, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig, additionalArgs []string) TestSuite {
|
|
if goFlagsConfig.Cover {
|
|
goFlagsConfig.CoverProfile = AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0)
|
|
}
|
|
if goFlagsConfig.BlockProfile != "" {
|
|
goFlagsConfig.BlockProfile = AbsPathForGeneratedAsset(goFlagsConfig.BlockProfile, suite, cliConfig, 0)
|
|
}
|
|
if goFlagsConfig.CPUProfile != "" {
|
|
goFlagsConfig.CPUProfile = AbsPathForGeneratedAsset(goFlagsConfig.CPUProfile, suite, cliConfig, 0)
|
|
}
|
|
if goFlagsConfig.MemProfile != "" {
|
|
goFlagsConfig.MemProfile = AbsPathForGeneratedAsset(goFlagsConfig.MemProfile, suite, cliConfig, 0)
|
|
}
|
|
if goFlagsConfig.MutexProfile != "" {
|
|
goFlagsConfig.MutexProfile = AbsPathForGeneratedAsset(goFlagsConfig.MutexProfile, suite, cliConfig, 0)
|
|
}
|
|
if reporterConfig.JSONReport != "" {
|
|
reporterConfig.JSONReport = AbsPathForGeneratedAsset(reporterConfig.JSONReport, suite, cliConfig, 0)
|
|
}
|
|
if reporterConfig.JUnitReport != "" {
|
|
reporterConfig.JUnitReport = AbsPathForGeneratedAsset(reporterConfig.JUnitReport, suite, cliConfig, 0)
|
|
}
|
|
if reporterConfig.TeamcityReport != "" {
|
|
reporterConfig.TeamcityReport = AbsPathForGeneratedAsset(reporterConfig.TeamcityReport, suite, cliConfig, 0)
|
|
}
|
|
|
|
args, err := types.GenerateGinkgoTestRunArgs(ginkgoConfig, reporterConfig, goFlagsConfig)
|
|
command.AbortIfError("Failed to generate test run arguments", err)
|
|
args = append([]string{"--test.timeout=0"}, args...)
|
|
args = append(args, additionalArgs...)
|
|
|
|
cmd, buf := buildAndStartCommand(suite, args, true)
|
|
|
|
cmd.Wait()
|
|
|
|
exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
|
|
suite.HasProgrammaticFocus = (exitStatus == types.GINKGO_FOCUS_EXIT_CODE)
|
|
passed := (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE)
|
|
passed = !(checkForNoTestsWarning(buf) && cliConfig.RequireSuite) && passed
|
|
if passed {
|
|
suite.State = TestSuiteStatePassed
|
|
} else {
|
|
suite.State = TestSuiteStateFailed
|
|
}
|
|
|
|
if suite.HasProgrammaticFocus {
|
|
if goFlagsConfig.Cover {
|
|
fmt.Fprintln(os.Stdout, "coverage: no coverfile was generated because specs are programmatically focused")
|
|
}
|
|
if goFlagsConfig.BlockProfile != "" {
|
|
fmt.Fprintln(os.Stdout, "no block profile was generated because specs are programmatically focused")
|
|
}
|
|
if goFlagsConfig.CPUProfile != "" {
|
|
fmt.Fprintln(os.Stdout, "no cpu profile was generated because specs are programmatically focused")
|
|
}
|
|
if goFlagsConfig.MemProfile != "" {
|
|
fmt.Fprintln(os.Stdout, "no mem profile was generated because specs are programmatically focused")
|
|
}
|
|
if goFlagsConfig.MutexProfile != "" {
|
|
fmt.Fprintln(os.Stdout, "no mutex profile was generated because specs are programmatically focused")
|
|
}
|
|
}
|
|
|
|
return suite
|
|
}
|
|
|
|
func runParallel(suite TestSuite, ginkgoConfig types.SuiteConfig, reporterConfig types.ReporterConfig, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig, additionalArgs []string) TestSuite {
|
|
type procResult struct {
|
|
passed bool
|
|
hasProgrammaticFocus bool
|
|
}
|
|
|
|
numProcs := cliConfig.ComputedProcs()
|
|
procOutput := make([]*bytes.Buffer, numProcs)
|
|
coverProfiles := []string{}
|
|
|
|
blockProfiles := []string{}
|
|
cpuProfiles := []string{}
|
|
memProfiles := []string{}
|
|
mutexProfiles := []string{}
|
|
|
|
procResults := make(chan procResult)
|
|
|
|
server, err := parallel_support.NewServer(numProcs, reporters.NewDefaultReporter(reporterConfig, formatter.ColorableStdOut))
|
|
command.AbortIfError("Failed to start parallel spec server", err)
|
|
server.Start()
|
|
defer server.Close()
|
|
|
|
if reporterConfig.JSONReport != "" {
|
|
reporterConfig.JSONReport = AbsPathForGeneratedAsset(reporterConfig.JSONReport, suite, cliConfig, 0)
|
|
}
|
|
if reporterConfig.JUnitReport != "" {
|
|
reporterConfig.JUnitReport = AbsPathForGeneratedAsset(reporterConfig.JUnitReport, suite, cliConfig, 0)
|
|
}
|
|
if reporterConfig.TeamcityReport != "" {
|
|
reporterConfig.TeamcityReport = AbsPathForGeneratedAsset(reporterConfig.TeamcityReport, suite, cliConfig, 0)
|
|
}
|
|
|
|
for proc := 1; proc <= numProcs; proc++ {
|
|
procGinkgoConfig := ginkgoConfig
|
|
procGinkgoConfig.ParallelProcess, procGinkgoConfig.ParallelTotal, procGinkgoConfig.ParallelHost = proc, numProcs, server.Address()
|
|
|
|
procGoFlagsConfig := goFlagsConfig
|
|
if goFlagsConfig.Cover {
|
|
procGoFlagsConfig.CoverProfile = AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, proc)
|
|
coverProfiles = append(coverProfiles, procGoFlagsConfig.CoverProfile)
|
|
}
|
|
if goFlagsConfig.BlockProfile != "" {
|
|
procGoFlagsConfig.BlockProfile = AbsPathForGeneratedAsset(goFlagsConfig.BlockProfile, suite, cliConfig, proc)
|
|
blockProfiles = append(blockProfiles, procGoFlagsConfig.BlockProfile)
|
|
}
|
|
if goFlagsConfig.CPUProfile != "" {
|
|
procGoFlagsConfig.CPUProfile = AbsPathForGeneratedAsset(goFlagsConfig.CPUProfile, suite, cliConfig, proc)
|
|
cpuProfiles = append(cpuProfiles, procGoFlagsConfig.CPUProfile)
|
|
}
|
|
if goFlagsConfig.MemProfile != "" {
|
|
procGoFlagsConfig.MemProfile = AbsPathForGeneratedAsset(goFlagsConfig.MemProfile, suite, cliConfig, proc)
|
|
memProfiles = append(memProfiles, procGoFlagsConfig.MemProfile)
|
|
}
|
|
if goFlagsConfig.MutexProfile != "" {
|
|
procGoFlagsConfig.MutexProfile = AbsPathForGeneratedAsset(goFlagsConfig.MutexProfile, suite, cliConfig, proc)
|
|
mutexProfiles = append(mutexProfiles, procGoFlagsConfig.MutexProfile)
|
|
}
|
|
|
|
args, err := types.GenerateGinkgoTestRunArgs(procGinkgoConfig, reporterConfig, procGoFlagsConfig)
|
|
command.AbortIfError("Failed to generate test run arguments", err)
|
|
args = append([]string{"--test.timeout=0"}, args...)
|
|
args = append(args, additionalArgs...)
|
|
|
|
cmd, buf := buildAndStartCommand(suite, args, false)
|
|
procOutput[proc-1] = buf
|
|
server.RegisterAlive(proc, func() bool { return cmd.ProcessState == nil || !cmd.ProcessState.Exited() })
|
|
|
|
go func() {
|
|
cmd.Wait()
|
|
exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
|
|
procResults <- procResult{
|
|
passed: (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE),
|
|
hasProgrammaticFocus: exitStatus == types.GINKGO_FOCUS_EXIT_CODE,
|
|
}
|
|
}()
|
|
}
|
|
|
|
passed := true
|
|
for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
|
|
result := <-procResults
|
|
passed = passed && result.passed
|
|
suite.HasProgrammaticFocus = suite.HasProgrammaticFocus || result.hasProgrammaticFocus
|
|
}
|
|
if passed {
|
|
suite.State = TestSuiteStatePassed
|
|
} else {
|
|
suite.State = TestSuiteStateFailed
|
|
}
|
|
|
|
select {
|
|
case <-server.GetSuiteDone():
|
|
fmt.Println("")
|
|
case <-time.After(time.Second):
|
|
//one of the nodes never finished reporting to the server. Something must have gone wrong.
|
|
fmt.Fprint(formatter.ColorableStdErr, formatter.F("\n{{bold}}{{red}}Ginkgo timed out waiting for all parallel procs to report back{{/}}\n"))
|
|
fmt.Fprint(formatter.ColorableStdErr, formatter.F("{{gray}}Test suite:{{/}} %s (%s)\n\n", suite.PackageName, suite.Path))
|
|
fmt.Fprint(formatter.ColorableStdErr, formatter.Fiw(0, formatter.COLS, "This occurs if a parallel process exits before it reports its results to the Ginkgo CLI. The CLI will now print out all the stdout/stderr output it's collected from the running processes. However you may not see anything useful in these logs because the individual test processes usually intercept output to stdout/stderr in order to capture it in the spec reports.\n\nYou may want to try rerunning your test suite with {{light-gray}}--output-interceptor-mode=none{{/}} to see additional output here and debug your suite.\n"))
|
|
fmt.Fprintln(formatter.ColorableStdErr, " ")
|
|
for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
|
|
fmt.Fprintf(formatter.ColorableStdErr, formatter.F("{{bold}}Output from proc %d:{{/}}\n", proc))
|
|
fmt.Fprintln(os.Stderr, formatter.Fi(1, "%s", procOutput[proc-1].String()))
|
|
}
|
|
fmt.Fprintf(os.Stderr, "** End **")
|
|
}
|
|
|
|
for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
|
|
output := procOutput[proc-1].String()
|
|
if proc == 1 && checkForNoTestsWarning(procOutput[0]) && cliConfig.RequireSuite {
|
|
suite.State = TestSuiteStateFailed
|
|
}
|
|
if strings.Contains(output, "deprecated Ginkgo functionality") {
|
|
fmt.Fprintln(os.Stderr, output)
|
|
}
|
|
}
|
|
|
|
if len(coverProfiles) > 0 {
|
|
if suite.HasProgrammaticFocus {
|
|
fmt.Fprintln(os.Stdout, "coverage: no coverfile was generated because specs are programmatically focused")
|
|
} else {
|
|
coverProfile := AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0)
|
|
err := MergeAndCleanupCoverProfiles(coverProfiles, coverProfile)
|
|
command.AbortIfError("Failed to combine cover profiles", err)
|
|
|
|
coverage, err := GetCoverageFromCoverProfile(coverProfile)
|
|
command.AbortIfError("Failed to compute coverage", err)
|
|
if coverage == 0 {
|
|
fmt.Fprintln(os.Stdout, "coverage: [no statements]")
|
|
} else {
|
|
fmt.Fprintf(os.Stdout, "coverage: %.1f%% of statements\n", coverage)
|
|
}
|
|
}
|
|
}
|
|
if len(blockProfiles) > 0 {
|
|
if suite.HasProgrammaticFocus {
|
|
fmt.Fprintln(os.Stdout, "no block profile was generated because specs are programmatically focused")
|
|
} else {
|
|
blockProfile := AbsPathForGeneratedAsset(goFlagsConfig.BlockProfile, suite, cliConfig, 0)
|
|
err := MergeProfiles(blockProfiles, blockProfile)
|
|
command.AbortIfError("Failed to combine blockprofiles", err)
|
|
}
|
|
}
|
|
if len(cpuProfiles) > 0 {
|
|
if suite.HasProgrammaticFocus {
|
|
fmt.Fprintln(os.Stdout, "no cpu profile was generated because specs are programmatically focused")
|
|
} else {
|
|
cpuProfile := AbsPathForGeneratedAsset(goFlagsConfig.CPUProfile, suite, cliConfig, 0)
|
|
err := MergeProfiles(cpuProfiles, cpuProfile)
|
|
command.AbortIfError("Failed to combine cpuprofiles", err)
|
|
}
|
|
}
|
|
if len(memProfiles) > 0 {
|
|
if suite.HasProgrammaticFocus {
|
|
fmt.Fprintln(os.Stdout, "no mem profile was generated because specs are programmatically focused")
|
|
} else {
|
|
memProfile := AbsPathForGeneratedAsset(goFlagsConfig.MemProfile, suite, cliConfig, 0)
|
|
err := MergeProfiles(memProfiles, memProfile)
|
|
command.AbortIfError("Failed to combine memprofiles", err)
|
|
}
|
|
}
|
|
if len(mutexProfiles) > 0 {
|
|
if suite.HasProgrammaticFocus {
|
|
fmt.Fprintln(os.Stdout, "no mutex profile was generated because specs are programmatically focused")
|
|
} else {
|
|
mutexProfile := AbsPathForGeneratedAsset(goFlagsConfig.MutexProfile, suite, cliConfig, 0)
|
|
err := MergeProfiles(mutexProfiles, mutexProfile)
|
|
command.AbortIfError("Failed to combine mutexprofiles", err)
|
|
}
|
|
}
|
|
|
|
return suite
|
|
}
|
|
|
|
func runAfterRunHook(command string, noColor bool, suite TestSuite) {
|
|
if command == "" {
|
|
return
|
|
}
|
|
f := formatter.NewWithNoColorBool(noColor)
|
|
|
|
// Allow for string replacement to pass input to the command
|
|
passed := "[FAIL]"
|
|
if suite.State.Is(TestSuiteStatePassed) {
|
|
passed = "[PASS]"
|
|
}
|
|
command = strings.ReplaceAll(command, "(ginkgo-suite-passed)", passed)
|
|
command = strings.ReplaceAll(command, "(ginkgo-suite-name)", suite.PackageName)
|
|
|
|
// Must break command into parts
|
|
splitArgs := regexp.MustCompile(`'.+'|".+"|\S+`)
|
|
parts := splitArgs.FindAllString(command, -1)
|
|
|
|
output, err := exec.Command(parts[0], parts[1:]...).CombinedOutput()
|
|
if err != nil {
|
|
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(0, "{{red}}{{bold}}After-run-hook failed:{{/}}"))
|
|
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(1, "{{red}}%s{{/}}", output))
|
|
} else {
|
|
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(0, "{{green}}{{bold}}After-run-hook succeeded:{{/}}"))
|
|
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(1, "{{green}}%s{{/}}", output))
|
|
}
|
|
}
|