package main import ( "fmt" "runtime" "sync" "github.com/onsi/ginkgo/config" "github.com/onsi/ginkgo/ginkgo/interrupthandler" "github.com/onsi/ginkgo/ginkgo/testrunner" "github.com/onsi/ginkgo/ginkgo/testsuite" colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" ) type compilationInput struct { runner *testrunner.TestRunner result chan compilationOutput } type compilationOutput struct { runner *testrunner.TestRunner err error } type SuiteRunner struct { notifier *Notifier interruptHandler *interrupthandler.InterruptHandler } func NewSuiteRunner(notifier *Notifier, interruptHandler *interrupthandler.InterruptHandler) *SuiteRunner { return &SuiteRunner{ notifier: notifier, interruptHandler: interruptHandler, } } func (r *SuiteRunner) compileInParallel(runners []*testrunner.TestRunner, numCompilers int, willCompile func(suite testsuite.TestSuite)) chan compilationOutput { //we return this to the consumer, it will return each runner in order as it compiles compilationOutputs := make(chan compilationOutput, len(runners)) //an array of channels - the nth runner's compilation output is sent to the nth channel in this array //we read from these channels in order to ensure we run the suites in order orderedCompilationOutputs := []chan compilationOutput{} for range runners { orderedCompilationOutputs = append(orderedCompilationOutputs, make(chan compilationOutput, 1)) } //we're going to spin up numCompilers compilers - they're going to run concurrently and will consume this channel //we prefill the channel then close it, this ensures we compile things in the correct order workPool := make(chan compilationInput, len(runners)) for i, runner := range runners { workPool <- compilationInput{runner, orderedCompilationOutputs[i]} } close(workPool) //pick a reasonable numCompilers if numCompilers == 0 { numCompilers = runtime.NumCPU() } //a WaitGroup to help us wait for all compilers to shut down wg := &sync.WaitGroup{} wg.Add(numCompilers) //spin up the concurrent compilers for i := 0; i < numCompilers; i++ { go func() { defer wg.Done() for input := range workPool { if r.interruptHandler.WasInterrupted() { return } if willCompile != nil { willCompile(input.runner.Suite) } //We retry because Go sometimes steps on itself when multiple compiles happen in parallel. This is ugly, but should help resolve flakiness... var err error retries := 0 for retries <= 5 { if r.interruptHandler.WasInterrupted() { return } if err = input.runner.Compile(); err == nil { break } retries++ } input.result <- compilationOutput{input.runner, err} } }() } //read from the compilation output channels *in order* and send them to the caller //close the compilationOutputs channel to tell the caller we're done go func() { defer close(compilationOutputs) for _, orderedCompilationOutput := range orderedCompilationOutputs { select { case compilationOutput := <-orderedCompilationOutput: compilationOutputs <- compilationOutput case <-r.interruptHandler.C: //interrupt detected, wait for the compilers to shut down then bail //this ensure we clean up after ourselves as we don't leave any compilation processes running wg.Wait() return } } }() return compilationOutputs } func (r *SuiteRunner) RunSuites(runners []*testrunner.TestRunner, numCompilers int, keepGoing bool, willCompile func(suite testsuite.TestSuite)) (testrunner.RunResult, int) { runResult := testrunner.PassingRunResult() compilationOutputs := r.compileInParallel(runners, numCompilers, willCompile) numSuitesThatRan := 0 suitesThatFailed := []testsuite.TestSuite{} for compilationOutput := range compilationOutputs { if compilationOutput.err != nil { fmt.Print(compilationOutput.err.Error()) } numSuitesThatRan++ suiteRunResult := testrunner.FailingRunResult() if compilationOutput.err == nil { suiteRunResult = compilationOutput.runner.Run() } r.notifier.SendSuiteCompletionNotification(compilationOutput.runner.Suite, suiteRunResult.Passed) r.notifier.RunCommand(compilationOutput.runner.Suite, suiteRunResult.Passed) runResult = runResult.Merge(suiteRunResult) if !suiteRunResult.Passed { suitesThatFailed = append(suitesThatFailed, compilationOutput.runner.Suite) if !keepGoing { break } } if numSuitesThatRan < len(runners) && !config.DefaultReporterConfig.Succinct { fmt.Println("") } } if keepGoing && !runResult.Passed { r.listFailedSuites(suitesThatFailed) } return runResult, numSuitesThatRan } func (r *SuiteRunner) listFailedSuites(suitesThatFailed []testsuite.TestSuite) { fmt.Println("") fmt.Println("There were failures detected in the following suites:") maxPackageNameLength := 0 for _, suite := range suitesThatFailed { if len(suite.PackageName) > maxPackageNameLength { maxPackageNameLength = len(suite.PackageName) } } packageNameFormatter := fmt.Sprintf("%%%ds", maxPackageNameLength) for _, suite := range suitesThatFailed { if config.DefaultReporterConfig.NoColor { fmt.Printf("\t"+packageNameFormatter+" %s\n", suite.PackageName, suite.Path) } else { fmt.Fprintf(colorable.NewColorableStdout(), "\t%s"+packageNameFormatter+"%s %s%s%s\n", redColor, suite.PackageName, defaultStyle, lightGrayColor, suite.Path, defaultStyle) } } }