diff --git a/config.go b/config.go index e6e16d9..1bf5842 100644 --- a/config.go +++ b/config.go @@ -5,55 +5,95 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "github.com/DATA-DOG/godog/gherkin" ) type registeredFormatter struct { - name string - fmt Formatter + name string + fmt Formatter + description string } var formatters []*registeredFormatter // RegisterFormatter registers a feature suite output -// Formatter for its given name -func RegisterFormatter(name string, f Formatter) { +// 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, + name: name, + fmt: f, + description: description, }) } -var cfg config +var cfg *config + +func s(n int) string { + return strings.Repeat(" ", n) +} func init() { - // @TODO: colorize flag help output - flag.StringVar(&cfg.featuresPath, "features", "features", "Path to feature files") - flag.StringVar(&cfg.formatterName, "formatter", "pretty", "Formatter name") + cfg = &config{} + + flag.StringVar(&cfg.format, "format", "pretty", "") + flag.StringVar(&cfg.format, "f", "pretty", "") + 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)) + // --> 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)) + } + fmt.Println("") + } } type config struct { - featuresPath string - formatterName string + paths []string + format string } -func (c config) validate() error { - // feature path - inf, err := os.Stat(c.featuresPath) - if err != nil { - return err +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"} + } } - if !inf.IsDir() { - return fmt.Errorf("feature path \"%s\" is not a directory.", c.featuresPath) - } - // formatter var found bool var names []string for _, f := range formatters { - if f.name == c.formatterName { + if f.name == c.format { found = true break } @@ -61,26 +101,61 @@ func (c config) validate() error { } if !found { - return fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, c.formatterName, strings.Join(names, ", ")) + 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) { - return lst, filepath.Walk(cfg.featuresPath, func(p string, f os.FileInfo, err error) error { - if err == nil && !f.IsDir() && strings.HasSuffix(p, ".feature") { - ft, err := gherkin.Parse(p) +func (c *config) features() (lst []*gherkin.Feature, err error) { + for _, pat := range c.paths { + parts := strings.Split(pat, ":") + path := parts[0] + line := -1 + if len(parts) > 1 { + line, err = strconv.Atoi(parts[1]) if err != nil { - return err + return lst, fmt.Errorf("line number should follow after colon path delimiter") } - lst = append(lst, ft) } - return err - }) + 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.Parse(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 + }) + if err != nil { + return + } + } + return } -func (c config) formatter() Formatter { - return &pretty{} +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") } func fatal(err error) { diff --git a/formatter.go b/formatter.go index 6990849..4253f79 100644 --- a/formatter.go +++ b/formatter.go @@ -6,24 +6,29 @@ import ( "reflect" "runtime" "strings" + "time" "github.com/DATA-DOG/godog/gherkin" ) func init() { - RegisterFormatter("pretty", &pretty{}) + RegisterFormatter("pretty", "Prints every feature with runtime statuses.", &pretty{ + started: time.Now(), + }) } -// Formatter is an interface for feature runner output +// Formatter is an interface for feature runner +// output summary presentation type Formatter interface { Node(interface{}) Failed(*gherkin.Step, *stepMatchHandler, error) Passed(*gherkin.Step, *stepMatchHandler) Skipped(*gherkin.Step) - Pending(*gherkin.Step) + Undefined(*gherkin.Step) Summary() } +// general pretty formatter structure type pretty struct { feature *gherkin.Feature scenario *gherkin.Scenario @@ -32,13 +37,16 @@ type pretty struct { background *gherkin.Background // summary - features []*gherkin.Feature - failures []*failed - passes []*passed - skips []*skipped - pendings []*pending + started time.Time + features []*gherkin.Feature + failed []*failed + passed []*passed + skipped []*skipped + undefined []*undefined } +// failed represents a failed step data structure +// with all necessary references type failed struct { feature *gherkin.Feature scenario *gherkin.Scenario @@ -46,28 +54,36 @@ type failed struct { err error } +// passed represents a successful step data structure +// with all necessary references type passed struct { feature *gherkin.Feature scenario *gherkin.Scenario step *gherkin.Step } +// skipped represents a skipped step data structure +// with all necessary references type skipped struct { feature *gherkin.Feature scenario *gherkin.Scenario step *gherkin.Step } -type pending struct { +// undefined represents a pending step data structure +// with all necessary references +type undefined struct { feature *gherkin.Feature scenario *gherkin.Scenario step *gherkin.Step } +// a line number representation in feature file func (f *pretty) line(tok *gherkin.Token) string { return cl(fmt.Sprintf("# %s:%d", f.feature.Path, tok.Line), black) } +// checks whether it should not print a background step once again func (f *pretty) canPrintStep(step *gherkin.Step) bool { if f.background == nil { return true @@ -89,11 +105,7 @@ func (f *pretty) canPrintStep(step *gherkin.Step) bool { return !f.doneBackground } -func (f *pretty) comment(text, comment string) string { - indent := f.commentPos - len(text) + 1 - return text + strings.Repeat(" ", indent) + comment -} - +// Node takes a gherkin node for formatting func (f *pretty) Node(node interface{}) { switch t := node.(type) { case *gherkin.Feature: @@ -102,11 +114,11 @@ func (f *pretty) Node(node interface{}) { f.scenario = nil f.background = nil f.features = append(f.features, t) - fmt.Println("\n" + bcl("Feature: ", white) + t.Title) + fmt.Println(bcl("Feature: ", white) + t.Title + "\n") fmt.Println(t.Description) case *gherkin.Background: f.background = t - fmt.Println("\n" + bcl("Background:", white)) + fmt.Println(bcl("Background:", white) + "\n") case *gherkin.Scenario: f.scenario = t f.commentPos = len(t.Token.Text) @@ -117,14 +129,64 @@ func (f *pretty) Node(node interface{}) { } text := strings.Repeat(" ", t.Token.Indent) + bcl("Scenario: ", white) + t.Title text += strings.Repeat(" ", f.commentPos-len(t.Token.Text)+1) + f.line(t.Token) - fmt.Println("\n" + text) + fmt.Println(text + "\n") } } +// Summary sumarize the feature formatter output func (f *pretty) Summary() { + if len(f.failed) > 0 { + fmt.Println("\n--- " + cl("Failed scenarios:", red) + "\n") + for _, fail := range f.failed { + fmt.Println(" " + cl(fmt.Sprintf("%s:%d", fail.feature.Path, fail.scenario.Token.Line), red)) + } + } + var total, passed int + for _, ft := range f.features { + total += len(ft.Scenarios) + } + passed = total + var steps, parts, scenarios []string + nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + if len(f.passed) > 0 { + steps = append(steps, cl(fmt.Sprintf("%d passed", len(f.passed)), green)) + } + if len(f.failed) > 0 { + passed -= len(f.failed) + parts = append(parts, cl(fmt.Sprintf("%d failed", len(f.failed)), red)) + steps = append(steps, parts[len(parts)-1]) + } + if len(f.skipped) > 0 { + steps = append(steps, cl(fmt.Sprintf("%d skipped", len(f.skipped)), cyan)) + } + if len(f.undefined) > 0 { + passed -= len(f.undefined) + parts = append(parts, cl(fmt.Sprintf("%d undefined", len(f.undefined)), yellow)) + steps = append(steps, parts[len(parts)-1]) + } + if passed > 0 { + scenarios = append(scenarios, cl(fmt.Sprintf("%d passed", passed), green)) + } + scenarios = append(scenarios, parts...) + elapsed := time.Since(f.started) + + fmt.Println("") + if total == 0 { + fmt.Println("No scenarios") + } else { + fmt.Println(fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", "))) + } + + if nsteps == 0 { + fmt.Println("No steps") + } else { + fmt.Println(fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", "))) + } + fmt.Println(elapsed) } +// prints a single matched step func (f *pretty) printMatchedStep(step *gherkin.Step, match *stepMatchHandler, c color) { if !f.canPrintStep(step) { return @@ -165,41 +227,45 @@ func (f *pretty) printMatchedStep(step *gherkin.Step, match *stepMatchHandler, c fmt.Println(text) } +// Passed is called to represent a passed step func (f *pretty) Passed(step *gherkin.Step, match *stepMatchHandler) { f.printMatchedStep(step, match, green) - f.passes = append(f.passes, &passed{ + f.passed = append(f.passed, &passed{ feature: f.feature, scenario: f.scenario, step: step, }) } +// Skipped is called to represent a passed step func (f *pretty) Skipped(step *gherkin.Step) { if f.canPrintStep(step) { fmt.Println(cl(step.Token.Text, cyan)) } - f.skips = append(f.skips, &skipped{ + f.skipped = append(f.skipped, &skipped{ feature: f.feature, scenario: f.scenario, step: step, }) } -func (f *pretty) Pending(step *gherkin.Step) { +// Undefined is called to represent a pending step +func (f *pretty) Undefined(step *gherkin.Step) { if f.canPrintStep(step) { fmt.Println(cl(step.Token.Text, yellow)) } - f.pendings = append(f.pendings, &pending{ + f.undefined = append(f.undefined, &undefined{ feature: f.feature, scenario: f.scenario, step: step, }) } +// Failed is called to represent a failed step func (f *pretty) Failed(step *gherkin.Step, match *stepMatchHandler, err error) { f.printMatchedStep(step, match, red) fmt.Println(strings.Repeat(" ", step.Token.Indent) + bcl(err, red)) - f.failures = append(f.failures, &failed{ + f.failed = append(f.failed, &failed{ feature: f.feature, scenario: f.scenario, step: step, diff --git a/suite.go b/suite.go index 39a8424..5d721f5 100644 --- a/suite.go +++ b/suite.go @@ -119,12 +119,10 @@ func (s *suite) Run() { s.features, err = cfg.features() fatal(err) - fmt.Println("running", cl("godog", cyan)+", num registered steps:", cl(len(s.steps), yellow)) - fmt.Println("have loaded", cl(len(s.features), yellow), "features from path:", cl(cfg.featuresPath, green)) - for _, f := range s.features { s.runFeature(f) } + s.fmt.Summary() } func (s *suite) runStep(step *gherkin.Step) (err error) { @@ -140,7 +138,7 @@ func (s *suite) runStep(step *gherkin.Step) (err error) { } } if match == nil { - s.fmt.Pending(step) + s.fmt.Undefined(step) return errPending } @@ -184,7 +182,6 @@ func (s *suite) runFeature(f *gherkin.Feature) { var failed bool for _, scenario := range f.Scenarios { // background - // @TODO: do not print more than once if f.Background != nil && !failed { s.fmt.Node(f.Background) failed = s.runSteps(f.Background.Steps) diff --git a/suite_test.go b/suite_test.go index d8f8edf..69df56d 100644 --- a/suite_test.go +++ b/suite_test.go @@ -10,7 +10,7 @@ type suiteFeature struct { } func (s *suiteFeature) featurePath(args ...Arg) error { - cfg.featuresPath = args[0].String() + cfg.paths = []string{args[0].String()} return nil }