From e19f7af8321dcd3e21848032870e4c92008d62f8 Mon Sep 17 00:00:00 2001 From: Michael Sauter Date: Tue, 16 Nov 2021 14:13:40 +0100 Subject: [PATCH] Support Rule keyword Note that rules are not indented as that would complicate the logic considerably: every print directive would need to check if it is surrounded by a rule or not. --- features/formatter/pretty.feature | 175 ++++++++++++++++++++++++++++++ internal/formatters/fmt_pretty.go | 61 ++++++++++- internal/models/feature.go | 66 ++++++++++- run_test.go | 12 +- 4 files changed, 302 insertions(+), 12 deletions(-) diff --git a/features/formatter/pretty.feature b/features/formatter/pretty.feature index 7b307bea..3ab0d61f 100644 --- a/features/formatter/pretty.feature +++ b/features/formatter/pretty.feature @@ -23,6 +23,181 @@ Feature: pretty formatter 0s """ + Scenario: Support of Feature Plus Background and Scenario Node + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + Background: simple background + Given passing step + And passing step + 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 + + Background: simple background + Given passing step + And passing step + + Scenario: simple scenario # features/simple.feature:6 + + 1 scenarios (1 undefined) + No steps + 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 Rule Node with Background + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + + Rule: simple rule + + Background: simple background + Given something + And another thing + + 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 + + Background: simple background + Given something + And another thing + + Scenario: simple scenario # features/simple.feature:10 + + 1 scenarios (1 undefined) + No steps + 0s + """ + + Scenario: Support of Feature Plus Rule Node with multiple scenarios + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + + Rule: simple rule + + Scenario: simple scenario + simple scenario description + + Given passing step + Then passing step + + Scenario: simple second scenario + simple second scenario description + + Given passing step + Then passing step + """ + 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:6 + Given passing step # suite_context_test.go:0 -> InitializeScenario.func2 + Then passing step # suite_context_test.go:0 -> InitializeScenario.func2 + + Scenario: simple second scenario # features/simple.feature:12 + Given passing step # suite_context_test.go:0 -> InitializeScenario.func2 + Then passing step # suite_context_test.go:0 -> InitializeScenario.func2 + + 2 scenarios (2 passed) + 4 steps (4 passed) + 0s + """ + + Scenario: Support of Feature Plus Rule Node with Background and multiple scenarios + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + Rule: simple rule + + Background: simple background + Given passing step + And passing step + + Scenario: simple scenario + simple scenario description + + Given passing step + Then passing step + + Scenario: simple second scenario + simple second scenario description + + Given passing step + Then passing step + """ + 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 + + Background: simple background + Given passing step # suite_context_test.go:0 -> InitializeScenario.func2 + And passing step # suite_context_test.go:0 -> InitializeScenario.func2 + + Scenario: simple scenario # features/simple.feature:9 + Given passing step # suite_context_test.go:0 -> InitializeScenario.func2 + Then passing step # suite_context_test.go:0 -> InitializeScenario.func2 + + Scenario: simple second scenario # features/simple.feature:15 + Given passing step # suite_context_test.go:0 -> InitializeScenario.func2 + Then passing step # suite_context_test.go:0 -> InitializeScenario.func2 + + 2 scenarios (2 passed) + 8 steps (8 passed) + 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..aead1764 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.Background.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,6 +375,7 @@ 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]) @@ -378,6 +402,9 @@ func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleS } if astBackgroundStep && firstExecutedBackgroundStep { + if astRule != nil { + fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astRule.Keyword, astRule.Name)) + } fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name)) } @@ -391,6 +418,28 @@ func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleS firstExecutedScenarioStep := astScenario.Steps[0].Id == pickleStep.AstNodeIds[0] if !astBackgroundStep && firstExecutedScenarioStep { + // The first scenario step is responsible for printing the rule unless + // it has already been printed by the background. + if astRule != nil { + var firstScenarioOfRule bool + var ruleHasBackground bool + for _, rc := range astRule.Children { + if sc := rc.Scenario; sc != nil { + firstScenarioOfRule = sc.Id == astScenario.Id + break + } + } + for _, rc := range astRule.Children { + if bc := rc.Background; bc != nil { + ruleHasBackground = true + break + } + } + if firstScenarioOfRule && !ruleHasBackground { + fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astRule.Keyword, astRule.Name)) + } + } + f.printScenarioHeader(pickle, astScenario, maxLength-scenarioHeaderLength) } @@ -421,6 +470,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 +487,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 +504,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 +530,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 +570,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 +585,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 +593,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..94a093a3 100644 --- a/internal/models/feature.go +++ b/internal/models/feature.go @@ -13,22 +13,60 @@ 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 ... +// FindBackground returns the background belonging to the given astScenarioID. +// It returns the closest background in case there are multiple, e.g. if there +// is a background for both feature and rule, then the background of the rule +// is returned. func (f Feature) FindBackground(astScenarioID string) *messages.Background { var bg *messages.Background for _, child := range f.GherkinDocument.Feature.Children { + if rc := child.Rule; rc != nil { + for _, rcc := range rc.Children { + if tmp := rcc.Background; tmp != nil { + bg = tmp + } + + if sc := rcc.Scenario; sc != nil && sc.Id == astScenarioID { + return bg + } + } + } + if tmp := child.Background; tmp != nil { bg = tmp } @@ -58,7 +96,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 +116,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 diff --git a/run_test.go b/run_test.go index 9d3a8c1e..49f2c150 100644 --- a/run_test.go +++ b/run_test.go @@ -414,11 +414,11 @@ func Test_AllFeaturesRun(t *testing.T) { ...................................................................... 140 ...................................................................... 210 ...................................................................... 280 -........................................ 320 +....................................................... 335 -83 scenarios (83 passed) -320 steps (320 passed) +88 scenarios (88 passed) +335 steps (335 passed) 0s ` @@ -441,11 +441,11 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) { ...................................................................... 140 ...................................................................... 210 ...................................................................... 280 -........................................ 320 +....................................................... 335 -83 scenarios (83 passed) -320 steps (320 passed) +88 scenarios (88 passed) +335 steps (335 passed) 0s `