diff --git a/README.md b/README.md index bfc0124..541fe08 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,11 @@ See **.travis.yml** for supported **go** versions. The public API is stable enough, but it may break until **1.0.0** version, see `godog --version`. +### FAQ + +**Q:** Where can I configure common options globally? +**A:** You can't. Alias your common or project based commands: `alias mygodog="godog --format=progress"` + ### Contributions Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) - diff --git a/config.go b/config.go deleted file mode 100644 index ad413d4..0000000 --- a/config.go +++ /dev/null @@ -1,178 +0,0 @@ -package godog - -import ( - "flag" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/DATA-DOG/godog/gherkin" -) - -type registeredFormatter struct { - name string - fmt Formatter - description string -} - -var formatters []*registeredFormatter - -// RegisterFormatter registers a feature suite output -// Formatter as the name and descriptiongiven. -// Formatter is used to represent suite output -func RegisterFormatter(name, description string, f Formatter) { - formatters = append(formatters, ®isteredFormatter{ - name: name, - fmt: f, - description: description, - }) -} - -var cfg *config - -func init() { - cfg = &config{} - - flag.StringVar(&cfg.format, "format", "pretty", "") - flag.StringVar(&cfg.format, "f", "pretty", "") - flag.BoolVar(&cfg.definitions, "definitions", false, "") - flag.BoolVar(&cfg.definitions, "d", false, "") - flag.BoolVar(&cfg.stopOnFailure, "stop-on-failure", false, "") - flag.BoolVar(&cfg.version, "version", false, "") - - flag.Usage = func() { - // prints an option or argument with a description, or only description - opt := func(name, desc string) string { - if len(name) > 0 { - name += ":" - } - return s(2) + cl(name, green) + s(30-len(name)) + desc - } - - // --- GENERAL --- - fmt.Println(cl("Usage:", yellow)) - fmt.Println(s(2) + "godog [options] []\n") - - // --- ARGUMENTS --- - fmt.Println(cl("Arguments:", yellow)) - // --> paths - fmt.Println(opt("paths", "Optional path(s) to execute. Can be:")) - fmt.Println(opt("", s(4)+"- dir "+cl("(features/)", yellow))) - fmt.Println(opt("", s(4)+"- feature "+cl("(*.feature)", yellow))) - fmt.Println(opt("", s(4)+"- scenario at specific line "+cl("(*.feature:10)", yellow))) - fmt.Println(opt("", "If no paths are listed, suite tries "+cl("features", yellow)+" path by default.")) - fmt.Println("") - - // --- OPTIONS --- - fmt.Println(cl("Options:", yellow)) - // --> step definitions - fmt.Println(opt("-d, --definitions", "Print all available step definitions.")) - // --> format - fmt.Println(opt("-f, --format=pretty", "How to format tests output. Available formats:")) - for _, f := range formatters { - fmt.Println(opt("", s(4)+"- "+cl(f.name, yellow)+": "+f.description)) - } - // --> stop on failure - fmt.Println(opt("--stop-on-failure", "Stop processing on first failed scenario.")) - // --> version - fmt.Println(opt("--version", "Show current "+cl("godog", yellow)+" version.")) - fmt.Println("") - } -} - -type config struct { - paths []string - format string - - definitions bool - stopOnFailure bool - version bool -} - -func (c *config) validate() error { - c.paths = flag.Args() - // check the default path - if len(c.paths) == 0 { - inf, err := os.Stat("features") - if err == nil && inf.IsDir() { - c.paths = []string{"features"} - } - } - // formatter - var found bool - var names []string - for _, f := range formatters { - if f.name == c.format { - found = true - break - } - names = append(names, f.name) - } - - if !found { - return fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, c.format, strings.Join(names, ", ")) - } - return nil -} - -func (c *config) features() (lst []*gherkin.Feature, err error) { - for _, pat := range c.paths { - // check if line number is specified - parts := strings.Split(pat, ":") - path := parts[0] - line := -1 - if len(parts) > 1 { - line, err = strconv.Atoi(parts[1]) - if err != nil { - return lst, fmt.Errorf("line number should follow after colon path delimiter") - } - } - // parse features - err = filepath.Walk(path, func(p string, f os.FileInfo, err error) error { - if err == nil && !f.IsDir() && strings.HasSuffix(p, ".feature") { - ft, err := gherkin.ParseFile(p) - switch { - case err == gherkin.ErrEmpty: - // its ok, just skip it - case err != nil: - return err - default: - lst = append(lst, ft) - } - // filter scenario by line number - if line != -1 { - var scenarios []*gherkin.Scenario - for _, s := range ft.Scenarios { - if s.Token.Line == line { - scenarios = append(scenarios, s) - break - } - } - ft.Scenarios = scenarios - } - } - return err - }) - // check error - switch { - case os.IsNotExist(err): - return lst, fmt.Errorf(`feature path "%s" is not available`, path) - case os.IsPermission(err): - return lst, fmt.Errorf(`feature path "%s" is not accessible`, path) - case err != nil: - return lst, err - } - } - return -} - -func (c *config) formatter() (f Formatter) { - for _, fmt := range formatters { - if fmt.name == cfg.format { - return fmt.fmt - } - } - panic("formatter name had to be validated") -} diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..68ba3fa --- /dev/null +++ b/flags.go @@ -0,0 +1,64 @@ +package godog + +import ( + "flag" + "fmt" +) + +func flags(s *suite) *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.Usage = usage + return set +} + +func usage() { + // prints an option or argument with a description, or only description + opt := func(name, desc string) string { + if len(name) > 0 { + name += ":" + } + return s(2) + cl(name, green) + s(30-len(name)) + desc + } + + // --- GENERAL --- + fmt.Println(cl("Usage:", yellow)) + fmt.Println(s(2) + "godog [options] []\n") + + // --- ARGUMENTS --- + fmt.Println(cl("Arguments:", yellow)) + // --> paths + fmt.Println(opt("paths", "Optional path(s) to execute. Can be:")) + fmt.Println(opt("", s(4)+"- dir "+cl("(features/)", yellow))) + fmt.Println(opt("", s(4)+"- feature "+cl("(*.feature)", yellow))) + fmt.Println(opt("", s(4)+"- scenario at specific line "+cl("(*.feature:10)", yellow))) + fmt.Println(opt("", "If no paths are listed, suite tries "+cl("features", yellow)+" path by default.")) + fmt.Println("") + + // --- OPTIONS --- + fmt.Println(cl("Options:", yellow)) + // --> step definitions + fmt.Println(opt("-d, --definitions", "Print all available step definitions.")) + // --> format + fmt.Println(opt("-f, --format=pretty", "How to format tests output. Available formats:")) + for _, f := range formatters { + fmt.Println(opt("", s(4)+"- "+cl(f.name, yellow)+": "+f.description)) + } + // --> tags + fmt.Println(opt("-t, --tags", "Filter scenarios by tags. Expression can be:")) + fmt.Println(opt("", s(4)+"- "+cl(`"wip"`, yellow)+": run all scenarios with wip tag")) + fmt.Println(opt("", s(4)+"- "+cl(`"!wip"`, yellow)+": exclude all scenarios with wip tag")) + fmt.Println(opt("", s(4)+"- "+cl(`"wip !new"`, yellow)+": run wip scenarios, but exclude new")) + // --> stop on failure + fmt.Println(opt("--stop-on-failure", "Stop processing on first failed scenario.")) + // --> version + fmt.Println(opt("--version", "Show current "+cl("godog", yellow)+" version.")) + fmt.Println("") +} diff --git a/fmt.go b/fmt.go index 8d6a0bf..4b58b5a 100644 --- a/fmt.go +++ b/fmt.go @@ -6,6 +6,25 @@ import ( "github.com/DATA-DOG/godog/gherkin" ) +type registeredFormatter struct { + name string + fmt Formatter + description string +} + +var formatters []*registeredFormatter + +// RegisterFormatter registers a feature suite output +// Formatter as the name and descriptiongiven. +// Formatter is used to represent suite output +func RegisterFormatter(name, description string, f Formatter) { + formatters = append(formatters, ®isteredFormatter{ + name: name, + fmt: f, + description: description, + }) +} + // Formatter is an interface for feature runner // output summary presentation. // diff --git a/suite.go b/suite.go index 8477ffd..b9847f1 100644 --- a/suite.go +++ b/suite.go @@ -1,12 +1,13 @@ package godog import ( - "flag" "fmt" "os" + "path/filepath" "reflect" "regexp" "runtime" + "strconv" "strings" "github.com/DATA-DOG/godog/gherkin" @@ -76,6 +77,14 @@ type suite struct { failed bool + // options + paths []string + format string + tags string + definitions bool + stopOnFailure bool + version bool + // suite event handlers beforeSuiteHandlers []func() beforeScenarioHandlers []func(*gherkin.Scenario) @@ -175,27 +184,43 @@ func (s *suite) AfterSuite(f func()) { // Run starts the Godog feature suite func (s *suite) Run() { - var err error - if !flag.Parsed() { - flag.Parse() + 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 cfg.version: + case s.version: fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow)) return - case cfg.definitions: + case s.definitions: s.printStepDefinitions() return } + fatal(s.parseFeatures()) // run a feature suite - fatal(cfg.validate()) - s.fmt = cfg.formatter() - s.features, err = cfg.features() - fatal(err) - s.run() if s.failed { @@ -211,7 +236,7 @@ func (s *suite) run() { // run features for _, f := range s.features { s.runFeature(f) - if s.failed && cfg.stopOnFailure { + if s.failed && s.stopOnFailure { // stop on first failure break } @@ -320,7 +345,7 @@ func (s *suite) runOutline(scenario *gherkin.Scenario) (err error) { scenario.Steps = steps if err = s.runScenario(scenario); err != nil && err != ErrUndefined { s.failed = true - if cfg.stopOnFailure { + if s.stopOnFailure { return } } @@ -340,7 +365,7 @@ func (s *suite) runFeature(f *gherkin.Feature) { } if err != nil && err != ErrUndefined { s.failed = true - if cfg.stopOnFailure { + if s.stopOnFailure { return } } @@ -394,3 +419,78 @@ func (s *suite) printStepDefinitions() { fmt.Println("there were no contexts registered, could not find any step definition..") } } + +func (s *suite) parseFeatures() (err error) { + for _, pat := range s.paths { + // check if line number is specified + parts := strings.Split(pat, ":") + path := parts[0] + line := -1 + if len(parts) > 1 { + line, err = strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("line number should follow after colon path delimiter") + } + } + // parse features + err = filepath.Walk(path, func(p string, f os.FileInfo, err error) error { + if err == nil && !f.IsDir() && strings.HasSuffix(p, ".feature") { + ft, err := gherkin.ParseFile(p) + switch { + case err == gherkin.ErrEmpty: + // its ok, just skip it + case err != nil: + return err + default: + s.features = append(s.features, ft) + } + // filter scenario by line number + if line != -1 { + var scenarios []*gherkin.Scenario + for _, s := range ft.Scenarios { + if s.Token.Line == line { + scenarios = append(scenarios, s) + break + } + } + ft.Scenarios = scenarios + } + s.applyTagFilter(ft) + } + return err + }) + // check error + switch { + case os.IsNotExist(err): + return fmt.Errorf(`feature path "%s" is not available`, path) + case os.IsPermission(err): + return fmt.Errorf(`feature path "%s" is not accessible`, path) + case err != nil: + return err + } + } + return +} + +func (s *suite) applyTagFilter(ft *gherkin.Feature) { + if len(s.tags) == 0 { + return + } + + for _, tag := range strings.Split(s.tags, " ") { + var scenarios []*gherkin.Scenario + var inverse bool + if tag[0] == '!' { + tag = tag[1:] + inverse = true + } + for _, scenario := range ft.Scenarios { + if inverse && !scenario.Tags.Has(gherkin.Tag(tag)) { + scenarios = append(scenarios, scenario) + } else if !inverse && scenario.Tags.Has(gherkin.Tag(tag)) { + scenarios = append(scenarios, scenario) + } + } + ft.Scenarios = scenarios + } +} diff --git a/suite_test.go b/suite_test.go index 18e3194..8c5efa1 100644 --- a/suite_test.go +++ b/suite_test.go @@ -47,8 +47,6 @@ func (s *suiteContext) HandleBeforeScenario(*gherkin.Scenario) { s.testedSuite = &suite{fmt: s.fmt} // our tested suite will have the same context registered SuiteContext(s.testedSuite) - // reset feature paths - cfg.paths = []string{} // reset all fired events s.events = []*firedEvent{} } @@ -147,17 +145,12 @@ func (s *suiteContext) aFeatureFile(args ...*Arg) error { } func (s *suiteContext) featurePath(args ...*Arg) error { - cfg.paths = append(cfg.paths, args[0].String()) + s.testedSuite.paths = append(s.testedSuite.paths, args[0].String()) return nil } func (s *suiteContext) parseFeatures(args ...*Arg) error { - features, err := cfg.features() - if err != nil { - return err - } - s.testedSuite.features = append(s.testedSuite.features, features...) - return nil + return s.testedSuite.parseFeatures() } func (s *suiteContext) theSuiteShouldHave(args ...*Arg) error {