diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31603016..8fa97da0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: test: strategy: matrix: - go-version: [ 1.16.x, 1.17.x, 1.19.x, 1.20.x, 1.21.x ] # Lowest supported and current stable versions. + go-version: [ 1.17.x, 1.19.x, 1.20.x, 1.21.x ] # Lowest supported and current stable versions. runs-on: ubuntu-latest steps: - name: Install Go diff --git a/CHANGELOG.md b/CHANGELOG.md index d0718848..17486478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt ## Unreleased +### Added + +- Option SnippetFunc to select a snippet func for generating code for missing steps ([596](https://github.com/cucumber/godog/pull/596) - [crosscode-nl](https://github.com/crosscode-nl)) +- Added SnippetFunc "gwt_func" to generate Given/When/Then step snippets ([596](https://github.com/cucumber/godog/pull/596) - [crosscode-nl](https://github.com/crosscode-nl)) +- Added SnippetFunc "step_func" to generate Step snippets - this is the original and the default functionality ([596](https://github.com/cucumber/godog/pull/596) - [crosscode-nl](https://github.com/crosscode-nl)) +- Added command line argument "snippet-func" that allows providing the snippet func via command line ([596](https://github.com/cucumber/godog/pull/596) - [crosscode-nl](https://github.com/crosscode-nl)) + +### Changed + +- BREAKING CHANGE, changed formatters.FormatterFunc to take a snippetFunc string parameter ([596](https://github.com/cucumber/godog/pull/596) - [crosscode-nl](https://github.com/crosscode-nl)) + +### Deprecated + +- Dropped support for Go v1.16. The strings.Title function is deprecated and the package to use is golang.org/x/text/cases, but that package is not compatible with Go v.1.16.x + ## [v0.13.0] ### Added - Support for reading feature files from an `fs.FS` ([550](https://github.com/cucumber/godog/pull/550) - [tigh-latte](https://github.com/tigh-latte)) diff --git a/_examples/custom-formatter/emoji.go b/_examples/custom-formatter/emoji.go index 50cc5d56..9f904c8d 100644 --- a/_examples/custom-formatter/emoji.go +++ b/_examples/custom-formatter/emoji.go @@ -20,13 +20,13 @@ func init() { godog.Format("emoji", "Progress formatter with emojis", emojiFormatterFunc) } -func emojiFormatterFunc(suite string, out io.Writer) godog.Formatter { - return newEmojiFmt(suite, out) +func emojiFormatterFunc(suite string, out io.Writer, snippetFunc string) godog.Formatter { + return newEmojiFmt(suite, out, snippetFunc) } -func newEmojiFmt(suite string, out io.Writer) *emojiFmt { +func newEmojiFmt(suite string, out io.Writer, snippetFunc string) *emojiFmt { return &emojiFmt{ - ProgressFmt: godog.NewProgressFmt(suite, out), + ProgressFmt: godog.NewProgressFmt(suite, out, snippetFunc), out: out, } } diff --git a/_examples/go.sum b/_examples/go.sum index f31bb39f..8e26df36 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -53,6 +53,38 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/_examples/snippets/README.md b/_examples/snippets/README.md new file mode 100644 index 00000000..7a153b06 --- /dev/null +++ b/_examples/snippets/README.md @@ -0,0 +1,213 @@ +# Snippets + +Snippets are generated when undefined steps are created. + +Currently, we support the following snippet functions: + +| name | description | +|------------|-------------------------------------------------------------------| +| step_func | Generates steps with the Step keyword and function bodies | +| gwt_func | Generates steps with Given/When/Then keywords and function bodies | + +Examples show the difference between each snippet generator. + +## Examples + +The first example uses the *step_func* snippet function to generate the snippet. This works by not providing a +snippet func or explicitly providing *step_func*. This example does not provide the snippet function explicitly. + +Run the following command to view the output of the step_func example. + +```shell +go test -test.v ./step_func +``` + +The output should be: + +``` +=== RUN TestFeatures +Feature: eat godogs + In order to be happy + As a hungry gopher + I need to be able to eat godogs +=== RUN TestFeatures/Eat_12_out_of_12 +=== RUN TestFeatures/Eat_5_out_of_12 + + Scenario: Eat 12 out of 12 # features/godogs.feature:11 + Given there are 12 godogs + + Scenario: Eat 5 out of 12 # features/godogs.feature:6 + Given there are 12 godogs + When I eat 12 + Then there should be none remaining + When I eat 5 + Then there should be 7 remaining + +2 scenarios (2 undefined) +6 steps (6 undefined) +271.125µs + +You can implement step definitions for undefined steps with these snippets: + +func iEat(arg1 int) error { + return godog.ErrPending +} + +func thereAreGodogs(arg1 int) error { + return godog.ErrPending +} + +func thereShouldBeNoneRemaining() error { + return godog.ErrPending +} + +func thereShouldBeRemaining(arg1 int) error { + return godog.ErrPending +} + +func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I eat (\d+)$`, iEat) + ctx.Given(`^there are (\d+) godogs$`, thereAreGodogs) + ctx.Then(`^there should be none remaining$`, thereShouldBeNoneRemaining) + ctx.Then(`^there should be (\d+) remaining$`, thereShouldBeRemaining) +} + +--- PASS: TestFeatures (0.00s) + --- PASS: TestFeatures/Eat_12_out_of_12 (0.00s) + --- PASS: TestFeatures/Eat_5_out_of_12 (0.00s) +PASS +ok github.com/cucumber/godog/_examples/snippets/gwt_func (cached) +``` + +The second example uses the *gwt_func* snippet function to generate the snippet. This works by providing a +snippet func or explicitly. + +```go +var opts = godog.Options{ + Output: colors.Colored(os.Stdout), + SnippetFunc: "gwt_func", + Concurrency: 4, +} +``` + +Run the following command to view the output of the gwt_func example. + +```shell +go test -test.v ./gwt_func +``` + +The output should be: + +``` +=== RUN TestFeatures +Feature: eat godogs + In order to be happy + As a hungry gopher + I need to be able to eat godogs +=== RUN TestFeatures/Eat_12_out_of_12 +=== RUN TestFeatures/Eat_5_out_of_12 + + Scenario: Eat 12 out of 12 # features/godogs.feature:11 + Given there are 12 godogs + + Scenario: Eat 5 out of 12 # features/godogs.feature:6 + Given there are 12 godogs + When I eat 12 + Then there should be none remaining + When I eat 5 + Then there should be 7 remaining + +2 scenarios (2 undefined) +6 steps (6 undefined) +271.125µs + +You can implement step definitions for undefined steps with these snippets: + +func iEat(arg1 int) error { + return godog.ErrPending +} + +func thereAreGodogs(arg1 int) error { + return godog.ErrPending +} + +func thereShouldBeNoneRemaining() error { + return godog.ErrPending +} + +func thereShouldBeRemaining(arg1 int) error { + return godog.ErrPending +} + +func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I eat (\d+)$`, iEat) + ctx.Given(`^there are (\d+) godogs$`, thereAreGodogs) + ctx.Then(`^there should be none remaining$`, thereShouldBeNoneRemaining) + ctx.Then(`^there should be (\d+) remaining$`, thereShouldBeRemaining) +} + +--- PASS: TestFeatures (0.00s) +``` + +The third option demonstrates selecting a snippet function using command line arguments. + +```shell +go test ./step_func -test.v -godog.snippet-func gwt_func +``` + +The output should be: + +``` +=== RUN TestFeatures +Feature: eat godogs + In order to be happy + As a hungry gopher + I need to be able to eat godogs +=== RUN TestFeatures/Eat_12_out_of_12 + + Scenario: Eat 12 out of 12 # features/godogs.feature:11 + Given there are 12 godogs +=== RUN TestFeatures/Eat_5_out_of_12 + + Scenario: Eat 5 out of 12 # features/godogs.feature:6 + Given there are 12 godogs + When I eat 12 + Then there should be none remaining + When I eat 5 + Then there should be 7 remaining + +2 scenarios (2 undefined) +6 steps (6 undefined) +427.166µs + +You can implement step definitions for undefined steps with these snippets: + +func iEat(arg1 int) error { + return godog.ErrPending +} + +func thereAreGodogs(arg1 int) error { + return godog.ErrPending +} + +func thereShouldBeNoneRemaining() error { + return godog.ErrPending +} + +func thereShouldBeRemaining(arg1 int) error { + return godog.ErrPending +} + +func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I eat (\d+)$`, iEat) + ctx.Given(`^there are (\d+) godogs$`, thereAreGodogs) + ctx.Then(`^there should be none remaining$`, thereShouldBeNoneRemaining) + ctx.Then(`^there should be (\d+) remaining$`, thereShouldBeRemaining) +} + +--- PASS: TestFeatures (0.00s) + --- PASS: TestFeatures/Eat_12_out_of_12 (0.00s) + --- PASS: TestFeatures/Eat_5_out_of_12 (0.00s) +PASS +ok github.com/cucumber/godog/_examples/snippets/step_func 0.222s +``` \ No newline at end of file diff --git a/_examples/snippets/gwt_func/features/godogs.feature b/_examples/snippets/gwt_func/features/godogs.feature new file mode 100644 index 00000000..101a6a03 --- /dev/null +++ b/_examples/snippets/gwt_func/features/godogs.feature @@ -0,0 +1,14 @@ +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 + + Scenario: Eat 12 out of 12 + Given there are 12 godogs + When I eat 12 + Then there should be none remaining diff --git a/_examples/snippets/gwt_func/godogs.go b/_examples/snippets/gwt_func/godogs.go new file mode 100644 index 00000000..fa7757a7 --- /dev/null +++ b/_examples/snippets/gwt_func/godogs.go @@ -0,0 +1,37 @@ +package gwt_func + +import ( + "fmt" +) + +// Godogs is an example behavior holder. +type Godogs int + +// Add increments Godogs count. +func (g *Godogs) Add(n int) { + *g = *g + Godogs(n) +} + +// Eat decrements Godogs count or fails if there is not enough available. +func (g *Godogs) Eat(n int) error { + ng := Godogs(n) + + if (g == nil && ng > 0) || ng > *g { + return fmt.Errorf("you cannot eat %d godogs, there are %d available", n, g.Available()) + } + + if ng > 0 { + *g = *g - ng + } + + return nil +} + +// Available returns the number of currently available Godogs. +func (g *Godogs) Available() int { + if g == nil { + return 0 + } + + return int(*g) +} diff --git a/_examples/snippets/gwt_func/godogs_test.go b/_examples/snippets/gwt_func/godogs_test.go new file mode 100644 index 00000000..9ac01e25 --- /dev/null +++ b/_examples/snippets/gwt_func/godogs_test.go @@ -0,0 +1,67 @@ +package gwt_func_test + +// This example shows how to set up test suite runner with Go subtests and godog command line parameters. +// Sample commands: +// * run all scenarios from default directory (features): go test -test.run "^TestFeatures/" +// * run all scenarios and list subtest names: go test -test.v -test.run "^TestFeatures/" +// * run all scenarios from one feature file: go test -test.v -godog.paths features/nodogs.feature -test.run "^TestFeatures/" +// * run all scenarios from multiple feature files: go test -test.v -godog.paths features/nodogs.feature,features/godogs.feature -test.run "^TestFeatures/" +// * run single scenario as a subtest: go test -test.v -test.run "^TestFeatures/Eat_5_out_of_12$" +// * show usage help: go test -godog.help +// * show usage help if there were other test files in directory: go test -godog.help godogs_test.go +// * run scenarios with multiple formatters: go test -test.v -godog.format cucumber:cuc.json,pretty -test.run "^TestFeatures/" + +import ( + "context" + "flag" + "os" + "testing" + + "github.com/cucumber/godog" + "github.com/cucumber/godog/_examples/godogs" + "github.com/cucumber/godog/colors" +) + +var opts = godog.Options{ + Output: colors.Colored(os.Stdout), + SnippetFunc: "gwt_func", + Concurrency: 4, +} + +func init() { + godog.BindFlags("godog.", flag.CommandLine, &opts) +} + +func TestFeatures(t *testing.T) { + o := opts + o.TestingT = t + + status := godog.TestSuite{ + Name: "godogs", + Options: &o, + ScenarioInitializer: InitializeScenario, + }.Run() + + if status == 2 { + t.SkipNow() + } + + if status != 0 { + t.Fatalf("zero status code expected, %d received", status) + } +} + +type godogsCtxKey struct{} + +func godogsToContext(ctx context.Context, g godogs.Godogs) context.Context { + return context.WithValue(ctx, godogsCtxKey{}, &g) +} + +func godogsFromContext(ctx context.Context) *godogs.Godogs { + g, _ := ctx.Value(godogsCtxKey{}).(*godogs.Godogs) + + return g +} + +func InitializeScenario(ctx *godog.ScenarioContext) { +} diff --git a/_examples/snippets/step_func/features/godogs.feature b/_examples/snippets/step_func/features/godogs.feature new file mode 100644 index 00000000..101a6a03 --- /dev/null +++ b/_examples/snippets/step_func/features/godogs.feature @@ -0,0 +1,14 @@ +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 + + Scenario: Eat 12 out of 12 + Given there are 12 godogs + When I eat 12 + Then there should be none remaining diff --git a/_examples/snippets/step_func/godogs.go b/_examples/snippets/step_func/godogs.go new file mode 100644 index 00000000..081e9b01 --- /dev/null +++ b/_examples/snippets/step_func/godogs.go @@ -0,0 +1,37 @@ +package step_func + +import ( + "fmt" +) + +// Godogs is an example behavior holder. +type Godogs int + +// Add increments Godogs count. +func (g *Godogs) Add(n int) { + *g = *g + Godogs(n) +} + +// Eat decrements Godogs count or fails if there is not enough available. +func (g *Godogs) Eat(n int) error { + ng := Godogs(n) + + if (g == nil && ng > 0) || ng > *g { + return fmt.Errorf("you cannot eat %d godogs, there are %d available", n, g.Available()) + } + + if ng > 0 { + *g = *g - ng + } + + return nil +} + +// Available returns the number of currently available Godogs. +func (g *Godogs) Available() int { + if g == nil { + return 0 + } + + return int(*g) +} diff --git a/_examples/snippets/step_func/godogs_test.go b/_examples/snippets/step_func/godogs_test.go new file mode 100644 index 00000000..de5ac65c --- /dev/null +++ b/_examples/snippets/step_func/godogs_test.go @@ -0,0 +1,66 @@ +package step_func_test + +// This example shows how to set up test suite runner with Go subtests and godog command line parameters. +// Sample commands: +// * run all scenarios from default directory (features): go test -test.run "^TestFeatures/" +// * run all scenarios and list subtest names: go test -test.v -test.run "^TestFeatures/" +// * run all scenarios from one feature file: go test -test.v -godog.paths features/nodogs.feature -test.run "^TestFeatures/" +// * run all scenarios from multiple feature files: go test -test.v -godog.paths features/nodogs.feature,features/godogs.feature -test.run "^TestFeatures/" +// * run single scenario as a subtest: go test -test.v -test.run "^TestFeatures/Eat_5_out_of_12$" +// * show usage help: go test -godog.help +// * show usage help if there were other test files in directory: go test -godog.help godogs_test.go +// * run scenarios with multiple formatters: go test -test.v -godog.format cucumber:cuc.json,pretty -test.run "^TestFeatures/" + +import ( + "context" + "flag" + "os" + "testing" + + "github.com/cucumber/godog" + "github.com/cucumber/godog/_examples/godogs" + "github.com/cucumber/godog/colors" +) + +var opts = godog.Options{ + Output: colors.Colored(os.Stdout), + Concurrency: 4, +} + +func init() { + godog.BindFlags("godog.", flag.CommandLine, &opts) +} + +func TestFeatures(t *testing.T) { + o := opts + o.TestingT = t + + status := godog.TestSuite{ + Name: "godogs", + Options: &o, + ScenarioInitializer: InitializeScenario, + }.Run() + + if status == 2 { + t.SkipNow() + } + + if status != 0 { + t.Fatalf("zero status code expected, %d received", status) + } +} + +type godogsCtxKey struct{} + +func godogsToContext(ctx context.Context, g godogs.Godogs) context.Context { + return context.WithValue(ctx, godogsCtxKey{}, &g) +} + +func godogsFromContext(ctx context.Context) *godogs.Godogs { + g, _ := ctx.Value(godogsCtxKey{}).(*godogs.Godogs) + + return g +} + +func InitializeScenario(ctx *godog.ScenarioContext) { +} diff --git a/features/snippets.feature b/features/snippets.feature index e5119f79..92f6a6e6 100644 --- a/features/snippets.feature +++ b/features/snippets.feature @@ -3,7 +3,7 @@ Feature: undefined step snippets As a test suite user I need to be able to get undefined step snippets - Scenario: should generate snippets + Scenario: should generate snippets with step_func snippet func when snippet func is not found Given a feature "undefined.feature" file: """ Feature: undefined steps @@ -12,6 +12,7 @@ Feature: undefined step snippets When I send "GET" request to "/version" Then the response code should be 200 """ + And snippet function is: " " When I run feature suite Then the following steps should be undefined: """ @@ -34,6 +35,70 @@ Feature: undefined step snippets } """ + Scenario: should generate snippets with step_func snippet func + Given a feature "undefined.feature" file: + """ + Feature: undefined steps + + Scenario: get version number from api + When I send "GET" request to "/version" + Then the response code should be 200 + """ + And snippet function is: "step_func" + When I run feature suite + Then the following steps should be undefined: + """ + I send "GET" request to "/version" + the response code should be 200 + """ + And the undefined step snippets should be: + """ + func iSendRequestTo(arg1, arg2 string) error { + return godog.ErrPending + } + + func theResponseCodeShouldBe(arg1 int) error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Step(`^I send "([^"]*)" request to "([^"]*)"$`, iSendRequestTo) + ctx.Step(`^the response code should be (\d+)$`, theResponseCodeShouldBe) + } + """ + + Scenario: should generate snippets with gwt_func snippet func + Given a feature "undefined.feature" file: + """ + Feature: undefined steps + + Scenario: get version number from api + When I send "GET" request to "/version" + Then the response code should be 200 + """ + And snippet function is: "gwt_func" + When I run feature suite + Then the following steps should be undefined: + """ + I send "GET" request to "/version" + the response code should be 200 + """ + And the undefined step snippets should be: + """ + func iSendRequestTo(arg1, arg2 string) error { + return godog.ErrPending + } + + func theResponseCodeShouldBe(arg1 int) error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I send "([^"]*)" request to "([^"]*)"$`, iSendRequestTo) + ctx.Then(`^the response code should be (\d+)$`, theResponseCodeShouldBe) + } + """ + Scenario: should generate snippets with more arguments Given a feature "undefined.feature" file: """ @@ -71,6 +136,44 @@ Feature: undefined step snippets } """ + Scenario: should generate snippets with more arguments with gwt_func snippet func + Given a feature "undefined.feature" file: + """ + Feature: undefined steps + + Scenario: get version number from api + When I send "GET" request to "/version" with: + | col1 | val1 | + | col2 | val2 | + Then the response code should be 200 and header "X-Powered-By" should be "godog" + And the response body should be: + \"\"\" + Hello World + \"\"\" + """ + And snippet function is: "gwt_func" + When I run feature suite + Then the undefined step snippets should be: + """ + func iSendRequestToWith(arg1, arg2 string, arg3 *godog.Table) error { + return godog.ErrPending + } + + func theResponseBodyShouldBe(arg1 *godog.DocString) error { + return godog.ErrPending + } + + func theResponseCodeShouldBeAndHeaderShouldBe(arg1 int, arg2, arg3 string) error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I send "([^"]*)" request to "([^"]*)" with:$`, iSendRequestToWith) + ctx.Then(`^the response body should be:$`, theResponseBodyShouldBe) + ctx.Then(`^the response code should be (\d+) and header "([^"]*)" should be "([^"]*)"$`, theResponseCodeShouldBeAndHeaderShouldBe) + } + """ + Scenario: should handle escaped symbols Given a feature "undefined.feature" file: """ @@ -102,6 +205,38 @@ Feature: undefined step snippets } """ + Scenario: should handle escaped symbols with gwt_func snippet func + Given a feature "undefined.feature" file: + """ + Feature: undefined steps + + Scenario: get version number from api + When I pull from github.com + Then the project should be there + """ + And snippet function is: "gwt_func" + When I run feature suite + Then the following steps should be undefined: + """ + I pull from github.com + the project should be there + """ + And the undefined step snippets should be: + """ + func iPullFromGithubcom() error { + return godog.ErrPending + } + + func theProjectShouldBeThere() error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I pull from github\.com$`, iPullFromGithubcom) + ctx.Then(`^the project should be there$`, theProjectShouldBeThere) + } + """ + Scenario: should handle string argument followed by comma Given a feature "undefined.feature" file: """ @@ -128,7 +263,34 @@ Feature: undefined step snippets } """ - Scenario: should handle arguments in the beggining or end of the step + Scenario: should handle string argument followed by comma with gwt_func snippet func + Given a feature "undefined.feature" file: + """ + Feature: undefined + + Scenario: add item to basket + Given there is a "Sith Lord Lightsaber", which costs £5 + When I add the "Sith Lord Lightsaber" to the basket + """ + And snippet function is: "gwt_func" + When I run feature suite + And the undefined step snippets should be: + """ + func iAddTheToTheBasket(arg1 string) error { + return godog.ErrPending + } + + func thereIsAWhichCosts(arg1 string, arg2 int) error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.When(`^I add the "([^"]*)" to the basket$`, iAddTheToTheBasket) + ctx.Given(`^there is a "([^"]*)", which costs £(\d+)$`, thereIsAWhichCosts) + } + """ + + Scenario: should handle arguments in the beginning or end of the step Given a feature "undefined.feature" file: """ Feature: undefined @@ -153,3 +315,30 @@ Feature: undefined step snippets ctx.Step(`^"([^"]*)", which costs £(\d+)$`, whichCosts) } """ + + Scenario: should handle arguments in the beginning or end of the step with gwt_func snippet func + Given a feature "undefined.feature" file: + """ + Feature: undefined + + Scenario: add item to basket + Given "Sith Lord Lightsaber", which costs £5 + And 12 godogs + """ + And snippet function is: "gwt_func" + When I run feature suite + And the undefined step snippets should be: + """ + func godogs(arg1 int) error { + return godog.ErrPending + } + + func whichCosts(arg1 string, arg2 int) error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Given(`^(\d+) godogs$`, godogs) + ctx.Given(`^"([^"]*)", which costs £(\d+)$`, whichCosts) + } + """ diff --git a/flags.go b/flags.go index 14782e16..17f89446 100644 --- a/flags.go +++ b/flags.go @@ -3,6 +3,7 @@ package godog import ( "flag" "fmt" + "github.com/cucumber/godog/internal/snippets" "io" "sort" "strconv" @@ -51,6 +52,12 @@ func FlagSet(opt *Options) *flag.FlagSet { // BindFlags binds godog flags to given flag set prefixed // by given prefix, without overriding usage func BindFlags(prefix string, set *flag.FlagSet, opt *Options) { + var descSnippetFuncOption = "Snippet function to use can be:\n" + for _, snippetFunc := range snippets.List() { + descSnippetFuncOption += s(4) + "- " + colors.Yellow(snippetFunc) + "\n" + } + descSnippetFuncOption += "If no snippet function is provided " + colors.Yellow("step_func") + " is used." + set.Usage = usage(set, set.Output()) descFormatOption := "How to format tests output. Built-in formats:\n" @@ -87,6 +94,11 @@ func BindFlags(prefix string, set *flag.FlagSet, opt *Options) { defTagsOption = opt.Tags } + defSnippetFuncOption := "" + if opt.SnippetFunc != "" { + defSnippetFuncOption = opt.SnippetFunc + } + defConcurrencyOption := 1 if opt.Concurrency != 0 { defConcurrencyOption = opt.Concurrency @@ -114,6 +126,7 @@ func BindFlags(prefix string, set *flag.FlagSet, opt *Options) { set.StringVar(&opt.Format, prefix+"format", defFormatOption, descFormatOption) set.StringVar(&opt.Format, prefix+"f", defFormatOption, descFormatOption) + set.StringVar(&opt.SnippetFunc, prefix+"snippet-func", defSnippetFuncOption, descSnippetFuncOption) set.StringVar(&opt.Tags, prefix+"tags", defTagsOption, descTagsOption) set.StringVar(&opt.Tags, prefix+"t", defTagsOption, descTagsOption) set.IntVar(&opt.Concurrency, prefix+"concurrency", defConcurrencyOption, descConcurrencyOption) diff --git a/fmt.go b/fmt.go index f30f9f89..05517ecf 100644 --- a/fmt.go +++ b/fmt.go @@ -76,33 +76,33 @@ func printStepDefinitions(steps []*models.StepDefinition, w io.Writer) { } // NewBaseFmt creates a new base formatter. -func NewBaseFmt(suite string, out io.Writer) *BaseFmt { - return internal_fmt.NewBase(suite, out) +func NewBaseFmt(suite string, out io.Writer, snippetFunc string) *BaseFmt { + return internal_fmt.NewBase(suite, out, snippetFunc) } // NewProgressFmt creates a new progress formatter. -func NewProgressFmt(suite string, out io.Writer) *ProgressFmt { - return internal_fmt.NewProgress(suite, out) +func NewProgressFmt(suite string, out io.Writer, snippetFunc string) *ProgressFmt { + return internal_fmt.NewProgress(suite, out, snippetFunc) } // NewPrettyFmt creates a new pretty formatter. -func NewPrettyFmt(suite string, out io.Writer) *PrettyFmt { - return &PrettyFmt{Base: NewBaseFmt(suite, out)} +func NewPrettyFmt(suite string, out io.Writer, snippetFunc string) *PrettyFmt { + return &PrettyFmt{Base: NewBaseFmt(suite, out, snippetFunc)} } // NewEventsFmt creates a new event streaming formatter. -func NewEventsFmt(suite string, out io.Writer) *EventsFmt { - return &EventsFmt{Base: NewBaseFmt(suite, out)} +func NewEventsFmt(suite string, out io.Writer, snippetFunc string) *EventsFmt { + return &EventsFmt{Base: NewBaseFmt(suite, out, snippetFunc)} } // NewCukeFmt creates a new Cucumber JSON formatter. -func NewCukeFmt(suite string, out io.Writer) *CukeFmt { - return &CukeFmt{Base: NewBaseFmt(suite, out)} +func NewCukeFmt(suite string, out io.Writer, snippetFunc string) *CukeFmt { + return &CukeFmt{Base: NewBaseFmt(suite, out, snippetFunc)} } // NewJUnitFmt creates a new JUnit formatter. -func NewJUnitFmt(suite string, out io.Writer) *JUnitFmt { - return &JUnitFmt{Base: NewBaseFmt(suite, out)} +func NewJUnitFmt(suite string, out io.Writer, snippetFunc string) *JUnitFmt { + return &JUnitFmt{Base: NewBaseFmt(suite, out, snippetFunc)} } // BaseFmt exports Base formatter. diff --git a/fmt_test.go b/fmt_test.go index 695da886..b6eb6397 100644 --- a/fmt_test.go +++ b/fmt_test.go @@ -62,6 +62,6 @@ func Test_Format(t *testing.T) { assert.NotNil(t, actual) } -func testFormatterFunc(suiteName string, out io.Writer) godog.Formatter { +func testFormatterFunc(suiteName string, out io.Writer, snippetFunc string) godog.Formatter { return nil } diff --git a/formatters/fmt.go b/formatters/fmt.go index aa098b0f..957f4679 100644 --- a/formatters/fmt.go +++ b/formatters/fmt.go @@ -16,7 +16,7 @@ type registeredFormatter struct { var registeredFormatters []*registeredFormatter // FindFmt searches available formatters registered -// and returns FormaterFunc matched by given +// and returns FormatterFunc matched by given // format name or nil otherwise func FindFmt(name string) FormatterFunc { for _, el := range registeredFormatters { @@ -75,7 +75,8 @@ type Formatter interface { // FormatterFunc builds a formatter with given // suite name and io.Writer to record output -type FormatterFunc func(string, io.Writer) Formatter +// snippet string can be provided to use a different snippet generator +type FormatterFunc func(string, io.Writer, string) Formatter // StepDefinition is a registered step definition // contains a StepHandler and regexp which diff --git a/formatters/fmt_test.go b/formatters/fmt_test.go index 186861c6..21a5f78b 100644 --- a/formatters/fmt_test.go +++ b/formatters/fmt_test.go @@ -60,6 +60,6 @@ func Test_Format(t *testing.T) { assert.NotNil(t, actual) } -func testFormatterFunc(suiteName string, out io.Writer) godog.Formatter { +func testFormatterFunc(suiteName string, out io.Writer, snippetFunc string) godog.Formatter { return nil } diff --git a/go.mod b/go.mod index cb94034a..699e5f1d 100644 --- a/go.mod +++ b/go.mod @@ -14,5 +14,6 @@ require ( github.com/cucumber/messages/go/v21 v21.0.1 github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect + golang.org/x/text v0.14.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 768a562f..a540787e 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,38 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 2d4a60d1..ab66fe67 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -1,6 +1,8 @@ package flags import ( + "github.com/cucumber/godog/colors" + "github.com/cucumber/godog/internal/snippets" "github.com/spf13/pflag" ) @@ -14,6 +16,16 @@ func BindRunCmdFlags(prefix string, flagSet *pflag.FlagSet, opts *Options) { opts.Format = "pretty" } + if opts.SnippetFunc == "" { + opts.SnippetFunc = "step_func" + } + + var descSnippetFuncOption = "Snippet function to use can be:\n" + for _, snippetFunc := range snippets.List() { + descSnippetFuncOption += "\t- " + colors.Yellow(snippetFunc) + "\n" + } + descSnippetFuncOption += "If no snippet function is provided " + colors.Yellow("step_func") + " is used.\n" + flagSet.BoolVar(&opts.NoColors, prefix+"no-colors", opts.NoColors, "disable ansi colors") flagSet.IntVarP(&opts.Concurrency, prefix+"concurrency", "c", opts.Concurrency, "run the test suite with concurrency") flagSet.StringVarP(&opts.Tags, prefix+"tags", "t", opts.Tags, `filter scenarios by tags, expression can be: @@ -21,6 +33,7 @@ func BindRunCmdFlags(prefix string, flagSet *pflag.FlagSet, opts *Options) { "~@wip" exclude all scenarios with wip tag "@wip && ~@new" run wip scenarios, but exclude new "@wip,@undone" run wip or undone scenarios`) + flagSet.StringVar(&opts.SnippetFunc, prefix+"snippet-func", opts.SnippetFunc, descSnippetFuncOption) flagSet.StringVarP(&opts.Format, prefix+"format", "f", opts.Format, `will write a report according to the selected formatter usage: diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index de133335..5e62edb9 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -20,6 +20,7 @@ func Test_BindFlagsShouldRespectFlagDefaults(t *testing.T) { assert.Equal(t, "pretty", opts.Format) assert.Equal(t, "", opts.Tags) assert.Equal(t, 1, opts.Concurrency) + assert.Equal(t, "step_func", opts.SnippetFunc) assert.False(t, opts.ShowStepDefinitions) assert.False(t, opts.StopOnFailure) assert.False(t, opts.Strict) @@ -44,6 +45,7 @@ func Test_BindFlagsShouldRespectFlagOverrides(t *testing.T) { flagSet.Parse([]string{ "--optOverrides.format=junit", + "--optOverrides.snippet-func=gwt_func", "--optOverrides.tags=test2", "--optOverrides.concurrency=3", "--optOverrides.definitions=false", @@ -56,6 +58,7 @@ func Test_BindFlagsShouldRespectFlagOverrides(t *testing.T) { assert.Equal(t, "junit", opts.Format) assert.Equal(t, "test2", opts.Tags) assert.Equal(t, 3, opts.Concurrency) + assert.Equal(t, "gwt_func", opts.SnippetFunc) assert.False(t, opts.ShowStepDefinitions) assert.False(t, opts.StopOnFailure) assert.False(t, opts.Strict) diff --git a/internal/flags/options.go b/internal/flags/options.go index fed059a7..7304eef1 100644 --- a/internal/flags/options.go +++ b/internal/flags/options.go @@ -77,6 +77,13 @@ type Options struct { // ShowHelp enables suite to show CLI flags usage help and exit. ShowHelp bool + + // SnippetFunc selects the snippet function to use when generating snippets + // for undefined steps. + // possible values: + // * step_func + // * gwt_func + SnippetFunc string } type Feature struct { diff --git a/internal/formatters/fmt_base.go b/internal/formatters/fmt_base.go index e2dc9bad..28a00fad 100644 --- a/internal/formatters/fmt_base.go +++ b/internal/formatters/fmt_base.go @@ -1,45 +1,45 @@ package formatters import ( - "bytes" "fmt" "io" "os" - "sort" "strconv" "strings" "sync" - "unicode" messages "github.com/cucumber/messages/go/v21" "github.com/cucumber/godog/colors" "github.com/cucumber/godog/formatters" "github.com/cucumber/godog/internal/models" + "github.com/cucumber/godog/internal/snippets" "github.com/cucumber/godog/internal/storage" "github.com/cucumber/godog/internal/utils" ) // BaseFormatterFunc implements the FormatterFunc for the base formatter. -func BaseFormatterFunc(suite string, out io.Writer) formatters.Formatter { - return NewBase(suite, out) +func BaseFormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { + return NewBase(suite, out, snippetFunc) } // NewBase creates a new base formatter. -func NewBase(suite string, out io.Writer) *Base { +func NewBase(suite string, out io.Writer, snippetFunc string) *Base { return &Base{ - suiteName: suite, - indent: 2, - out: out, - Lock: new(sync.Mutex), + snippetFunc: snippets.Find(snippetFunc), + suiteName: suite, + indent: 2, + out: out, + Lock: new(sync.Mutex), } } // Base is a base formatter. type Base struct { - suiteName string - out io.Writer - indent int + suiteName string + out io.Writer + indent int + snippetFunc snippets.Func Storage *storage.Storage Lock *sync.Mutex @@ -185,7 +185,6 @@ func (f *Base) Summary() { 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:")) @@ -195,67 +194,5 @@ func (f *Base) Summary() { // Snippets returns code suggestions for undefined steps. func (f *Base) 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) + return f.snippetFunc(f.Storage) } diff --git a/internal/formatters/fmt_cucumber.go b/internal/formatters/fmt_cucumber.go index 8adbfcba..af79b082 100644 --- a/internal/formatters/fmt_cucumber.go +++ b/internal/formatters/fmt_cucumber.go @@ -28,8 +28,8 @@ func init() { } // CucumberFormatterFunc implements the FormatterFunc for the cucumber formatter -func CucumberFormatterFunc(suite string, out io.Writer) formatters.Formatter { - return &Cuke{Base: NewBase(suite, out)} +func CucumberFormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { + return &Cuke{Base: NewBase(suite, out, snippetFunc)} } // Cuke ... diff --git a/internal/formatters/fmt_events.go b/internal/formatters/fmt_events.go index e264db57..26b648d8 100644 --- a/internal/formatters/fmt_events.go +++ b/internal/formatters/fmt_events.go @@ -18,8 +18,8 @@ func init() { } // EventsFormatterFunc implements the FormatterFunc for the events formatter -func EventsFormatterFunc(suite string, out io.Writer) formatters.Formatter { - return &Events{Base: NewBase(suite, out)} +func EventsFormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { + return &Events{Base: NewBase(suite, out, snippetFunc)} } // Events - Events formatter diff --git a/internal/formatters/fmt_junit.go b/internal/formatters/fmt_junit.go index bc6ed270..44b87909 100644 --- a/internal/formatters/fmt_junit.go +++ b/internal/formatters/fmt_junit.go @@ -18,8 +18,8 @@ func init() { } // JUnitFormatterFunc implements the FormatterFunc for the junit formatter -func JUnitFormatterFunc(suite string, out io.Writer) formatters.Formatter { - return &JUnit{Base: NewBase(suite, out)} +func JUnitFormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { + return &JUnit{Base: NewBase(suite, out, snippetFunc)} } // JUnit renders test results in JUnit format. diff --git a/internal/formatters/fmt_multi.go b/internal/formatters/fmt_multi.go index e23e6ade..8f1518c2 100644 --- a/internal/formatters/fmt_multi.go +++ b/internal/formatters/fmt_multi.go @@ -118,14 +118,14 @@ func (m *MultiFormatter) Add(name string, out io.Writer) { } // FormatterFunc implements the FormatterFunc for the multi formatter. -func (m *MultiFormatter) FormatterFunc(suite string, out io.Writer) formatters.Formatter { +func (m *MultiFormatter) FormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { for _, f := range m.formatters { out := out if f.out != nil { out = f.out } - m.repeater = append(m.repeater, f.fmt(suite, out)) + m.repeater = append(m.repeater, f.fmt(suite, out, snippetFunc)) } return m.repeater diff --git a/internal/formatters/fmt_pretty.go b/internal/formatters/fmt_pretty.go index e7b9e325..026e252d 100644 --- a/internal/formatters/fmt_pretty.go +++ b/internal/formatters/fmt_pretty.go @@ -20,8 +20,8 @@ func init() { } // PrettyFormatterFunc implements the FormatterFunc for the pretty formatter -func PrettyFormatterFunc(suite string, out io.Writer) formatters.Formatter { - return &Pretty{Base: NewBase(suite, out)} +func PrettyFormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { + return &Pretty{Base: NewBase(suite, out, snippetFunc)} } var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>") diff --git a/internal/formatters/fmt_progress.go b/internal/formatters/fmt_progress.go index 23086963..6b7aab33 100644 --- a/internal/formatters/fmt_progress.go +++ b/internal/formatters/fmt_progress.go @@ -16,15 +16,15 @@ func init() { } // ProgressFormatterFunc implements the FormatterFunc for the progress formatter. -func ProgressFormatterFunc(suite string, out io.Writer) formatters.Formatter { - return NewProgress(suite, out) +func ProgressFormatterFunc(suite string, out io.Writer, snippetFunc string) formatters.Formatter { + return NewProgress(suite, out, snippetFunc) } // NewProgress creates a new progress formatter. -func NewProgress(suite string, out io.Writer) *Progress { +func NewProgress(suite string, out io.Writer, snippetFunc string) *Progress { steps := 0 return &Progress{ - Base: NewBase(suite, out), + Base: NewBase(suite, out, snippetFunc), StepsPerRow: 70, Steps: &steps, } diff --git a/internal/models/stepdef_test.go b/internal/models/stepdef_test.go index 6617f48e..b1c1de49 100644 --- a/internal/models/stepdef_test.go +++ b/internal/models/stepdef_test.go @@ -369,7 +369,7 @@ func TestStepDefinition_Run_StringConversionToFunctionType(t *testing.T) { // @TODO maybe we should support duration // fn2 := func(err time.Duration) error { return nil } -// def = &models.StepDefinition{Handler: fn2, HandlerValue: reflect.ValueOf(fn2)} +// def = &formatters.StepDefinition{Handler: fn2, HandlerValue: reflect.ValueOf(fn2)} // def.Args = []interface{}{"1"} // if _, err := def.Run(context.Background()); err == nil { diff --git a/internal/snippets/snippets.go b/internal/snippets/snippets.go new file mode 100644 index 00000000..a101869c --- /dev/null +++ b/internal/snippets/snippets.go @@ -0,0 +1,35 @@ +package snippets + +import "github.com/cucumber/godog/internal/storage" + +// Func defines an interface for functions that render snippets +type Func func(*storage.Storage) string + +var registry map[string]Func + +func init() { + registry = make(map[string]Func) +} + +func register(name string, f Func) { + registry[name] = f +} + +// Find finds a registered snippet function and returns it +// +// If the snippet function is not found, the original function is returned +func Find(name string) Func { + f, ok := registry[name] + if ok { + return f + } + return StepFunc +} + +func List() []string { + result := make([]string, 0, len(registry)) + for n := range registry { + result = append(result, n) + } + return result +} diff --git a/internal/snippets/step_func_snippet.go b/internal/snippets/step_func_snippet.go new file mode 100644 index 00000000..98e45791 --- /dev/null +++ b/internal/snippets/step_func_snippet.go @@ -0,0 +1,115 @@ +package snippets + +import ( + "bytes" + "fmt" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "sort" + "strings" + "text/template" + "unicode" + + messages "github.com/cucumber/messages/go/v21" + + "github.com/cucumber/godog/internal/models" + "github.com/cucumber/godog/internal/storage" +) + +// init registers the snippet functions +func init() { + register("step_func", StepFunc) + register("gwt_func", GwtFunc) +} + +// StepFunc renders a snippet using Step keywords with empty functions +func StepFunc(s *storage.Storage) string { + return BaseFunc(s, undefinedStepFuncSnippetsTpl) +} + +// GwtFunc renders a snippet using Given/When/Then keywords +func GwtFunc(s *storage.Storage) string { + return BaseFunc(s, undefinedGwtFuncSnippetsTpl) +} + +// BaseFunc renders a snippet with the provided template +func BaseFunc(s *storage.Storage, tpl *template.Template) string { + undefinedStepResults := s.MustGetPickleStepResultsByStatus(models.Undefined) + if len(undefinedStepResults) == 0 { + return "" + } + + var index int + var snips []undefinedSnippet + // build snippets + for _, u := range undefinedStepResults { + pickleStep := s.MustGetPickleStep(u.PickleStepID) + + steps := []string{pickleStep.Text} + arg := pickleStep.Argument + if u.Def != nil { + steps = u.Def.Undefined + arg = nil + } + + // Not sure if range is needed... don't understand it yet. + for _, step := range steps { + var stepType string + + switch pickleStep.Type { + case messages.PickleStepType_ACTION: + stepType = "When" + case messages.PickleStepType_CONTEXT: + stepType = "Given" + case messages.PickleStepType_OUTCOME: + stepType = "Then" + default: + stepType = "Step" + } + + 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 = cases.Title(language.English).String(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, Type: stepType, Expr: expr, argument: arg}) + } + } + } + + sort.Sort(snippetSortByMethod(snips)) + + var buf bytes.Buffer + if err := tpl.Execute(&buf, snips); err != nil { + panic(err) + } + // there may be trailing spaces + return strings.Replace(buf.String(), " \n", "\n", -1) +} diff --git a/internal/formatters/undefined_snippets_gen.go b/internal/snippets/undefined_snippets_gen.go similarity index 82% rename from internal/formatters/undefined_snippets_gen.go rename to internal/snippets/undefined_snippets_gen.go index ff6cd79e..634fca16 100644 --- a/internal/formatters/undefined_snippets_gen.go +++ b/internal/snippets/undefined_snippets_gen.go @@ -1,4 +1,4 @@ -package formatters +package snippets import ( "fmt" @@ -22,7 +22,7 @@ var snippetHelperFuncs = template.FuncMap{ }, } -var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(` +var undefinedStepFuncSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(` {{ range . }}func {{ .Method }}({{ .Args }}) error { return godog.ErrPending } @@ -32,9 +32,20 @@ var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetH } `)) +var undefinedGwtFuncSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(` +{{ range . }}func {{ .Method }}({{ .Args }}) error { + return godog.ErrPending +} + +{{end}}func InitializeScenario(ctx *godog.ScenarioContext) { {{ range . }} + ctx.{{ .Type }}({{ backticked .Expr }}, {{ .Method }}){{end}} +} +`)) + type undefinedSnippet struct { Method string Expr string + Type string argument *messages.PickleStepArgument } @@ -44,7 +55,9 @@ func (s undefinedSnippet) Args() (ret string) { pos int breakLoop bool ) + /* + */ for !breakLoop { part := s.Expr[pos:] ipos := strings.Index(part, "(\\d+)") diff --git a/run.go b/run.go index 09fdee07..2fafa0e8 100644 --- a/run.go +++ b/run.go @@ -234,7 +234,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int { opt.Concurrency = 1 } - runner.fmt = multiFmt.FormatterFunc(suiteName, output) + runner.fmt = multiFmt.FormatterFunc(suiteName, output, opt.SnippetFunc) opt.FS = storage.FS{FS: opt.FS} if len(opt.FeatureContents) > 0 { diff --git a/run_progress_test.go b/run_progress_test.go index e11c8648..7314f8fd 100644 --- a/run_progress_test.go +++ b/run_progress_test.go @@ -38,7 +38,7 @@ func Test_ProgressFormatterWhenStepPanics(t *testing.T) { var buf bytes.Buffer w := colors.Uncolored(&buf) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", w), + fmt: formatters.ProgressFormatterFunc("progress", w, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^one$`, func() error { return nil }) @@ -72,7 +72,7 @@ func Test_ProgressFormatterWithPanicInMultistep(t *testing.T) { var buf bytes.Buffer w := colors.Uncolored(&buf) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", w), + fmt: formatters.ProgressFormatterFunc("progress", w, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^sub1$`, func() error { return nil }) @@ -106,7 +106,7 @@ func Test_ProgressFormatterMultistepTemplates(t *testing.T) { var buf bytes.Buffer w := colors.Uncolored(&buf) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", w), + fmt: formatters.ProgressFormatterFunc("progress", w, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^sub-sub$`, func() error { return nil }) @@ -182,7 +182,7 @@ Feature: basic var buf bytes.Buffer w := colors.Uncolored(&buf) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", w), + fmt: formatters.ProgressFormatterFunc("progress", w, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^one$`, func() error { return nil }) @@ -226,7 +226,7 @@ Feature: basic var buf bytes.Buffer w := colors.Uncolored(&buf) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", w), + fmt: formatters.ProgressFormatterFunc("progress", w, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^one$`, func() error { return nil }) diff --git a/run_test.go b/run_test.go index 94f71744..5d761cee 100644 --- a/run_test.go +++ b/run_test.go @@ -83,7 +83,7 @@ func Test_FailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) { var beforeScenarioFired, afterScenarioFired int r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", ioutil.Discard), + fmt: formatters.ProgressFormatterFunc("progress", ioutil.Discard, ""), features: []*models.Feature{&ft}, testSuiteInitializer: func(ctx *TestSuiteContext) { ctx.ScenarioContext().Before(func(ctx context.Context, sc *Scenario) (context.Context, error) { @@ -135,7 +135,7 @@ func Test_FailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) { ft.Pickles = gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", ioutil.Discard), + fmt: formatters.ProgressFormatterFunc("progress", ioutil.Discard, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^one$`, func() error { return nil }) @@ -168,7 +168,7 @@ func Test_ShouldFailOnError(t *testing.T) { ft.Pickles = gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId) r := runner{ - fmt: formatters.ProgressFormatterFunc("progress", ioutil.Discard), + fmt: formatters.ProgressFormatterFunc("progress", ioutil.Discard, ""), features: []*models.Feature{&ft}, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^two$`, func() error { return fmt.Errorf("error") }) @@ -525,11 +525,11 @@ func Test_AllFeaturesRun(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -...... 356 +.................................. 384 -94 scenarios (94 passed) -356 steps (356 passed) +100 scenarios (100 passed) +384 steps (384 passed) 0s ` @@ -553,11 +553,11 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -...... 356 +.................................. 384 -94 scenarios (94 passed) -356 steps (356 passed) +100 scenarios (100 passed) +384 steps (384 passed) 0s ` diff --git a/suite_context_test.go b/suite_context_test.go index 6df223b7..42bd72cc 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -49,6 +49,7 @@ func InitializeScenario(ctx *ScenarioContext) { ctx.Step(`^I run feature suite with formatter "([^"]*)"$`, tc.iRunFeatureSuiteWithFormatter) ctx.Step(`^(?:I )(allow|disable) variable injection`, tc.iSetVariableInjectionTo) ctx.Step(`^(?:a )?feature "([^"]*)"(?: file)?:$`, tc.aFeatureFile) + ctx.Step(`^snippet function is: "([^"]*)"$`, tc.snippetFunctionIs) ctx.Step(`^the suite should have (passed|failed)$`, tc.theSuiteShouldHave) ctx.Step(`^I should have ([\d]+) features? files?:$`, tc.iShouldHaveNumFeatureFiles) @@ -218,6 +219,7 @@ type godogFeaturesScenario struct { features []*models.Feature testedSuite *suite testSuiteContext TestSuiteContext + snippetFunc string events []*firedEvent out bytes.Buffer allowInjection bool @@ -239,6 +241,10 @@ func (tc *godogFeaturesScenario) ResetBeforeEachScenario(ctx context.Context, sc return ctx, nil } +func (tc *godogFeaturesScenario) snippetFunctionIs(snippetFunc string) { + tc.snippetFunc = snippetFunc +} + func (tc *godogFeaturesScenario) iSetVariableInjectionTo(to string) error { tc.allowInjection = to == "allow" return nil @@ -275,7 +281,7 @@ func (tc *godogFeaturesScenario) iRunFeatureSuiteWithTagsAndFormatter(filter str } } - tc.testedSuite.fmt = fmtFunc("godog", colors.Uncolored(&tc.out)) + tc.testedSuite.fmt = fmtFunc("godog", colors.Uncolored(&tc.out), tc.snippetFunc) if fmt, ok := tc.testedSuite.fmt.(storageFormatter); ok { fmt.SetStorage(tc.testedSuite.storage) }