diff --git a/features/formatter/pretty.feature b/features/formatter/pretty.feature index 7b307bea..4173d5ce 100644 --- a/features/formatter/pretty.feature +++ b/features/formatter/pretty.feature @@ -23,6 +23,30 @@ Feature: pretty formatter 0s """ + Scenario: Support of Feature Plus Rule Node + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + Rule: simple rule + Scenario: simple scenario + simple scenario description + """ + When I run feature suite with formatter "pretty" + Then the rendered output will be as follows: + """ + Feature: simple feature + simple feature description + + Rule: simple rule + + Scenario: simple scenario # features/simple.feature:4 + + 1 scenarios (1 undefined) + No steps + 0s + """ + Scenario: Support of Feature Plus Scenario Node With Tags Given a feature "features/simple.feature" file: """ diff --git a/internal/formatters/fmt_pretty.go b/internal/formatters/fmt_pretty.go index 0860dda9..beca22bd 100644 --- a/internal/formatters/fmt_pretty.go +++ b/internal/formatters/fmt_pretty.go @@ -123,6 +123,7 @@ func (f *Pretty) Pending(pickle *messages.Pickle, step *messages.PickleStep, mat f.printStep(pickle, step) } +// printFeature prints given feature (with title and description) to f.out. func (f *Pretty) printFeature(feature *messages.Feature) { fmt.Fprintln(f.out, keywordAndName(feature.Keyword, feature.Name)) if strings.TrimSpace(feature.Description) != "" { @@ -132,6 +133,7 @@ func (f *Pretty) printFeature(feature *messages.Feature) { } } +// keywordAndName returns formatted keyword and name. func keywordAndName(keyword, name string) string { title := whiteb(keyword + ":") if len(name) > 0 { @@ -140,8 +142,11 @@ func keywordAndName(keyword, name string) string { return title } +// scenarioLengths returns the length of the scenario header, and the maximum +// length of all steps. func (f *Pretty) scenarioLengths(pickle *messages.Pickle) (scenarioHeaderLength int, maxLength int) { feature := f.Storage.MustGetFeature(pickle.Uri) + astRule := feature.FindRule(pickle.AstNodeIds[0]) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) astBackground := feature.FindBackground(pickle.AstNodeIds[0]) @@ -151,10 +156,22 @@ func (f *Pretty) scenarioLengths(pickle *messages.Pickle) (scenarioHeaderLength if astBackground != nil { maxLength = f.longestStep(astBackground.Steps, maxLength) } + if astRule != nil { + for _, rc := range astRule.Children { + if rc.Scenario != nil { + scenarioHeaderLength = f.lengthPickle(astScenario.Keyword, astScenario.Name) + maxLength = f.longestStep(rc.Scenario.Steps, maxLength) + } else if rc.Background != nil { + maxLength = f.longestStep(rc.Scenario.Steps, maxLength) + } + } + } return scenarioHeaderLength, maxLength } +// printScenarioHeader prints scenario header (keyword/name) with feature line +// reference. The scenario is prefixed with whitespace equal to spaceFilling. func (f *Pretty) printScenarioHeader(pickle *messages.Pickle, astScenario *messages.Scenario, spaceFilling int) { feature := f.Storage.MustGetFeature(pickle.Uri) text := s(f.indent) + keywordAndName(astScenario.Keyword, astScenario.Name) @@ -162,13 +179,19 @@ func (f *Pretty) printScenarioHeader(pickle *messages.Pickle, astScenario *messa fmt.Fprintln(f.out, "\n"+text) } +// printUndefinedPickle prints pickles that are not defined yet. func (f *Pretty) printUndefinedPickle(pickle *messages.Pickle) { feature := f.Storage.MustGetFeature(pickle.Uri) + astRule := feature.FindRule(pickle.AstNodeIds[0]) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) astBackground := feature.FindBackground(pickle.AstNodeIds[0]) scenarioHeaderLength, maxLength := f.scenarioLengths(pickle) + if astRule != nil { + fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astRule.Keyword, astRule.Name)) + } + if astBackground != nil { fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name)) for _, step := range astBackground.Steps { @@ -352,10 +375,15 @@ func (f *Pretty) printTableHeader(row *messages.TableRow, max []int) { func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleStep) { feature := f.Storage.MustGetFeature(pickle.Uri) + astRule := feature.FindRule(pickle.AstNodeIds[0]) astBackground := feature.FindBackground(pickle.AstNodeIds[0]) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) astStep := feature.FindStep(pickleStep.AstNodeIds[0]) + // TODO: if this is the first scenario, and there is a rule, then the rule + // header must be printed. + // indent all if there is a rule. + var astBackgroundStep bool var firstExecutedBackgroundStep bool var backgroundSteps int @@ -391,6 +419,9 @@ func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleS firstExecutedScenarioStep := astScenario.Steps[0].Id == pickleStep.AstNodeIds[0] if !astBackgroundStep && firstExecutedScenarioStep { + if astRule != nil { + fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astRule.Keyword, astRule.Name)) + } f.printScenarioHeader(pickle, astScenario, maxLength-scenarioHeaderLength) } @@ -421,6 +452,7 @@ func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleS } } +// printDocString prints a formatted docString to f.out. func (f *Pretty) printDocString(docString *messages.DocString) { var ct string @@ -437,7 +469,7 @@ func (f *Pretty) printDocString(docString *messages.DocString) { fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter)) } -// print table with aligned table cells +// printTable prints table with aligned table cells // @TODO: need to make example header cells bold func (f *Pretty) printTable(t *messages.PickleTable, c colors.ColorFunc) { maxColLengths := maxColLengths(t, c) @@ -454,7 +486,7 @@ func (f *Pretty) printTable(t *messages.PickleTable, c colors.ColorFunc) { } } -// longest gives a list of longest columns of all rows in Table +// maxColLengths returns a list of longest columns of all rows in Table func maxColLengths(t *messages.PickleTable, clrs ...colors.ColorFunc) []int { if t == nil { return []int{} @@ -480,6 +512,7 @@ func maxColLengths(t *messages.PickleTable, clrs ...colors.ColorFunc) []int { return longest } +// longestExampleRow returns a list of longest example rows func longestExampleRow(t *messages.Examples, clrs ...colors.ColorFunc) []int { if t == nil { return []int{} @@ -519,6 +552,8 @@ func longestExampleRow(t *messages.Examples, clrs ...colors.ColorFunc) []int { return longest } +// longestStep returns the length of the longest step in given steps, or +// pickleLength if that is greater. func (f *Pretty) longestStep(steps []*messages.Step, pickleLength int) int { max := pickleLength @@ -532,7 +567,7 @@ func (f *Pretty) longestStep(steps []*messages.Step, pickleLength int) int { return max } -// a line number representation in feature file +// line returns a line number representation in feature file func line(path string, loc *messages.Location) string { // Path can contain a line number already. // This line number has to be trimmed to avoid duplication. @@ -540,6 +575,8 @@ func line(path string, loc *messages.Location) string { return " " + blackb(fmt.Sprintf("# %s:%d", path, loc.Line)) } +// lengthPickleStep returns the length of a pickle step. The length is +// calculated based on indent, keyword, and associated text. func (f *Pretty) lengthPickleStep(keyword, text string) int { return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(keyword)+" "+text) } diff --git a/internal/models/feature.go b/internal/models/feature.go index d38a224a..d7d39ab0 100644 --- a/internal/models/feature.go +++ b/internal/models/feature.go @@ -13,18 +13,42 @@ type Feature struct { Content []byte } -// FindScenario ... +// FindRule returns the rule containing astScenarioID. +func (f Feature) FindRule(astScenarioID string) *messages.Rule { + for _, child := range f.GherkinDocument.Feature.Children { + if ru := child.Rule; ru != nil { + for _, ruc := range ru.Children { + if sc := ruc.Scenario; sc != nil && sc.Id == astScenarioID { + return ru + } + } + } + } + + return nil +} + +// FindScenario returns the scenario matching astScenarioID. The scenario +// might be a direct child of Feature, or a child of a Rule within a Feature. func (f Feature) FindScenario(astScenarioID string) *messages.Scenario { for _, child := range f.GherkinDocument.Feature.Children { if sc := child.Scenario; sc != nil && sc.Id == astScenarioID { return sc } + if rc := child.Rule; rc != nil { + for _, rcc := range rc.Children { + if sc := rcc.Scenario; sc != nil && sc.Id == astScenarioID { + return sc + } + } + } } return nil } // FindBackground ... +// TODO: must find children of rule!!!! func (f Feature) FindBackground(astScenarioID string) *messages.Background { var bg *messages.Background @@ -58,7 +82,9 @@ func (f Feature) FindExample(exampleAstID string) (*messages.Examples, *messages return nil, nil } -// FindStep ... +// FindStep returns the step matching astStepID. The step +// might be a child of a Scenario or a Background (which might be contained +// inside a Rule). func (f Feature) FindStep(astStepID string) *messages.Step { for _, child := range f.GherkinDocument.Feature.Children { if sc := child.Scenario; sc != nil { @@ -76,6 +102,26 @@ func (f Feature) FindStep(astStepID string) *messages.Step { } } } + + if ru := child.Rule; ru != nil { + for _, ruc := range ru.Children { + if sc := ruc.Scenario; sc != nil { + for _, step := range sc.Steps { + if step.Id == astStepID { + return step + } + } + } + + if bg := ruc.Background; bg != nil { + for _, step := range bg.Steps { + if step.Id == astStepID { + return step + } + } + } + } + } } return nil