diff --git a/internal/flags/options.go b/internal/flags/options.go index 69e39d4f..a0eb1260 100644 --- a/internal/flags/options.go +++ b/internal/flags/options.go @@ -64,4 +64,14 @@ type Options struct { // TestingT runs scenarios as subtests. TestingT *testing.T + + // FeatureContents allows passing in each feature manually + // where the contents of each feature is stored as a byte slice + // in a map entry + FeatureContents []Feature +} + +type Feature struct { + Name string + Contents []byte } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index c505cde9..e5d945d2 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -13,6 +13,7 @@ import ( "github.com/cucumber/gherkin-go/v19" "github.com/cucumber/messages-go/v16" + "github.com/cucumber/godog/internal/flags" "github.com/cucumber/godog/internal/models" "github.com/cucumber/godog/internal/tags" ) @@ -53,6 +54,22 @@ func parseFeatureFile(path string, newIDFunc func() string) (*models.Feature, er return &f, nil } +func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.Feature, error) { + reader := bytes.NewReader(feature) + + 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 := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: buf.Bytes()} + return &f, nil +} + func parseFeatureDir(dir string, newIDFunc func() string) ([]*models.Feature, error) { var features []*models.Feature return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error { @@ -162,6 +179,41 @@ func ParseFeatures(filter string, paths []string) ([]*models.Feature, error) { return features, nil } +type FeatureContent = flags.Feature + +func ParseFromBytes(filter string, featuresInputs []FeatureContent) ([]*models.Feature, error) { + var order int + + featureIdxs := make(map[string]int) + uniqueFeatureURI := make(map[string]*models.Feature) + newIDFunc := (&messages.Incrementing{}).NewId + for _, f := range featuresInputs { + ft, err := parseBytes(f.Name, f.Contents, newIDFunc) + if err != nil { + return nil, err + } + + if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate { + continue + } + + uniqueFeatureURI[ft.Uri] = ft + featureIdxs[ft.Uri] = order + + order++ + } + + var features = make([]*models.Feature, len(uniqueFeatureURI)) + for uri, feature := range uniqueFeatureURI { + idx := featureIdxs[uri] + features[idx] = feature + } + + features = filterFeatures(filter, features) + + return features, nil +} + func filterFeatures(filter string, features []*models.Feature) (result []*models.Feature) { for _, ft := range features { ft.Pickles = tags.ApplyTagFilter(filter, ft.Pickles) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 19fbf851..f03dfbf3 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -37,6 +37,64 @@ func Test_FeatureFilePathParser(t *testing.T) { } } +func Test_ParseFromBytes_FromMultipleFeatures_DuplicateNames(t *testing.T) { + eatGodogContents := ` +Feature: eat godogs + In order to be happy + As a hungry gopher + I need to be able to eat godogs + + Scenario: Eat 5 out of 12 + Given there are 12 godogs + When I eat 5 + Then there should be 7 remaining` + input := []parser.FeatureContent{ + {Name: "MyCoolDuplicatedFeature", Contents: []byte(eatGodogContents)}, + {Name: "MyCoolDuplicatedFeature", Contents: []byte(eatGodogContents)}, + } + + featureFromBytes, err := parser.ParseFromBytes("", input) + require.NoError(t, err) + require.Len(t, featureFromBytes, 1) +} + +func Test_ParseFromBytes_FromMultipleFeatures(t *testing.T) { + featureFileName := "godogs.feature" + eatGodogContents := ` +Feature: eat godogs + In order to be happy + As a hungry gopher + I need to be able to eat godogs + + Scenario: Eat 5 out of 12 + Given there are 12 godogs + When I eat 5 + Then there should be 7 remaining` + + baseDir := filepath.Join(os.TempDir(), t.Name(), "godogs") + errA := os.MkdirAll(baseDir+"/a", 0755) + defer os.RemoveAll(baseDir) + + require.Nil(t, errA) + + err := ioutil.WriteFile(filepath.Join(baseDir, featureFileName), []byte(eatGodogContents), 0644) + require.Nil(t, err) + + featureFromFile, err := parser.ParseFeatures("", []string{baseDir}) + require.NoError(t, err) + require.Len(t, featureFromFile, 1) + + input := []parser.FeatureContent{ + {Name: filepath.Join(baseDir, featureFileName), Contents: []byte(eatGodogContents)}, + } + + featureFromBytes, err := parser.ParseFromBytes("", input) + require.NoError(t, err) + require.Len(t, featureFromBytes, 1) + + assert.Equal(t, featureFromFile, featureFromBytes) +} + func Test_ParseFeatures_FromMultiplePaths(t *testing.T) { const featureFileName = "godogs.feature" const featureFileContents = `Feature: eat godogs diff --git a/run.go b/run.go index 5fa6e158..f837b921 100644 --- a/run.go +++ b/run.go @@ -213,7 +213,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int { return exitOptionError } - if len(opt.Paths) == 0 { + if len(opt.Paths) == 0 && len(opt.FeatureContents) == 0 { inf, err := os.Stat("features") if err == nil && inf.IsDir() { opt.Paths = []string{"features"} @@ -226,10 +226,22 @@ func runWithOptions(suiteName string, runner runner, opt Options) int { runner.fmt = multiFmt.FormatterFunc(suiteName, output) - var err error - if runner.features, err = parser.ParseFeatures(opt.Tags, opt.Paths); err != nil { - fmt.Fprintln(os.Stderr, err) - return exitOptionError + if len(opt.FeatureContents) > 0 { + features, err := parser.ParseFromBytes(opt.Tags, opt.FeatureContents) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return exitOptionError + } + runner.features = append(runner.features, features...) + } + + if len(opt.Paths) > 0 { + features, err := parser.ParseFeatures(opt.Tags, opt.Paths) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return exitOptionError + } + runner.features = append(runner.features, features...) } runner.storage = storage.NewStorage() diff --git a/run_test.go b/run_test.go index b2f9bd2f..9c07a6dd 100644 --- a/run_test.go +++ b/run_test.go @@ -263,6 +263,92 @@ func Test_ByDefaultRunsFeaturesPath(t *testing.T) { assert.Equal(t, exitSuccess, status) } +func Test_RunsWithFeatureContentsOption(t *testing.T) { + items, err := ioutil.ReadDir("./features") + require.NoError(t, err) + + var featureContents []Feature + for _, item := range items { + if !item.IsDir() && strings.Contains(item.Name(), ".feature") { + contents, err := os.ReadFile("./features/" + item.Name()) + require.NoError(t, err) + featureContents = append(featureContents, Feature{ + Name: item.Name(), + Contents: contents, + }) + } + } + + opts := Options{ + Format: "progress", + Output: ioutil.Discard, + Strict: true, + FeatureContents: featureContents, + } + + status := TestSuite{ + Name: "fails", + ScenarioInitializer: func(_ *ScenarioContext) {}, + Options: &opts, + }.Run() + + // should fail in strict mode due to undefined steps + assert.Equal(t, exitFailure, status) + + opts.Strict = false + status = TestSuite{ + Name: "succeeds", + ScenarioInitializer: func(_ *ScenarioContext) {}, + Options: &opts, + }.Run() + + // should succeed in non strict mode due to undefined steps + assert.Equal(t, exitSuccess, status) +} + +func Test_RunsWithFeatureContentsAndPathsOptions(t *testing.T) { + featureContents := []Feature{ + { + Name: "MySuperCoolFeature", + Contents: []byte(` +Feature: run features from bytes + Scenario: should run a normal feature + Given a feature "normal.feature" file: + """ + Feature: normal feature + + Scenario: parse a scenario + Given a feature path "features/load.feature:6" + When I parse features + Then I should have 1 scenario registered + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + a feature path "features/load.feature:6" + I parse features + I should have 1 scenario registered + """`), + }, + } + + opts := Options{ + Format: "progress", + Output: ioutil.Discard, + Paths: []string{"./features"}, + FeatureContents: featureContents, + } + + status := TestSuite{ + Name: "succeeds", + ScenarioInitializer: func(_ *ScenarioContext) {}, + Options: &opts, + }.Run() + + assert.Equal(t, exitSuccess, status) +} + func bufErrorPipe(t *testing.T) (io.ReadCloser, func()) { stderr := os.Stderr r, w, err := os.Pipe() diff --git a/test_context.go b/test_context.go index 862280e8..fbbe981f 100644 --- a/test_context.go +++ b/test_context.go @@ -8,6 +8,7 @@ import ( "github.com/cucumber/godog/formatters" "github.com/cucumber/godog/internal/builder" + "github.com/cucumber/godog/internal/flags" "github.com/cucumber/godog/internal/models" "github.com/cucumber/messages-go/v16" ) @@ -316,3 +317,5 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { func Build(bin string) error { return builder.Build(bin) } + +type Feature = flags.Feature