Skip to content

Commit d45a9aa

Browse files
authored
add new option for created features with parsing from byte slices (#476)
1 parent b2672bb commit d45a9aa

File tree

6 files changed

+226
-5
lines changed

6 files changed

+226
-5
lines changed

internal/flags/options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,14 @@ type Options struct {
6464

6565
// TestingT runs scenarios as subtests.
6666
TestingT *testing.T
67+
68+
// FeatureContents allows passing in each feature manually
69+
// where the contents of each feature is stored as a byte slice
70+
// in a map entry
71+
FeatureContents []Feature
72+
}
73+
74+
type Feature struct {
75+
Name string
76+
Contents []byte
6777
}

internal/parser/parser.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/cucumber/gherkin-go/v19"
1414
"github.com/cucumber/messages-go/v16"
1515

16+
"github.com/cucumber/godog/internal/flags"
1617
"github.com/cucumber/godog/internal/models"
1718
"github.com/cucumber/godog/internal/tags"
1819
)
@@ -53,6 +54,22 @@ func parseFeatureFile(path string, newIDFunc func() string) (*models.Feature, er
5354
return &f, nil
5455
}
5556

57+
func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.Feature, error) {
58+
reader := bytes.NewReader(feature)
59+
60+
var buf bytes.Buffer
61+
gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
62+
if err != nil {
63+
return nil, fmt.Errorf("%s - %v", path, err)
64+
}
65+
66+
gherkinDocument.Uri = path
67+
pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
68+
69+
f := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: buf.Bytes()}
70+
return &f, nil
71+
}
72+
5673
func parseFeatureDir(dir string, newIDFunc func() string) ([]*models.Feature, error) {
5774
var features []*models.Feature
5875
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) {
162179
return features, nil
163180
}
164181

