diff --git a/README.md b/README.md index 27b9461..a68fd0d 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,11 @@ All the output from the `go test -v` command is shown. Reports each test (package) being ran. +### Parallel: -p +`Run test in parallel` + +Run multiple test (packages) in parallel, similar to `go test -p`. + # Output Courtney will fail if the tests fail. If the tests succeed, it will create or overwrite a `coverage.out` file in the current directory. diff --git a/courtney.go b/courtney.go index 56a98df..9dbc425 100644 --- a/courtney.go +++ b/courtney.go @@ -22,6 +22,7 @@ func main() { enforceFlag := flag.Bool("e", false, "Enforce 100% code coverage") verboseFlag := flag.Bool("v", false, "Verbose output") reportFlag := flag.Bool("r", false, "Print each package being tested") + parallelFlag := flag.Int("p", 1, "Run multiple tests in parallel") shortFlag := flag.Bool("short", false, "Pass the short flag to the go test command") timeoutFlag := flag.String("timeout", "", "Pass the timeout flag to the go test command") outputFlag := flag.String("o", "", "Override coverage file location") @@ -54,6 +55,7 @@ func main() { setup := &shared.Setup{ Env: env, Paths: patsy.NewCache(env), + Parallel: *parallelFlag, Enforce: *enforceFlag, Verbose: *verboseFlag, ReportTestRun: *reportFlag, diff --git a/shared/shared.go b/shared/shared.go index d81e3d5..e613159 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -12,6 +12,7 @@ import ( type Setup struct { Env vos.Env Paths *patsy.Cache + Parallel int Enforce bool Verbose bool ReportTestRun bool diff --git a/tester/tester.go b/tester/tester.go index affeaef..ef337da 100644 --- a/tester/tester.go +++ b/tester/tester.go @@ -1,6 +1,7 @@ package tester import ( + "context" "crypto/md5" "fmt" "io/ioutil" @@ -9,6 +10,7 @@ import ( "path/filepath" "regexp" "strings" + "sync" "github.com/dave/courtney/shared" "github.com/dave/courtney/tester/logger" @@ -29,6 +31,7 @@ func New(setup *shared.Setup) *Tester { type Tester struct { setup *shared.Setup cover string + mu sync.Mutex Results []*cover.Profile } @@ -60,14 +63,60 @@ func (t *Tester) Test() error { excludes[s] = true } + ctx, closeCtx := context.WithCancel(context.Background()) + + // run parallel worker routines to process dirs + parallel := t.setup.Parallel + if parallel < 1 { + parallel = 1 + } + work := make(chan string, len(t.setup.Packages)) + done := make(chan error, parallel) + for i := 0; i < parallel; i++ { + go func() { + for { + select { + case <-ctx.Done(): + done <- nil + return + + case dir := <-work: + // when channel is empty and closed we'll get a zero value + if dir == "" { + done <- nil + return + } + + if err := t.processDir(dir); err != nil { + done <- err + return + } + } + } + }() + } + for _, spec := range t.setup.Packages { if !excludes[spec.Path] { - if err := t.processDir(spec.Dir); err != nil { - return err - } + work <- spec.Dir } } + // close work channel, the worker routines can continue reading but will get a zero value when out of work + close(work) + + // wait for all workers to be done, if any of them error then we return the first error received + for i := 0; i < parallel; i++ { + err := <-done + if err != nil { + // close ctx to prevent the worker routines from processing anymore + closeCtx() + return err + } + } + + closeCtx() + return nil } @@ -284,6 +333,9 @@ func (t *Tester) processDir(dir string) error { } func (t *Tester) processCoverageFile(filename string) error { + t.mu.Lock() + defer t.mu.Unlock() + profiles, err := cover.ParseProfiles(filename) if err != nil { return err