diff --git a/.travis.yml b/.travis.yml index 6711356..e299701 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ script: - go test -race # run features - - go run cmd/godog/main.go -f progress + - go run cmd/godog/main.go --format=progress --concurrency=4 # code correctness - sh -c 'RES="$(go fmt ./...)"; if [ ! -z "$RES" ]; then echo $RES; exit 1; fi' diff --git a/Makefile b/Makefile index 88d87c9..2d321bc 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ test: @golint ./... go vet ./... go test - go run cmd/godog/main.go -f progress + go run cmd/godog/main.go -f progress -c 4 deps: @echo "updating all dependencies" diff --git a/README.md b/README.md index dffa6ab..70682e1 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ We only need a number of **godogs** for now. Lets define steps. /* file: examples/godogs/godog.go */ package main +// Godogs to eat var Godogs int func main() { /* usual main func */ } @@ -146,6 +147,9 @@ See implementation examples: - changed **godog.Suite** from interface to struct. Context registration should be updated accordingly. The reason for change: since it exports the same methods and there is no need to mock a function in tests, there is no obvious reason to keep an interface. +- in order to support running suite concurrently, needed to refactor an entry point of application. The **Run** method +now is a func of godog package which initializes and run the suite (or more suites). Method **New** is removed. This +change made godog a little cleaner. ### FAQ diff --git a/builder.go b/builder.go index 4d3f606..3954051 100644 --- a/builder.go +++ b/builder.go @@ -49,11 +49,12 @@ func newBuilder(buildPath string) (*builder, error) { ){{ end }} func main() { - suite := {{ if not .Internal }}godog.{{ end }}New() - {{range .Contexts}} - {{ . }}(suite) - {{end}} - suite.Run() + + {{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) { + {{range .Contexts}} + {{ . }}(suite) + {{end}} + }) }`)), } diff --git a/cmd/godog/main.go b/cmd/godog/main.go index 4eebb95..cb00c40 100644 --- a/cmd/godog/main.go +++ b/cmd/godog/main.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "os" "os/exec" + "time" "github.com/DATA-DOG/godog" "github.com/shiena/ansicolor" @@ -12,7 +14,7 @@ func buildAndRun() error { // will support Ansi colors for windows stdout := ansicolor.NewAnsiColorWriter(os.Stdout) - builtFile := "/tmp/bgodog.go" + builtFile := fmt.Sprintf("%s/%dgodog.go", os.TempDir(), time.Now().UnixNano()) // @TODO: then there is a suite error or panic, it may // be interesting to see the built file. But we // even cannot determine the status of exit error diff --git a/examples/godogs/godog.go b/examples/godogs/godog.go index 69b51b2..45d328c 100644 --- a/examples/godogs/godog.go +++ b/examples/godogs/godog.go @@ -1,5 +1,6 @@ package main +// Godogs to eat var Godogs int func main() { /* usual main func */ } diff --git a/features/snippets.feature b/features/snippets.feature index 936a5cb..d9d26ac 100644 --- a/features/snippets.feature +++ b/features/snippets.feature @@ -28,7 +28,7 @@ Feature: undefined step snippets return godog.ErrPending } - func featureContext(s godog.Suite) { + func featureContext(s *godog.Suite) { s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, iSendrequestTo) s.Step(`^the response code should be (\d+)$`, theResponseCodeShouldBe) } @@ -56,7 +56,7 @@ Feature: undefined step snippets return godog.ErrPending } - func featureContext(s godog.Suite) { + func featureContext(s *godog.Suite) { s.Step(`^I send "([^"]*)" request to "([^"]*)" with:$`, iSendrequestTowith) s.Step(`^the response code should be (\d+) and header "([^"]*)" should be "([^"]*)"$`, theResponseCodeShouldBeAndHeadershouldBe) } diff --git a/flags.go b/flags.go index 8f0c43c..5560ebe 100644 --- a/flags.go +++ b/flags.go @@ -5,18 +5,18 @@ import ( "fmt" ) -// Flags builds a *flag.FlagSet with all flags -// required for the godog suite -func flags(s *Suite) *flag.FlagSet { +func flags(format, tags *string, defs, sof, vers *bool, cl *int) *flag.FlagSet { set := flag.NewFlagSet("godog", flag.ExitOnError) - set.StringVar(&s.format, "format", "pretty", "") - set.StringVar(&s.format, "f", "pretty", "") - set.StringVar(&s.tags, "tags", "", "") - set.StringVar(&s.tags, "t", "", "") - set.BoolVar(&s.definitions, "definitions", false, "") - set.BoolVar(&s.definitions, "d", false, "") - set.BoolVar(&s.stopOnFailure, "stop-on-failure", false, "") - set.BoolVar(&s.version, "version", false, "") + set.StringVar(format, "format", "pretty", "") + set.StringVar(format, "f", "pretty", "") + set.StringVar(tags, "tags", "", "") + set.StringVar(tags, "t", "", "") + set.IntVar(cl, "concurrency", 1, "") + set.IntVar(cl, "c", 1, "") + set.BoolVar(defs, "definitions", false, "") + set.BoolVar(defs, "d", false, "") + set.BoolVar(sof, "stop-on-failure", false, "") + set.BoolVar(vers, "version", false, "") set.Usage = usage return set } @@ -27,7 +27,7 @@ func usage() { if len(name) > 0 { name += ":" } - return s(2) + cl(name, green) + s(30-len(name)) + desc + return s(2) + cl(name, green) + s(22-len(name)) + desc } // --- GENERAL --- @@ -48,6 +48,11 @@ func usage() { fmt.Println(cl("Options:", yellow)) // --> step definitions fmt.Println(opt("-d, --definitions", "Print all available step definitions.")) + // --> concurrency + fmt.Println(opt("-c, --concurrency=1", "Run the test suite with concurrency level:")) + fmt.Println(opt("", s(4)+"- "+cl(`= 1`, yellow)+": supports all types of formats.")) + fmt.Println(opt("", s(4)+"- "+cl(`>= 2`, yellow)+": only supports "+cl("progress", yellow)+". Note, that")) + fmt.Println(opt("", s(4)+"your context needs to support parallel execution.")) // --> format fmt.Println(opt("-f, --format=pretty", "How to format tests output. Available formats:")) for _, f := range formatters { diff --git a/fmt.go b/fmt.go index d7c086a..53ca65b 100644 --- a/fmt.go +++ b/fmt.go @@ -49,6 +49,22 @@ type registeredFormatter struct { var formatters []*registeredFormatter +func findFmt(format string) (f Formatter, err error) { + var names []string + for _, el := range formatters { + if el.name == format { + f = el.fmt + break + } + names = append(names, el.name) + } + + if f == nil { + err = fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, format, strings.Join(names, ", ")) + } + return +} + // RegisterFormatter registers a feature suite output // Formatter as the name and descriptiongiven. // Formatter is used to represent suite output diff --git a/fmt_progress.go b/fmt_progress.go index 4cece13..641c8df 100644 --- a/fmt_progress.go +++ b/fmt_progress.go @@ -3,6 +3,7 @@ package godog import ( "fmt" "math" + "sync" "time" "github.com/cucumber/gherkin-go" @@ -20,10 +21,23 @@ func init() { type progress struct { basefmt + sync.Mutex stepsPerRow int steps int } +func (f *progress) Node(n interface{}) { + f.Lock() + defer f.Unlock() + f.basefmt.Node(n) +} + +func (f *progress) Feature(ft *gherkin.Feature, p string) { + f.Lock() + defer f.Unlock() + f.basefmt.Feature(ft, p) +} + func (f *progress) Summary() { left := math.Mod(float64(f.steps), float64(f.stepsPerRow)) if left != 0 { @@ -65,26 +79,36 @@ func (f *progress) step(res *stepResult) { } func (f *progress) Passed(step *gherkin.Step, match *StepDef) { + f.Lock() + defer f.Unlock() f.basefmt.Passed(step, match) f.step(f.passed[len(f.passed)-1]) } func (f *progress) Skipped(step *gherkin.Step) { + f.Lock() + defer f.Unlock() f.basefmt.Skipped(step) f.step(f.skipped[len(f.skipped)-1]) } func (f *progress) Undefined(step *gherkin.Step) { + f.Lock() + defer f.Unlock() f.basefmt.Undefined(step) f.step(f.undefined[len(f.undefined)-1]) } func (f *progress) Failed(step *gherkin.Step, match *StepDef, err error) { + f.Lock() + defer f.Unlock() f.basefmt.Failed(step, match, err) f.step(f.failed[len(f.failed)-1]) } func (f *progress) Pending(step *gherkin.Step, match *StepDef) { + f.Lock() + defer f.Unlock() f.basefmt.Pending(step, match) f.step(f.pending[len(f.pending)-1]) } diff --git a/run.go b/run.go new file mode 100644 index 0000000..9b6e404 --- /dev/null +++ b/run.go @@ -0,0 +1,104 @@ +package godog + +import ( + "fmt" + "os" + "sync" +) + +type initializer func(*Suite) + +type runner struct { + sync.WaitGroup + + semaphore chan int + features []*feature + fmt Formatter // needs to support concurrency + initializer initializer +} + +func (r *runner) run() (failed bool) { + r.Add(len(r.features)) + for _, ft := range r.features { + go func(fail *bool, feat *feature) { + r.semaphore <- 1 + suite := &Suite{ + fmt: r.fmt, + features: []*feature{feat}, + } + r.initializer(suite) + suite.run() + if suite.failed { + *fail = true + } + <-r.semaphore + r.Done() + }(&failed, ft) + } + r.Wait() + + r.fmt.Summary() + return +} + +// Run creates and runs the feature suite. +// uses contextInitializer to register contexts +// +// the concurrency option allows runner to +// initialize a number of suites to be run +// separately. Only progress formatter +// is supported when concurrency level is +// higher than 1 +// +// contextInitializer must be able to register +// the step definitions and event handlers. +func Run(contextInitializer func(suite *Suite)) { + var vers, defs, sof bool + var tags, format string + var concurrency int + flagSet := flags(&format, &tags, &defs, &sof, &vers, &concurrency) + err := flagSet.Parse(os.Args[1:]) + fatal(err) + + switch { + case vers: + fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow)) + return + case defs: + s := &Suite{} + contextInitializer(s) + s.printStepDefinitions() + return + } + + paths := flagSet.Args() + if len(paths) == 0 { + inf, err := os.Stat("features") + if err == nil && inf.IsDir() { + paths = []string{"features"} + } + } + + if concurrency > 1 && format != "progress" { + fatal(fmt.Errorf("when concurrency level is higher than 1, only progress format is supported")) + } + if concurrency > 1 && sof { + fatal(fmt.Errorf("when concurrency level is higher than 1, cannot stop on first failure for now")) + } + formatter, err := findFmt(format) + fatal(err) + + features, err := parseFeatures(tags, paths) + fatal(err) + + r := runner{ + fmt: formatter, + initializer: contextInitializer, + semaphore: make(chan int, concurrency), + features: features, + } + + if failed := r.run(); failed { + os.Exit(1) + } +} diff --git a/suite.go b/suite.go index 3a86a56..16fe5a6 100644 --- a/suite.go +++ b/suite.go @@ -44,15 +44,8 @@ type Suite struct { features []*feature fmt Formatter - failed bool - - // options - paths []string - format string - tags string - definitions bool + failed bool stopOnFailure bool - version bool // suite event handlers beforeSuiteHandlers []func() @@ -63,12 +56,6 @@ type Suite struct { afterSuiteHandlers []func() } -// New initializes a Suite. The instance is passed around -// to all context initialization functions from *_test.go files. -func New() *Suite { - return &Suite{} -} - // Step allows to register a *StepDef in Godog // feature suite, the definition will be applied // to all steps matching the given Regexp expr. @@ -172,52 +159,6 @@ func (s *Suite) AfterSuite(f func()) { s.afterSuiteHandlers = append(s.afterSuiteHandlers, f) } -// Run starts the Godog feature suite -func (s *Suite) Run() { - flagSet := flags(s) - fatal(flagSet.Parse(os.Args[1:])) - - s.paths = flagSet.Args() - // check the default path - if len(s.paths) == 0 { - inf, err := os.Stat("features") - if err == nil && inf.IsDir() { - s.paths = []string{"features"} - } - } - // validate formatter - var names []string - for _, f := range formatters { - if f.name == s.format { - s.fmt = f.fmt - break - } - names = append(names, f.name) - } - - if s.fmt == nil { - fatal(fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, s.format, strings.Join(names, ", "))) - } - - // check if we need to just show something first - switch { - case s.version: - fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow)) - return - case s.definitions: - s.printStepDefinitions() - return - } - - fatal(s.parseFeatures()) - // run a feature suite - s.run() - - if s.failed { - os.Exit(1) - } -} - func (s *Suite) run() { // run before suite handlers for _, f := range s.beforeSuiteHandlers { @@ -235,7 +176,6 @@ func (s *Suite) run() { for _, f := range s.afterSuiteHandlers { f() } - s.fmt.Summary() } func (s *Suite) matchStep(step *gherkin.Step) *StepDef { @@ -434,8 +374,8 @@ func (s *Suite) printStepDefinitions() { } } -func (s *Suite) parseFeatures() (err error) { - for _, pat := range s.paths { +func parseFeatures(filter string, paths []string) (features []*feature, err error) { + for _, pat := range paths { // check if line number is specified parts := strings.Split(pat, ":") path := parts[0] @@ -443,7 +383,7 @@ func (s *Suite) parseFeatures() (err error) { if len(parts) > 1 { line, err = strconv.Atoi(parts[1]) if err != nil { - return fmt.Errorf("line number should follow after colon path delimiter") + return features, fmt.Errorf("line number should follow after colon path delimiter") } } // parse features @@ -458,7 +398,7 @@ func (s *Suite) parseFeatures() (err error) { if err != nil { return err } - s.features = append(s.features, &feature{Path: p, Feature: ft}) + features = append(features, &feature{Path: p, Feature: ft}) // filter scenario by line number if line != -1 { var scenarios []interface{} @@ -477,31 +417,31 @@ func (s *Suite) parseFeatures() (err error) { } ft.ScenarioDefinitions = scenarios } - s.applyTagFilter(ft) + applyTagFilter(filter, ft) } return err }) // check error switch { case os.IsNotExist(err): - return fmt.Errorf(`feature path "%s" is not available`, path) + return features, fmt.Errorf(`feature path "%s" is not available`, path) case os.IsPermission(err): - return fmt.Errorf(`feature path "%s" is not accessible`, path) + return features, fmt.Errorf(`feature path "%s" is not accessible`, path) case err != nil: - return err + return features, err } } return } -func (s *Suite) applyTagFilter(ft *gherkin.Feature) { - if len(s.tags) == 0 { +func applyTagFilter(tags string, ft *gherkin.Feature) { + if len(tags) == 0 { return } var scenarios []interface{} for _, scenario := range ft.ScenarioDefinitions { - if s.matchesTags(allTags(ft, scenario)) { + if matchesTags(tags, allTags(ft, scenario)) { scenarios = append(scenarios, scenario) } } @@ -554,9 +494,9 @@ func hasTag(tags []string, tag string) bool { } // based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters -func (s *Suite) matchesTags(tags []string) (ok bool) { +func matchesTags(filter string, tags []string) (ok bool) { ok = true - for _, andTags := range strings.Split(s.tags, "&&") { + for _, andTags := range strings.Split(filter, "&&") { var okComma bool for _, tag := range strings.Split(andTags, ",") { tag = strings.Replace(strings.TrimSpace(tag), "@", "", -1) diff --git a/suite_test.go b/suite_test.go index f7c175d..9b438c1 100644 --- a/suite_test.go +++ b/suite_test.go @@ -50,6 +50,7 @@ type firedEvent struct { } type suiteContext struct { + paths []string testedSuite *Suite events []*firedEvent fmt *testFormatter @@ -58,6 +59,7 @@ type suiteContext struct { func (s *suiteContext) ResetBeforeEachScenario(interface{}) { // reset whole suite with the state s.fmt = &testFormatter{} + s.paths = []string{} s.testedSuite = &Suite{fmt: s.fmt} // our tested suite will have the same context registered SuiteContext(s.testedSuite) @@ -177,12 +179,17 @@ func (s *suiteContext) aFeatureFile(name string, body *gherkin.DocString) error } func (s *suiteContext) featurePath(path string) error { - s.testedSuite.paths = append(s.testedSuite.paths, path) + s.paths = append(s.paths, path) return nil } func (s *suiteContext) parseFeatures() error { - return s.testedSuite.parseFeatures() + fts, err := parseFeatures("", s.paths) + if err != nil { + return err + } + s.testedSuite.features = append(s.testedSuite.features, fts...) + return nil } func (s *suiteContext) theSuiteShouldHave(state string) error { diff --git a/tag_filter_test.go b/tag_filter_test.go index 817448b..8b965b9 100644 --- a/tag_filter_test.go +++ b/tag_filter_test.go @@ -5,15 +5,13 @@ import ( ) func assertNotMatchesTagFilter(tags []string, filter string, t *testing.T) { - s := &Suite{tags: filter} - if s.matchesTags(tags) { + if matchesTags(filter, tags) { t.Errorf(`expected tags: %v not to match tag filter "%s", but it did`, tags, filter) } } func assertMatchesTagFilter(tags []string, filter string, t *testing.T) { - s := &Suite{tags: filter} - if !s.matchesTags(tags) { + if !matchesTags(filter, tags) { t.Errorf(`expected tags: %v to match tag filter "%s", but it did not`, tags, filter) } }