182+
type FeatureContent = flags.Feature
183+
184+
func ParseFromBytes(filter string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
185+
var order int
186+
187+
featureIdxs := make(map[string]int)
188+
uniqueFeatureURI := make(map[string]*models.Feature)
189+
newIDFunc := (&messages.Incrementing{}).NewId
190+
for _, f := range featuresInputs {
191+
ft, err := parseBytes(f.Name, f.Contents, newIDFunc)
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate {
197+
continue
198+
}
199+
200+
uniqueFeatureURI[ft.Uri] = ft
201+
featureIdxs[ft.Uri] = order
202+
203+
order++
204+
}
205+
206+
var features = make([]*models.Feature, len(uniqueFeatureURI))
207+
for uri, feature := range uniqueFeatureURI {
208+
idx := featureIdxs[uri]
209+
features[idx] = feature
210+
}
211+
212+
features = filterFeatures(filter, features)
213+
214+
return features, nil
215+
}
216+
165217
func filterFeatures(filter string, features []*models.Feature) (result []*models.Feature) {
166218
for _, ft := range features {
167219
ft.Pickles = tags.ApplyTagFilter(filter, ft.Pickles)

internal/parser/parser_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,64 @@ func Test_FeatureFilePathParser(t *testing.T) {
3737
}
3838
}
3939

40+
func Test_ParseFromBytes_FromMultipleFeatures_DuplicateNames(t *testing.T) {
41+
eatGodogContents := `
42+
Feature: eat godogs
43+
In order to be happy
44+
As a hungry gopher
45+
I need to be able to eat godogs
46+
47+
Scenario: Eat 5 out of 12
48+
Given there are 12 godogs
49+
When I eat 5
50+
Then there should be 7 remaining`
51+
input := []parser.FeatureContent{
52+
{Name: "MyCoolDuplicatedFeature", Contents: []byte(eatGodogContents)},
53+
{Name: "MyCoolDuplicatedFeature", Contents: []byte(eatGodogContents)},
54+
}
55+
56+
featureFromBytes, err := parser.ParseFromBytes("", input)
57+
require.NoError(t, err)
58+
require.Len(t, featureFromBytes, 1)
59+
}
60+
61+
func Test_ParseFromBytes_FromMultipleFeatures(t *testing.T) {
62+
featureFileName := "godogs.feature"
63+
eatGodogContents := `
64+
Feature: eat godogs
65+
In order to be happy
66+
As a hungry gopher
67+
I need to be able to eat godogs
68+
69+
Scenario: Eat 5 out of 12
70+
Given there are 12 godogs
71+
When I eat 5
72+
Then there should be 7 remaining`
73+
74+
baseDir := filepath.Join(os.TempDir(), t.Name(), "godogs")
75+
errA := os.MkdirAll(baseDir+"/a", 0755)
76+
defer os.RemoveAll(baseDir)
77+
78+
require.Nil(t, errA)
79+
80+
err := ioutil.WriteFile(filepath.Join(baseDir, featureFileName), []byte(eatGodogContents), 0644)
81+
require.Nil(t, err)
82+
83+
featureFromFile, err := parser.ParseFeatures("", []string{baseDir})
84+
require.NoError(t, err)
85+
require.Len(t, featureFromFile, 1)
86+
87+
input := []parser.FeatureContent{
88+
{Name: filepath.Join(baseDir, featureFileName), Contents: []byte(eatGodogContents)},
89+
}
90+
91+
featureFromBytes, err := parser.ParseFromBytes("", input)
92+
require.NoError(t, err)
93+
require.Len(t, featureFromBytes, 1)
94+
95+
assert.Equal(t, featureFromFile, featureFromBytes)
96+
}
97+
4098
func Test_ParseFeatures_FromMultiplePaths(t *testing.T) {
4199
const featureFileName = "godogs.feature"
42100
const featureFileContents = `Feature: eat godogs

run.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int {
213213
return exitOptionError
214214
}
215215

216-
if len(opt.Paths) == 0 {
216+
if len(opt.Paths) == 0 && len(opt.FeatureContents) == 0 {
217217
inf, err := os.Stat("features")
218218
if err == nil && inf.IsDir() {
219219
opt.Paths = []string{"features"}
@@ -226,10 +226,22 @@ func runWithOptions(suiteName string, runner runner, opt Options) int {
226226

227227
runner.fmt = multiFmt.FormatterFunc(suiteName, output)
228228

229-
var err error
230-
if runner.features, err = parser.ParseFeatures(opt.Tags, opt.Paths); err != nil {
231-
fmt.Fprintln(os.Stderr, err)
232-
return exitOptionError
229+
if len(opt.FeatureContents) > 0 {
230+
features, err := parser.ParseFromBytes(opt.Tags, opt.FeatureContents)
231+
if err != nil {
232+
fmt.Fprintln(os.Stderr, err)
233+
return exitOptionError
234+
}
235+
runner.features = append(runner.features, features...)
236+
}
237+
238+
if len(opt.Paths) > 0 {
239+
features, err := parser.ParseFeatures(opt.Tags, opt.Paths)
240+
if err != nil {
241+
fmt.Fprintln(os.Stderr, err)
242+
return exitOptionError
243+
}
244+
runner.features = append(runner.features, features...)
233245
}
234246

235247
runner.storage = storage.NewStorage()

run_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,92 @@ func Test_ByDefaultRunsFeaturesPath(t *testing.T) {
263263
assert.Equal(t, exitSuccess, status)
264264
}
265265

266+
func Test_RunsWithFeatureContentsOption(t *testing.T) {
267+
items, err := ioutil.ReadDir("./features")
268+
require.NoError(t, err)
269+
270+
var featureContents []Feature
271+
for _, item := range items {
272+
if !item.IsDir() && strings.Contains(item.Name(), ".feature") {
273+
contents, err := os.ReadFile("./features/" + item.Name())
274+
require.NoError(t, err)
275+
featureContents = append(featureContents, Feature{
276+
Name: item.Name(),
277+
Contents: contents,
278+
})
279+
}
280+
}
281+
282+
opts := Options{
283+
Format: "progress",
284+
Output: ioutil.Discard,
285+
Strict: true,
286+
FeatureContents: featureContents,
287+
}
288+
289+
status := TestSuite{
290+
Name: "fails",
291+
ScenarioInitializer: func(_ *ScenarioContext) {},
292+
Options: &opts,
293+
}.Run()
294+
295+
// should fail in strict mode due to undefined steps
296+
assert.Equal(t, exitFailure, status)
297+
298+
opts.Strict = false
299+
status = TestSuite{
300+
Name: "succeeds",
301+
ScenarioInitializer: func(_ *ScenarioContext) {},
302+
Options: &opts,
303+
}.Run()
304+
305+
// should succeed in non strict mode due to undefined steps
306+
assert.Equal(t, exitSuccess, status)
307+
}
308+
309+
func Test_RunsWithFeatureContentsAndPathsOptions(t *testing.T) {
310+
featureContents := []Feature{
311+
{
312+
Name: "MySuperCoolFeature",
313+
Contents: []byte(`
314+
Feature: run features from bytes
315+
Scenario: should run a normal feature
316+
Given a feature "normal.feature" file:
317+
"""
318+
Feature: normal feature
319+
320+
Scenario: parse a scenario
321+
Given a feature path "features/load.feature:6"
322+
When I parse features
323+
Then I should have 1 scenario registered
324+
"""
325+
When I run feature suite
326+
Then the suite should have passed
327+
And the following steps should be passed:
328+
"""
329+
a feature path "features/load.feature:6"
330+
I parse features
331+
I should have 1 scenario registered
332+
"""`),
333+
},
334+
}
335+
336+
opts := Options{
337+
Format: "progress",
338+
Output: ioutil.Discard,
339+
Paths: []string{"./features"},
340+
FeatureContents: featureContents,
341+
}
342+
343+
status := TestSuite{
344+
Name: "succeeds",
345+
ScenarioInitializer: func(_ *ScenarioContext) {},
346+
Options: &opts,
347+
}.Run()
348+
349+
assert.Equal(t, exitSuccess, status)
350+
}
351+
266352
func bufErrorPipe(t *testing.T) (io.ReadCloser, func()) {
267353
stderr := os.Stderr
268354
r, w, err := os.Pipe()

test_context.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/cucumber/godog/formatters"
1010
"github.com/cucumber/godog/internal/builder"
11+
"github.com/cucumber/godog/internal/flags"
1112
"github.com/cucumber/godog/internal/models"
1213
"github.com/cucumber/messages-go/v16"
1314
)
@@ -316,3 +317,5 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) {
316317
func Build(bin string) error {
317318
return builder.Build(bin)
318319
}
320+
321+
type Feature = flags.Feature

0 commit comments

Comments
 (0)