diff --git a/feature.go b/feature.go new file mode 100644 index 00000000..ccb73024 --- /dev/null +++ b/feature.go @@ -0,0 +1,96 @@ +package godog + +import ( + "time" + + "github.com/cucumber/messages-go/v10" +) + +type feature struct { + *messages.GherkinDocument + pickles []*messages.Pickle + + time time.Time + content []byte + order int +} + +type sortFeaturesByName []*feature + +func (s sortFeaturesByName) Len() int { return len(s) } +func (s sortFeaturesByName) Less(i, j int) bool { return s[i].Feature.Name < s[j].Feature.Name } +func (s sortFeaturesByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type sortFeaturesByOrder []*feature + +func (s sortFeaturesByOrder) Len() int { return len(s) } +func (s sortFeaturesByOrder) Less(i, j int) bool { return s[i].order < s[j].order } +func (s sortFeaturesByOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (f feature) findScenario(astScenarioID string) *messages.GherkinDocument_Feature_Scenario { + for _, child := range f.GherkinDocument.Feature.Children { + if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID { + return sc + } + } + + return nil +} + +func (f feature) findBackground(astScenarioID string) *messages.GherkinDocument_Feature_Background { + var bg *messages.GherkinDocument_Feature_Background + + for _, child := range f.GherkinDocument.Feature.Children { + if tmp := child.GetBackground(); tmp != nil { + bg = tmp + } + + if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID { + return bg + } + } + + return nil +} + +func (f feature) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) { + for _, child := range f.GherkinDocument.Feature.Children { + if sc := child.GetScenario(); sc != nil { + for _, example := range sc.Examples { + for _, row := range example.TableBody { + if row.Id == exampleAstID { + return example, row + } + } + } + } + } + + return nil, nil +} + +func (f feature) findStep(astStepID string) *messages.GherkinDocument_Feature_Step { + for _, child := range f.GherkinDocument.Feature.Children { + if sc := child.GetScenario(); sc != nil { + for _, step := range sc.GetSteps() { + if step.Id == astStepID { + return step + } + } + } + + if bg := child.GetBackground(); bg != nil { + for _, step := range bg.GetSteps() { + if step.Id == astStepID { + return step + } + } + } + } + + return nil +} + +func (f feature) startedAt() time.Time { + return f.time +} diff --git a/fmt.go b/fmt.go index 7a149060..416dbac9 100644 --- a/fmt.go +++ b/fmt.go @@ -1,20 +1,12 @@ package godog import ( - "bytes" "fmt" "io" - "os" - "sort" - "strconv" "strings" - "sync" - "time" - "unicode" + "unicode/utf8" "github.com/cucumber/messages-go/v10" - - "github.com/cucumber/godog/colors" ) type registeredFormatter struct { @@ -99,301 +91,27 @@ type storageFormatter interface { // suite name and io.Writer to record output type FormatterFunc func(string, io.Writer) Formatter -type stepResultStatus int - -const ( - passed stepResultStatus = iota - failed - skipped - undefined - pending -) - -func (st stepResultStatus) clr() colors.ColorFunc { - switch st { - case passed: - return green - case failed: - return red - case skipped: - return cyan - default: - return yellow - } -} - -func (st stepResultStatus) String() string { - switch st { - case passed: - return "passed" - case failed: - return "failed" - case skipped: - return "skipped" - case undefined: - return "undefined" - case pending: - return "pending" - default: - return "unknown" - } -} - -type pickleStepResult struct { - Status stepResultStatus - finishedAt time.Time - err error - - PickleID string - PickleStepID string - - def *StepDefinition -} - -func newStepResult(pickleID, pickleStepID string, match *StepDefinition) pickleStepResult { - return pickleStepResult{finishedAt: timeNowFunc(), PickleID: pickleID, PickleStepID: pickleStepID, def: match} -} - -func newBaseFmt(suite string, out io.Writer) *basefmt { - return &basefmt{ - suiteName: suite, - startedAt: timeNowFunc(), - indent: 2, - out: out, - lock: new(sync.Mutex), - } -} - -type basefmt struct { - suiteName string - - out io.Writer - owner interface{} - indent int - - storage *storage - - startedAt time.Time - - firstFeature *bool - lock *sync.Mutex -} - -func (f *basefmt) setStorage(st *storage) { - f.storage = st -} - -func (f *basefmt) TestRunStarted() { - f.lock.Lock() - defer f.lock.Unlock() - - firstFeature := true - f.firstFeature = &firstFeature -} - -func (f *basefmt) Pickle(p *messages.Pickle) {} - -func (f *basefmt) Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) {} - -func (f *basefmt) Feature(ft *messages.GherkinDocument, p string, c []byte) { - f.lock.Lock() - defer f.lock.Unlock() - - *f.firstFeature = false -} - -func (f *basefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { -} -func (f *basefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { -} -func (f *basefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { -} -func (f *basefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) { -} -func (f *basefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { +func isLastStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep) bool { + return pickle.Steps[len(pickle.Steps)-1].Id == step.Id } -func (f *basefmt) Summary() { - var totalSc, passedSc, undefinedSc int - var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt int - - pickleResults := f.storage.mustGetPickleResults() - for _, pr := range pickleResults { - var prStatus stepResultStatus - totalSc++ - - pickleStepResults := f.storage.mustGetPickleStepResultsByPickleID(pr.PickleID) - - if len(pickleStepResults) == 0 { - prStatus = undefined - } - - for _, sr := range pickleStepResults { - totalSt++ - - switch sr.Status { - case passed: - prStatus = passed - passedSt++ - case failed: - prStatus = failed - failedSt++ - case skipped: - skippedSt++ - case undefined: - prStatus = undefined - undefinedSt++ - case pending: - prStatus = pending - pendingSt++ - } +func printStepDefinitions(steps []*StepDefinition, w io.Writer) { + var longest int + for _, def := range steps { + n := utf8.RuneCountInString(def.Expr.String()) + if longest < n { + longest = n } - - if prStatus == passed { - passedSc++ - } else if prStatus == undefined { - undefinedSc++ - } - } - - var steps, parts, scenarios []string - if passedSt > 0 { - steps = append(steps, green(fmt.Sprintf("%d passed", passedSt))) - } - if failedSt > 0 { - parts = append(parts, red(fmt.Sprintf("%d failed", failedSt))) - steps = append(steps, red(fmt.Sprintf("%d failed", failedSt))) - } - if pendingSt > 0 { - parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt))) - steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt))) - } - if undefinedSt > 0 { - parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) - steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt))) - } else if undefinedSc > 0 { - // there may be some scenarios without steps - parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) - } - if skippedSt > 0 { - steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt))) - } - if passedSc > 0 { - scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc))) - } - scenarios = append(scenarios, parts...) - elapsed := timeNowFunc().Sub(f.startedAt) - - fmt.Fprintln(f.out, "") - - if totalSc == 0 { - fmt.Fprintln(f.out, "No scenarios") - } else { - fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", totalSc, strings.Join(scenarios, ", "))) - } - - if totalSt == 0 { - fmt.Fprintln(f.out, "No steps") - } else { - fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", totalSt, strings.Join(steps, ", "))) - } - - elapsedString := elapsed.String() - if elapsed.Nanoseconds() == 0 { - // go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero. - elapsedString = "0s" } - fmt.Fprintln(f.out, elapsedString) - // prints used randomization seed - seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) - if err == nil && seed != 0 { - fmt.Fprintln(f.out, "") - fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) + for _, def := range steps { + n := utf8.RuneCountInString(def.Expr.String()) + location := def.definitionID() + spaces := strings.Repeat(" ", longest-n) + fmt.Fprintln(w, yellow(def.Expr.String())+spaces, blackb("# "+location)) } - if text := f.snippets(); text != "" { - fmt.Fprintln(f.out, "") - fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:")) - fmt.Fprintln(f.out, yellow(text)) + if len(steps) == 0 { + fmt.Fprintln(w, "there were no contexts registered, could not find any step definition..") } } - -func (f *basefmt) Sync(cf ConcurrentFormatter) { - if source, ok := cf.(*basefmt); ok { - f.lock = source.lock - f.firstFeature = source.firstFeature - } -} - -func (f *basefmt) Copy(cf ConcurrentFormatter) {} - -func (f *basefmt) snippets() string { - undefinedStepResults := f.storage.mustGetPickleStepResultsByStatus(undefined) - if len(undefinedStepResults) == 0 { - return "" - } - - var index int - var snips []undefinedSnippet - // build snippets - for _, u := range undefinedStepResults { - pickleStep := f.storage.mustGetPickleStep(u.PickleStepID) - - steps := []string{pickleStep.Text} - arg := pickleStep.Argument - if u.def != nil { - steps = u.def.undefined - arg = nil - } - for _, step := range steps { - expr := snippetExprCleanup.ReplaceAllString(step, "\\$1") - expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") - expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") - expr = "^" + strings.TrimSpace(expr) + "$" - - name := snippetNumbers.ReplaceAllString(step, " ") - name = snippetExprQuoted.ReplaceAllString(name, " ") - name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, "")) - var words []string - for i, w := range strings.Split(name, " ") { - switch { - case i != 0: - w = strings.Title(w) - case len(w) > 0: - w = string(unicode.ToLower(rune(w[0]))) + w[1:] - } - words = append(words, w) - } - name = strings.Join(words, "") - if len(name) == 0 { - index++ - name = fmt.Sprintf("StepDefinitioninition%d", index) - } - - var found bool - for _, snip := range snips { - if snip.Expr == expr { - found = true - break - } - } - if !found { - snips = append(snips, undefinedSnippet{Method: name, Expr: expr, argument: arg}) - } - } - } - - sort.Sort(snippetSortByMethod(snips)) - - var buf bytes.Buffer - if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { - panic(err) - } - // there may be trailing spaces - return strings.Replace(buf.String(), " \n", "\n", -1) -} - -func isLastStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep) bool { - return pickle.Steps[len(pickle.Steps)-1].Id == step.Id -} diff --git a/fmt_base.go b/fmt_base.go new file mode 100644 index 00000000..7b81cd78 --- /dev/null +++ b/fmt_base.go @@ -0,0 +1,258 @@ +package godog + +import ( + "bytes" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + "unicode" + + "github.com/cucumber/messages-go/v10" + + "github.com/cucumber/godog/colors" +) + +func newBaseFmt(suite string, out io.Writer) *basefmt { + return &basefmt{ + suiteName: suite, + startedAt: timeNowFunc(), + indent: 2, + out: out, + lock: new(sync.Mutex), + } +} + +type basefmt struct { + suiteName string + + out io.Writer + owner interface{} + indent int + + storage *storage + + startedAt time.Time + + firstFeature *bool + lock *sync.Mutex +} + +func (f *basefmt) setStorage(st *storage) { + f.storage = st +} + +func (f *basefmt) TestRunStarted() { + f.lock.Lock() + defer f.lock.Unlock() + + firstFeature := true + f.firstFeature = &firstFeature +} + +func (f *basefmt) Pickle(p *messages.Pickle) {} + +func (f *basefmt) Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) {} + +func (f *basefmt) Feature(ft *messages.GherkinDocument, p string, c []byte) { + f.lock.Lock() + defer f.lock.Unlock() + + *f.firstFeature = false +} + +func (f *basefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { +} +func (f *basefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { +} +func (f *basefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { +} +func (f *basefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) { +} +func (f *basefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { +} + +func (f *basefmt) Summary() { + var totalSc, passedSc, undefinedSc int + var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt int + + pickleResults := f.storage.mustGetPickleResults() + for _, pr := range pickleResults { + var prStatus stepResultStatus + totalSc++ + + pickleStepResults := f.storage.mustGetPickleStepResultsByPickleID(pr.PickleID) + + if len(pickleStepResults) == 0 { + prStatus = undefined + } + + for _, sr := range pickleStepResults { + totalSt++ + + switch sr.Status { + case passed: + prStatus = passed + passedSt++ + case failed: + prStatus = failed + failedSt++ + case skipped: + skippedSt++ + case undefined: + prStatus = undefined + undefinedSt++ + case pending: + prStatus = pending + pendingSt++ + } + } + + if prStatus == passed { + passedSc++ + } else if prStatus == undefined { + undefinedSc++ + } + } + + var steps, parts, scenarios []string + if passedSt > 0 { + steps = append(steps, green(fmt.Sprintf("%d passed", passedSt))) + } + if failedSt > 0 { + parts = append(parts, red(fmt.Sprintf("%d failed", failedSt))) + steps = append(steps, red(fmt.Sprintf("%d failed", failedSt))) + } + if pendingSt > 0 { + parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt))) + steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt))) + } + if undefinedSt > 0 { + parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) + steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt))) + } else if undefinedSc > 0 { + // there may be some scenarios without steps + parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) + } + if skippedSt > 0 { + steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt))) + } + if passedSc > 0 { + scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc))) + } + scenarios = append(scenarios, parts...) + elapsed := timeNowFunc().Sub(f.startedAt) + + fmt.Fprintln(f.out, "") + + if totalSc == 0 { + fmt.Fprintln(f.out, "No scenarios") + } else { + fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", totalSc, strings.Join(scenarios, ", "))) + } + + if totalSt == 0 { + fmt.Fprintln(f.out, "No steps") + } else { + fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", totalSt, strings.Join(steps, ", "))) + } + + elapsedString := elapsed.String() + if elapsed.Nanoseconds() == 0 { + // go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero. + elapsedString = "0s" + } + fmt.Fprintln(f.out, elapsedString) + + // prints used randomization seed + seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) + if err == nil && seed != 0 { + fmt.Fprintln(f.out, "") + fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) + } + + if text := f.snippets(); text != "" { + fmt.Fprintln(f.out, "") + fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:")) + fmt.Fprintln(f.out, yellow(text)) + } +} + +func (f *basefmt) Sync(cf ConcurrentFormatter) { + if source, ok := cf.(*basefmt); ok { + f.lock = source.lock + f.firstFeature = source.firstFeature + } +} + +func (f *basefmt) Copy(cf ConcurrentFormatter) {} + +func (f *basefmt) snippets() string { + undefinedStepResults := f.storage.mustGetPickleStepResultsByStatus(undefined) + if len(undefinedStepResults) == 0 { + return "" + } + + var index int + var snips []undefinedSnippet + // build snippets + for _, u := range undefinedStepResults { + pickleStep := f.storage.mustGetPickleStep(u.PickleStepID) + + steps := []string{pickleStep.Text} + arg := pickleStep.Argument + if u.def != nil { + steps = u.def.undefined + arg = nil + } + for _, step := range steps { + expr := snippetExprCleanup.ReplaceAllString(step, "\\$1") + expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") + expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") + expr = "^" + strings.TrimSpace(expr) + "$" + + name := snippetNumbers.ReplaceAllString(step, " ") + name = snippetExprQuoted.ReplaceAllString(name, " ") + name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, "")) + var words []string + for i, w := range strings.Split(name, " ") { + switch { + case i != 0: + w = strings.Title(w) + case len(w) > 0: + w = string(unicode.ToLower(rune(w[0]))) + w[1:] + } + words = append(words, w) + } + name = strings.Join(words, "") + if len(name) == 0 { + index++ + name = fmt.Sprintf("StepDefinitioninition%d", index) + } + + var found bool + for _, snip := range snips { + if snip.Expr == expr { + found = true + break + } + } + if !found { + snips = append(snips, undefinedSnippet{Method: name, Expr: expr, argument: arg}) + } + } + } + + sort.Sort(snippetSortByMethod(snips)) + + var buf bytes.Buffer + if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { + panic(err) + } + // there may be trailing spaces + return strings.Replace(buf.String(), " \n", "\n", -1) +} diff --git a/parser.go b/parser.go new file mode 100644 index 00000000..3c184019 --- /dev/null +++ b/parser.go @@ -0,0 +1,169 @@ +package godog + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/cucumber/gherkin-go/v11" + "github.com/cucumber/messages-go/v10" +) + +var pathLineRe = regexp.MustCompile(`:([\d]+)$`) + +func extractFeaturePathLine(p string) (string, int) { + line := -1 + retPath := p + if m := pathLineRe.FindStringSubmatch(p); len(m) > 0 { + if i, err := strconv.Atoi(m[1]); err == nil { + line = i + retPath = p[:strings.LastIndexByte(p, ':')] + } + } + return retPath, line +} + +func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) { + reader, err := os.Open(path) + if err != nil { + return nil, err + } + + defer reader.Close() + + var buf bytes.Buffer + gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc) + if err != nil { + return nil, fmt.Errorf("%s - %v", path, err) + } + + gherkinDocument.Uri = path + pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc) + + f := feature{GherkinDocument: gherkinDocument, pickles: pickles, content: buf.Bytes()} + return &f, nil +} + +func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) { + var features []*feature + return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + if f.IsDir() { + return nil + } + + if !strings.HasSuffix(p, ".feature") { + return nil + } + + feat, err := parseFeatureFile(p, newIDFunc) + if err != nil { + return err + } + + features = append(features, feat) + return nil + }) +} + +func parsePath(path string) ([]*feature, error) { + var features []*feature + + path, line := extractFeaturePathLine(path) + + fi, err := os.Stat(path) + if err != nil { + return features, err + } + + newIDFunc := (&messages.Incrementing{}).NewId + + if fi.IsDir() { + return parseFeatureDir(path, newIDFunc) + } + + ft, err := parseFeatureFile(path, newIDFunc) + if err != nil { + return features, err + } + + // filter scenario by line number + var pickles []*messages.Pickle + for _, pickle := range ft.pickles { + sc := ft.findScenario(pickle.AstNodeIds[0]) + + if line == -1 || uint32(line) == sc.Location.Line { + pickles = append(pickles, pickle) + } + } + ft.pickles = pickles + + return append(features, ft), nil +} + +func parseFeatures(filter string, paths []string) ([]*feature, error) { + var order int + + uniqueFeatureURI := make(map[string]*feature) + for _, path := range paths { + feats, err := parsePath(path) + + switch { + case os.IsNotExist(err): + return nil, fmt.Errorf(`feature path "%s" is not available`, path) + case os.IsPermission(err): + return nil, fmt.Errorf(`feature path "%s" is not accessible`, path) + case err != nil: + return nil, err + } + + for _, ft := range feats { + if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate { + continue + } + + ft.order = order + order++ + uniqueFeatureURI[ft.Uri] = ft + } + } + + return filterFeatures(filter, uniqueFeatureURI), nil +} + +func filterFeatures(tags string, collected map[string]*feature) (features []*feature) { + for _, ft := range collected { + ft.pickles = applyTagFilter(tags, ft.pickles) + + if ft.Feature != nil { + features = append(features, ft) + } + } + + sort.Sort(sortFeaturesByOrder(features)) + + return features +} + +func applyTagFilter(tags string, pickles []*messages.Pickle) (result []*messages.Pickle) { + if len(tags) == 0 { + return pickles + } + + for _, pickle := range pickles { + if matchesTags(tags, pickle.Tags) { + result = append(result, pickle) + } + } + + return +} diff --git a/results.go b/results.go new file mode 100644 index 00000000..0f916921 --- /dev/null +++ b/results.go @@ -0,0 +1,77 @@ +package godog + +import ( + "time" + + "github.com/cucumber/godog/colors" +) + +type pickleResult struct { + PickleID string + StartedAt time.Time +} + +type pickleStepResult struct { + Status stepResultStatus + finishedAt time.Time + err error + + PickleID string + PickleStepID string + + def *StepDefinition +} + +func newStepResult(pickleID, pickleStepID string, match *StepDefinition) pickleStepResult { + return pickleStepResult{finishedAt: timeNowFunc(), PickleID: pickleID, PickleStepID: pickleStepID, def: match} +} + +type sortPickleStepResultsByPickleStepID []pickleStepResult + +func (s sortPickleStepResultsByPickleStepID) Len() int { return len(s) } +func (s sortPickleStepResultsByPickleStepID) Less(i, j int) bool { + iID := mustConvertStringToInt(s[i].PickleStepID) + jID := mustConvertStringToInt(s[j].PickleStepID) + return iID < jID +} +func (s sortPickleStepResultsByPickleStepID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type stepResultStatus int + +const ( + passed stepResultStatus = iota + failed + skipped + undefined + pending +) + +func (st stepResultStatus) clr() colors.ColorFunc { + switch st { + case passed: + return green + case failed: + return red + case skipped: + return cyan + default: + return yellow + } +} + +func (st stepResultStatus) String() string { + switch st { + case passed: + return "passed" + case failed: + return "failed" + case skipped: + return "skipped" + case undefined: + return "undefined" + case pending: + return "pending" + default: + return "unknown" + } +} diff --git a/run.go b/run.go index c058af9e..b41ec6d4 100644 --- a/run.go +++ b/run.go @@ -199,7 +199,7 @@ func runWithOptions(suite string, runner runner, opt Options) int { if opt.ShowStepDefinitions { s := &Suite{} runner.initializer(s) - s.printStepDefinitions(output) + printStepDefinitions(s.steps, output) return exitOptionError } diff --git a/run_test.go b/run_test.go index 08b13fa8..f68112b3 100644 --- a/run_test.go +++ b/run_test.go @@ -34,7 +34,8 @@ func TestPrintsStepDefinitions(t *testing.T) { for _, step := range steps { s.Step(step, okStep) } - s.printStepDefinitions(w) + + printStepDefinitions(s.steps, w) out := buf.String() ref := `okStep` @@ -52,7 +53,8 @@ func TestPrintsNoStepDefinitionsIfNoneFound(t *testing.T) { var buf bytes.Buffer w := colors.Uncolored(&buf) s := &Suite{} - s.printStepDefinitions(w) + + printStepDefinitions(s.steps, w) out := strings.TrimSpace(buf.String()) assert.Equal(t, "there were no contexts registered, could not find any step definition..", out) diff --git a/suite.go b/suite.go index c483bda2..6da0df63 100644 --- a/suite.go +++ b/suite.go @@ -1,143 +1,17 @@ package godog import ( - "bytes" "fmt" - "io" - "os" - "path/filepath" "reflect" "regexp" - "sort" - "strconv" "strings" - "time" - "unicode/utf8" - "github.com/cucumber/gherkin-go/v11" "github.com/cucumber/messages-go/v10" ) var errorInterface = reflect.TypeOf((*error)(nil)).Elem() var typeOfBytes = reflect.TypeOf([]byte(nil)) -type feature struct { - *messages.GherkinDocument - pickles []*messages.Pickle - - time time.Time - content []byte - order int -} - -func (f feature) findScenario(astScenarioID string) *messages.GherkinDocument_Feature_Scenario { - for _, child := range f.GherkinDocument.Feature.Children { - if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID { - return sc - } - } - - return nil -} - -func (f feature) findBackground(astScenarioID string) *messages.GherkinDocument_Feature_Background { - var bg *messages.GherkinDocument_Feature_Background - - for _, child := range f.GherkinDocument.Feature.Children { - if tmp := child.GetBackground(); tmp != nil { - bg = tmp - } - - if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID { - return bg - } - } - - return nil -} - -func (f feature) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) { - for _, child := range f.GherkinDocument.Feature.Children { - if sc := child.GetScenario(); sc != nil { - for _, example := range sc.Examples { - for _, row := range example.TableBody { - if row.Id == exampleAstID { - return example, row - } - } - } - } - } - - return nil, nil -} - -func (f feature) findStep(astStepID string) *messages.GherkinDocument_Feature_Step { - for _, child := range f.GherkinDocument.Feature.Children { - if sc := child.GetScenario(); sc != nil { - for _, step := range sc.GetSteps() { - if step.Id == astStepID { - return step - } - } - } - - if bg := child.GetBackground(); bg != nil { - for _, step := range bg.GetSteps() { - if step.Id == astStepID { - return step - } - } - } - } - - return nil -} - -func (f feature) startedAt() time.Time { - return f.time -} - -type sortFeaturesByName []*feature - -func (s sortFeaturesByName) Len() int { return len(s) } -func (s sortFeaturesByName) Less(i, j int) bool { return s[i].Feature.Name < s[j].Feature.Name } -func (s sortFeaturesByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -type sortPicklesByID []*messages.Pickle - -func (s sortPicklesByID) Len() int { return len(s) } -func (s sortPicklesByID) Less(i, j int) bool { - iID := mustConvertStringToInt(s[i].Id) - jID := mustConvertStringToInt(s[j].Id) - return iID < jID -} -func (s sortPicklesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -type sortPickleStepResultsByPickleStepID []pickleStepResult - -func (s sortPickleStepResultsByPickleStepID) Len() int { return len(s) } -func (s sortPickleStepResultsByPickleStepID) Less(i, j int) bool { - iID := mustConvertStringToInt(s[i].PickleStepID) - jID := mustConvertStringToInt(s[j].PickleStepID) - return iID < jID -} -func (s sortPickleStepResultsByPickleStepID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -func mustConvertStringToInt(s string) int { - i, err := strconv.Atoi(s) - if err != nil { - panic(err) - } - - return i -} - -type pickleResult struct { - PickleID string - StartedAt time.Time -} - // ErrUndefined is returned in case if step definition was not found var ErrUndefined = fmt.Errorf("step is undefined") @@ -654,181 +528,3 @@ func (s *Suite) runPickle(pickle *messages.Pickle) (err error) { return } - -func (s *Suite) printStepDefinitions(w io.Writer) { - var longest int - for _, def := range s.steps { - n := utf8.RuneCountInString(def.Expr.String()) - if longest < n { - longest = n - } - } - for _, def := range s.steps { - n := utf8.RuneCountInString(def.Expr.String()) - location := def.definitionID() - spaces := strings.Repeat(" ", longest-n) - fmt.Fprintln(w, yellow(def.Expr.String())+spaces, blackb("# "+location)) - } - if len(s.steps) == 0 { - fmt.Fprintln(w, "there were no contexts registered, could not find any step definition..") - } -} - -var pathLineRe = regexp.MustCompile(`:([\d]+)$`) - -func extractFeaturePathLine(p string) (string, int) { - line := -1 - retPath := p - if m := pathLineRe.FindStringSubmatch(p); len(m) > 0 { - if i, err := strconv.Atoi(m[1]); err == nil { - line = i - retPath = p[:strings.LastIndexByte(p, ':')] - } - } - return retPath, line -} - -func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) { - reader, err := os.Open(path) - if err != nil { - return nil, err - } - - defer reader.Close() - - var buf bytes.Buffer - gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc) - if err != nil { - return nil, fmt.Errorf("%s - %v", path, err) - } - - gherkinDocument.Uri = path - pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc) - - f := feature{GherkinDocument: gherkinDocument, pickles: pickles, content: buf.Bytes()} - return &f, nil -} - -func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) { - var features []*feature - return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error { - if err != nil { - return err - } - - if f.IsDir() { - return nil - } - - if !strings.HasSuffix(p, ".feature") { - return nil - } - - feat, err := parseFeatureFile(p, newIDFunc) - if err != nil { - return err - } - - features = append(features, feat) - return nil - }) -} - -func parsePath(path string) ([]*feature, error) { - var features []*feature - - path, line := extractFeaturePathLine(path) - - fi, err := os.Stat(path) - if err != nil { - return features, err - } - - newIDFunc := (&messages.Incrementing{}).NewId - - if fi.IsDir() { - return parseFeatureDir(path, newIDFunc) - } - - ft, err := parseFeatureFile(path, newIDFunc) - if err != nil { - return features, err - } - - // filter scenario by line number - var pickles []*messages.Pickle - for _, pickle := range ft.pickles { - sc := ft.findScenario(pickle.AstNodeIds[0]) - - if line == -1 || uint32(line) == sc.Location.Line { - pickles = append(pickles, pickle) - } - } - ft.pickles = pickles - - return append(features, ft), nil -} - -func parseFeatures(filter string, paths []string) ([]*feature, error) { - var order int - - uniqueFeatureURI := make(map[string]*feature) - for _, path := range paths { - feats, err := parsePath(path) - - switch { - case os.IsNotExist(err): - return nil, fmt.Errorf(`feature path "%s" is not available`, path) - case os.IsPermission(err): - return nil, fmt.Errorf(`feature path "%s" is not accessible`, path) - case err != nil: - return nil, err - } - - for _, ft := range feats { - if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate { - continue - } - - ft.order = order - order++ - uniqueFeatureURI[ft.Uri] = ft - } - } - - return filterFeatures(filter, uniqueFeatureURI), nil -} - -type sortFeaturesByOrder []*feature - -func (s sortFeaturesByOrder) Len() int { return len(s) } -func (s sortFeaturesByOrder) Less(i, j int) bool { return s[i].order < s[j].order } -func (s sortFeaturesByOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -func filterFeatures(tags string, collected map[string]*feature) (features []*feature) { - for _, ft := range collected { - ft.pickles = applyTagFilter(tags, ft.pickles) - - if ft.Feature != nil { - features = append(features, ft) - } - } - - sort.Sort(sortFeaturesByOrder(features)) - - return features -} - -func applyTagFilter(tags string, pickles []*messages.Pickle) (result []*messages.Pickle) { - if len(tags) == 0 { - return pickles - } - - for _, pickle := range pickles { - if matchesTags(tags, pickle.Tags) { - result = append(result, pickle) - } - } - - return -} diff --git a/utils.go b/utils.go index ac63a557..584394bc 100644 --- a/utils.go +++ b/utils.go @@ -1,10 +1,12 @@ package godog import ( + "strconv" "strings" "time" "github.com/cucumber/godog/colors" + "github.com/cucumber/messages-go/v10" ) var ( @@ -37,3 +39,22 @@ func trimAllLines(s string) string { } return strings.Join(lines, "\n") } + +type sortPicklesByID []*messages.Pickle + +func (s sortPicklesByID) Len() int { return len(s) } +func (s sortPicklesByID) Less(i, j int) bool { + iID := mustConvertStringToInt(s[i].Id) + jID := mustConvertStringToInt(s[j].Id) + return iID < jID +} +func (s sortPicklesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func mustConvertStringToInt(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + + return i +}