diff --git a/README.md b/README.md index 17daa59f..8d6a6ce7 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,6 @@ import ( "fmt" "github.com/cucumber/godog" - messages "github.com/cucumber/messages-go/v10" ) func thereAreGodogs(available int) error { @@ -183,12 +182,12 @@ func thereShouldBeRemaining(remaining int) error { return nil } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { s.Step(`^there are (\d+) godogs$`, thereAreGodogs) s.Step(`^I eat (\d+)$`, iEat) s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) - s.BeforeScenario(func(*messages.Pickle) { + s.BeforeScenario(func(*godog.Scenario) { Godogs = 0 // clean the state before every scenario }) } @@ -250,22 +249,24 @@ The following example binds **godog** flags with specified prefix `godog` in order to prevent flag collisions. ``` go -var opt = godog.Options{ +var opts = godog.Options{ Output: colors.Colored(os.Stdout), Format: "progress", // can define default values } func init() { - godog.BindFlags("godog.", flag.CommandLine, &opt) + godog.BindFlags("godog.", flag.CommandLine, &opts) } func TestMain(m *testing.M) { flag.Parse() - opt.Paths = flag.Args() + opts.Paths = flag.Args() - status := godog.RunWithOptions("godogs", func(s *godog.Suite) { - FeatureContext(s) - }, opt) + status := godog.TestSuite{ + Name: "godogs", + ScenarioInitializer: ScenarioContext, + Options: &opts, + }.Run() if st := m.Run(); st > status { status = st @@ -287,13 +288,17 @@ configuring needed options. ``` go func TestMain(m *testing.M) { - status := godog.RunWithOptions("godog", func(s *godog.Suite) { - FeatureContext(s) - }, godog.Options{ + opts := godog.Options{ Format: "progress", Paths: []string{"features"}, Randomize: time.Now().UTC().UnixNano(), // randomize scenario execution order - }) + } + + status := godog.TestSuite{ + Name: "godogs", + ScenarioInitializer: ScenarioContext, + Options: opts, + }.Run() if st := m.Run(); st > status { status = st @@ -315,12 +320,17 @@ func TestMain(m *testing.M) { break } } - status := godog.RunWithOptions("godog", func(s *godog.Suite) { - godog.SuiteContext(s) - }, godog.Options{ + + opts := godog.Options{ Format: format, Paths: []string{"features"}, - }) + } + + status := godog.TestSuite{ + Name: "godogs", + ScenarioInitializer: ScenarioContext, + Options: opts, + }.Run() if st := m.Run(); st > status { status = st diff --git a/_examples/api/README.md b/_examples/api/README.md index 387fa68c..b5821bf4 100644 --- a/_examples/api/README.md +++ b/_examples/api/README.md @@ -56,7 +56,6 @@ need to store state within steps (a response), we should introduce a structure w package main import ( - "github.com/cucumber/gherkin-go/v11" "github.com/cucumber/godog" ) @@ -71,11 +70,11 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error { return godog.ErrPending } -func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error { +func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) error { return godog.ErrPending } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { api := &apiFeature{} s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo) s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe) @@ -98,7 +97,6 @@ import ( "net/http" "net/http/httptest" - "github.com/cucumber/gherkin-go/v11" "github.com/cucumber/godog" ) @@ -142,7 +140,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error { return nil } -func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error { +func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) error { var expected, actual []byte var data interface{} if err = json.Unmarshal([]byte(body.Content), &data); err != nil { @@ -158,7 +156,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen return } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { api := &apiFeature{} s.BeforeScenario(api.resetResponse) diff --git a/_examples/api/api_test.go b/_examples/api/api_test.go index 89cfbc6f..1c292c23 100644 --- a/_examples/api/api_test.go +++ b/_examples/api/api_test.go @@ -8,14 +8,13 @@ import ( "reflect" "github.com/cucumber/godog" - "github.com/cucumber/messages-go/v10" ) type apiFeature struct { resp *httptest.ResponseRecorder } -func (a *apiFeature) resetResponse(*messages.Pickle) { +func (a *apiFeature) resetResponse(*godog.Scenario) { a.resp = httptest.NewRecorder() } @@ -51,7 +50,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error { return nil } -func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) (err error) { +func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) { var expected, actual interface{} // re-encode expected response @@ -71,7 +70,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen return nil } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { api := &apiFeature{} s.BeforeScenario(api.resetResponse) diff --git a/_examples/assert-godogs/features/godogs.feature b/_examples/assert-godogs/features/godogs.feature index d333d55e..857bf822 100644 --- a/_examples/assert-godogs/features/godogs.feature +++ b/_examples/assert-godogs/features/godogs.feature @@ -6,10 +6,10 @@ Feature: eat godogs Scenario: Eat 5 out of 12 Given there are 12 godogs - When I eat 4 + When I eat 5 Then there should be 7 remaining Scenario: Eat 12 out of 12 Given there are 12 godogs - When I eat 11 + When I eat 12 Then there should be none remaining diff --git a/_examples/assert-godogs/godogs_test.go b/_examples/assert-godogs/godogs_test.go index b59ba5ce..559aee99 100644 --- a/_examples/assert-godogs/godogs_test.go +++ b/_examples/assert-godogs/godogs_test.go @@ -1,4 +1,3 @@ -/* file: $GOPATH/src/assert-godogs/godogs_test.go */ package main import ( @@ -9,23 +8,24 @@ import ( "github.com/cucumber/godog" "github.com/cucumber/godog/colors" - messages "github.com/cucumber/messages-go/v10" "github.com/stretchr/testify/assert" ) -var opt = godog.Options{Output: colors.Colored(os.Stdout)} +var opts = godog.Options{Output: colors.Colored(os.Stdout)} func init() { - godog.BindFlags("godog.", flag.CommandLine, &opt) + godog.BindFlags("godog.", flag.CommandLine, &opts) } func TestMain(m *testing.M) { flag.Parse() - opt.Paths = flag.Args() + opts.Paths = flag.Args() - status := godog.RunWithOptions("godogs", func(s *godog.Suite) { - FeatureContext(s) - }, opt) + status := godog.TestSuite{ + Name: "godogs", + ScenarioInitializer: ScenarioContext, + Options: &opts, + }.Run() if st := m.Run(); st > status { status = st @@ -65,13 +65,13 @@ func thereShouldBeNoneRemaining() error { ) } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { s.Step(`^there are (\d+) godogs$`, thereAreGodogs) s.Step(`^I eat (\d+)$`, iEat) s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) s.Step(`^there should be none remaining$`, thereShouldBeNoneRemaining) - s.BeforeScenario(func(*messages.Pickle) { + s.BeforeScenario(func(*godog.Scenario) { Godogs = 0 // clean the state before every scenario }) } diff --git a/_examples/db/api_test.go b/_examples/db/api_test.go index 7d51d722..ec765841 100644 --- a/_examples/db/api_test.go +++ b/_examples/db/api_test.go @@ -11,7 +11,6 @@ import ( txdb "github.com/DATA-DOG/go-txdb" "github.com/cucumber/godog" - "github.com/cucumber/messages-go/v10" ) func init() { @@ -24,7 +23,7 @@ type apiFeature struct { resp *httptest.ResponseRecorder } -func (a *apiFeature) resetResponse(*messages.Pickle) { +func (a *apiFeature) resetResponse(*godog.Scenario) { a.resp = httptest.NewRecorder() if a.db != nil { a.db.Close() @@ -71,7 +70,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error { return nil } -func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) (err error) { +func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) { var expected, actual interface{} // re-encode expected response @@ -91,7 +90,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen return nil } -func (a *apiFeature) thereAreUsers(users *messages.PickleStepArgument_PickleTable) error { +func (a *apiFeature) thereAreUsers(users *godog.Table) error { var fields []string var marks []string head := users.Rows[0].Cells @@ -123,7 +122,7 @@ func (a *apiFeature) thereAreUsers(users *messages.PickleStepArgument_PickleTabl return nil } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { api := &apiFeature{} s.BeforeScenario(api.resetResponse) diff --git a/_examples/godogs/godogs_test.go b/_examples/godogs/godogs_test.go index fac78c5b..4e5cea57 100644 --- a/_examples/godogs/godogs_test.go +++ b/_examples/godogs/godogs_test.go @@ -1,4 +1,3 @@ -/* file: $GOPATH/src/godogs/godogs_test.go */ package main import ( @@ -9,22 +8,23 @@ import ( "github.com/cucumber/godog" "github.com/cucumber/godog/colors" - messages "github.com/cucumber/messages-go/v10" ) -var opt = godog.Options{Output: colors.Colored(os.Stdout)} +var opts = godog.Options{Output: colors.Colored(os.Stdout)} func init() { - godog.BindFlags("godog.", flag.CommandLine, &opt) + godog.BindFlags("godog.", flag.CommandLine, &opts) } func TestMain(m *testing.M) { flag.Parse() - opt.Paths = flag.Args() + opts.Paths = flag.Args() - status := godog.RunWithOptions("godogs", func(s *godog.Suite) { - FeatureContext(s) - }, opt) + status := godog.TestSuite{ + Name: "godogs", + ScenarioInitializer: ScenarioContext, + Options: &opts, + }.Run() if st := m.Run(); st > status { status = st @@ -52,12 +52,12 @@ func thereShouldBeRemaining(remaining int) error { return nil } -func FeatureContext(s *godog.Suite) { +func ScenarioContext(s *godog.ScenarioContext) { s.Step(`^there are (\d+) godogs$`, thereAreGodogs) s.Step(`^I eat (\d+)$`, iEat) s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) - s.BeforeScenario(func(*messages.Pickle) { + s.BeforeScenario(func(*godog.Scenario) { Godogs = 0 // clean the state before every scenario }) } diff --git a/ast.go b/ast.go index fe275e03..6dba3d08 100644 --- a/ast.go +++ b/ast.go @@ -2,7 +2,7 @@ package godog import "go/ast" -func astContexts(f *ast.File) []string { +func astContexts(f *ast.File, selectName string) []string { var contexts []string for _, d := range f.Decls { switch fun := d.(type) { @@ -12,13 +12,13 @@ func astContexts(f *ast.File) []string { case *ast.StarExpr: switch x := expr.X.(type) { case *ast.Ident: - if x.Name == "Suite" { + if x.Name == selectName { contexts = append(contexts, fun.Name.Name) } case *ast.SelectorExpr: switch t := x.X.(type) { case *ast.Ident: - if t.Name == "godog" && x.Sel.Name == "Suite" { + if t.Name == "godog" && x.Sel.Name == selectName { contexts = append(contexts, fun.Name.Name) } } diff --git a/ast_test.go b/ast_test.go index 799ce789..d1454af7 100644 --- a/ast_test.go +++ b/ast_test.go @@ -34,7 +34,7 @@ func astContextParse(src string, t *testing.T) []string { t.Fatalf("unexpected error while parsing ast: %v", err) } - return astContexts(f) + return astContexts(f, "Suite") } func TestShouldGetSingleContextFromSource(t *testing.T) { diff --git a/builder.go b/builder.go index 64414b7b..09a2ae7a 100644 --- a/builder.go +++ b/builder.go @@ -30,28 +30,51 @@ var ( import ( "github.com/cucumber/godog" - {{if .Contexts}}_test "{{.ImportPath}}"{{end}} - {{if .XContexts}}_xtest "{{.ImportPath}}_test"{{end}} - {{if .XContexts}}"testing/internal/testdeps"{{end}} + {{if or .DeprecatedFeatureContexts .TestSuiteContexts .ScenarioContexts}}_test "{{.ImportPath}}"{{end}} + {{if or .XDeprecatedFeatureContexts .XTestSuiteContexts .XScenarioContexts}}_xtest "{{.ImportPath}}_test"{{end}} + {{if or .XDeprecatedFeatureContexts .XTestSuiteContexts .XScenarioContexts}}"testing/internal/testdeps"{{end}} "os" ) -{{if .XContexts}} +{{if or .XDeprecatedFeatureContexts .XTestSuiteContexts .XScenarioContexts}} func init() { testdeps.ImportPath = "{{.ImportPath}}" } {{end}} func main() { + {{if or .TestSuiteContexts .ScenarioContexts .XTestSuiteContexts .XScenarioContexts}} + status := godog.TestSuite{ + Name: "{{ .Name }}", + TestSuiteInitializer: func (ctx *godog.TestSuiteContext) { + os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}") + {{range .TestSuiteContexts}} + _test.{{ . }}(ctx) + {{end}} + {{range .XTestSuiteContexts}} + _xtest.{{ . }}(ctx) + {{end}} + }, + ScenarioInitializer: func (ctx *godog.ScenarioContext) { + {{range .ScenarioContexts}} + _test.{{ . }}(ctx) + {{end}} + {{range .XScenarioContexts}} + _xtest.{{ . }}(ctx) + {{end}} + }, + }.Run() + {{else}} status := godog.Run("{{ .Name }}", func (suite *godog.Suite) { os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}") - {{range .Contexts}} + {{range .DeprecatedFeatureContexts}} _test.{{ . }}(suite) {{end}} - {{range .XContexts}} + {{range .XDeprecatedFeatureContexts}} _xtest.{{ . }}(suite) {{end}} }) + {{end}} os.Exit(status) }`)) @@ -308,35 +331,44 @@ func buildTempFile(pkg *build.Package) ([]byte, error) { // and produces a testmain source code. func buildTestMain(pkg *build.Package) ([]byte, error) { var ( - contexts []string - xcontexts []string - err error - name, importPath string + ctxs, xctxs contexts + err error + name = "main" + importPath string ) + if nil != pkg { - contexts, err = processPackageTestFiles(pkg.TestGoFiles) - if err != nil { + if ctxs, err = processPackageTestFiles(pkg.TestGoFiles); err != nil { return nil, err } - xcontexts, err = processPackageTestFiles(pkg.XTestGoFiles) - if err != nil { + + if xctxs, err = processPackageTestFiles(pkg.XTestGoFiles); err != nil { return nil, err } + importPath = parseImport(pkg.ImportPath, pkg.Root) name = pkg.Name } else { name = "main" } data := struct { - Name string - Contexts []string - XContexts []string - ImportPath string + Name string + ImportPath string + DeprecatedFeatureContexts []string + TestSuiteContexts []string + ScenarioContexts []string + XDeprecatedFeatureContexts []string + XTestSuiteContexts []string + XScenarioContexts []string }{ - Name: name, - Contexts: contexts, - XContexts: xcontexts, - ImportPath: importPath, + Name: name, + ImportPath: importPath, + DeprecatedFeatureContexts: ctxs.deprecatedFeatureCtxs, + TestSuiteContexts: ctxs.testSuiteCtxs, + ScenarioContexts: ctxs.scenarioCtxs, + XDeprecatedFeatureContexts: xctxs.deprecatedFeatureCtxs, + XTestSuiteContexts: xctxs.testSuiteCtxs, + XScenarioContexts: xctxs.scenarioCtxs, } var buf bytes.Buffer @@ -380,11 +412,38 @@ func parseImport(rawPath, rootPath string) string { return mod.Path + filepath.ToSlash(strings.TrimPrefix(rawPath, normaliseLocalImportPath(mod.Dir))) } +type contexts struct { + deprecatedFeatureCtxs []string + testSuiteCtxs []string + scenarioCtxs []string +} + +func (ctxs contexts) validate() error { + var allCtxs []string + allCtxs = append(allCtxs, ctxs.deprecatedFeatureCtxs...) + allCtxs = append(allCtxs, ctxs.testSuiteCtxs...) + allCtxs = append(allCtxs, ctxs.scenarioCtxs...) + + var failed []string + for _, ctx := range allCtxs { + runes := []rune(ctx) + if unicode.IsLower(runes[0]) { + expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...) + failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected))) + } + } + + if len(failed) > 0 { + return fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t")) + } + + return nil +} + // processPackageTestFiles runs through ast of each test // file pack and looks for godog suite contexts to register // on run -func processPackageTestFiles(packs ...[]string) ([]string, error) { - var ctxs []string +func processPackageTestFiles(packs ...[]string) (ctxs contexts, _ error) { fset := token.NewFileSet() for _, pack := range packs { for _, testFile := range pack { @@ -393,21 +452,13 @@ func processPackageTestFiles(packs ...[]string) ([]string, error) { return ctxs, err } - ctxs = append(ctxs, astContexts(node)...) - } - } - var failed []string - for _, ctx := range ctxs { - runes := []rune(ctx) - if unicode.IsLower(runes[0]) { - expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...) - failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected))) + ctxs.deprecatedFeatureCtxs = append(ctxs.deprecatedFeatureCtxs, astContexts(node, "Suite")...) + ctxs.testSuiteCtxs = append(ctxs.testSuiteCtxs, astContexts(node, "TestSuiteContext")...) + ctxs.scenarioCtxs = append(ctxs.scenarioCtxs, astContexts(node, "ScenarioContext")...) } } - if len(failed) > 0 { - return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t")) - } - return ctxs, nil + + return ctxs, ctxs.validate() } func findToolDir() string { diff --git a/run.go b/run.go index f50851b7..2b2ee554 100644 --- a/run.go +++ b/run.go @@ -20,6 +20,8 @@ const ( ) type initializer func(*Suite) +type testSuiteInitializer func(*TestSuiteContext) +type scenarioInitializer func(*ScenarioContext) type runner struct { randomSeed int64 @@ -27,6 +29,8 @@ type runner struct { features []*feature fmt Formatter initializer initializer + testSuiteInitializer testSuiteInitializer + scenarioInitializer scenarioInitializer } func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool) { @@ -38,8 +42,17 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool useFmtCopy = true } + testSuiteContext := TestSuiteContext{} + if r.testSuiteInitializer != nil { + r.testSuiteInitializer(&testSuiteContext) + } r.fmt.TestRunStarted() + // run before suite handlers + for _, f := range testSuiteContext.beforeSuiteHandlers { + f() + } + queue := make(chan int, rate) for i, ft := range r.features { queue <- i // reserve space in queue @@ -74,7 +87,14 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool suite.fmt = r.fmt } - r.initializer(suite) + if r.initializer != nil { + r.initializer(suite) + } + + if r.scenarioInitializer != nil { + suite.scenarioInitializer = r.scenarioInitializer + } + suite.run() if suite.failed { copyLock.Lock() @@ -105,6 +125,11 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool } close(queue) + // run after suite handlers + for _, f := range testSuiteContext.afterSuiteHandlers { + f() + } + // print summary r.fmt.Summary() return @@ -126,7 +151,22 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool // // If there are flag related errors they will // be directed to os.Stderr -func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Options) int { +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. +// Use: +// godog.TestSuite{ +// Name: name, +// TestSuiteInitializer: testSuiteInitializer, +// ScenarioInitializer: scenarioInitializer, +// Options: &opts, +// }.Run() +// instead. +func RunWithOptions(suite string, initializer func(*Suite), opt Options) int { + return runWithOptions(suite, runner{initializer: initializer}, opt) +} + +func runWithOptions(suite string, runner runner, opt Options) int { var output io.Writer = os.Stdout if nil != opt.Output { output = opt.Output @@ -140,7 +180,7 @@ func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Opt if opt.ShowStepDefinitions { s := &Suite{} - contextInitializer(s) + runner.initializer(s) s.printStepDefinitions(output) return exitOptionError } @@ -169,35 +209,30 @@ func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Opt )) return exitOptionError } + runner.fmt = formatter(suite, output) - features, err := parseFeatures(opt.Tags, opt.Paths) - if err != nil { + var err error + if runner.features, err = parseFeatures(opt.Tags, opt.Paths); err != nil { fmt.Fprintln(os.Stderr, err) return exitOptionError } // user may have specified -1 option to create random seed - randomize := opt.Randomize - if randomize == -1 { - randomize = makeRandomSeed() + runner.randomSeed = opt.Randomize + if runner.randomSeed == -1 { + runner.randomSeed = makeRandomSeed() } - r := runner{ - fmt: formatter(suite, output), - initializer: contextInitializer, - features: features, - randomSeed: randomize, - stopOnFailure: opt.StopOnFailure, - strict: opt.Strict, - } + runner.stopOnFailure = opt.StopOnFailure + runner.strict = opt.Strict // store chosen seed in environment, so it could be seen in formatter summary report - os.Setenv("GODOG_SEED", strconv.FormatInt(r.randomSeed, 10)) + os.Setenv("GODOG_SEED", strconv.FormatInt(runner.randomSeed, 10)) // determine tested package _, filename, _, _ := runtime.Caller(1) os.Setenv("GODOG_TESTED_PACKAGE", runsFromPackage(filename)) - failed := r.concurrent(opt.Concurrency, func() Formatter { return formatter(suite, output) }) + failed := runner.concurrent(opt.Concurrency, func() Formatter { return formatter(suite, output) }) // @TODO: should prevent from having these os.Setenv("GODOG_SEED", "") @@ -240,9 +275,20 @@ func runsFromPackage(fp string) string { // // If there are flag related errors they will // be directed to os.Stderr -func Run(suite string, contextInitializer func(suite *Suite)) int { +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. +// Use: +// godog.TestSuite{ +// Name: name, +// TestSuiteInitializer: testSuiteInitializer, +// ScenarioInitializer: scenarioInitializer, +// }.Run() +// instead. +func Run(suite string, initializer func(*Suite)) int { var opt Options opt.Output = colors.Colored(os.Stdout) + flagSet := FlagSet(&opt) if err := flagSet.Parse(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) @@ -251,5 +297,44 @@ func Run(suite string, contextInitializer func(suite *Suite)) int { opt.Paths = flagSet.Args() - return RunWithOptions(suite, contextInitializer, opt) + return RunWithOptions(suite, initializer, opt) +} + +// TestSuite allows for configuration +// of the Test Suite Execution +type TestSuite struct { + Name string + TestSuiteInitializer func(*TestSuiteContext) + ScenarioInitializer func(*ScenarioContext) + Options *Options +} + +// Run will execute the test suite. +// +// If options are not set, it will reads +// all configuration options from flags. +// +// The exit codes may vary from: +// 0 - success +// 1 - failed +// 2 - command line usage error +// 128 - or higher, os signal related error exit codes +// +// If there are flag related errors they will be directed to os.Stderr +func (ts TestSuite) Run() int { + if ts.Options == nil { + ts.Options = &Options{} + ts.Options.Output = colors.Colored(os.Stdout) + + flagSet := FlagSet(ts.Options) + if err := flagSet.Parse(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + return exitOptionError + } + + ts.Options.Paths = flagSet.Args() + } + + r := runner{testSuiteInitializer: ts.TestSuiteInitializer, scenarioInitializer: ts.ScenarioInitializer} + return runWithOptions(ts.Name, r, *ts.Options) } diff --git a/suite.go b/suite.go index 06667785..aa874fcd 100644 --- a/suite.go +++ b/suite.go @@ -165,6 +165,10 @@ var ErrPending = fmt.Errorf("step implementation is pending") // in order to have a trace information. Only step // executions are catching panic error since it may // be a context specific error. +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. +// This struct will therefore not be exported in the future. type Suite struct { steps []*StepDefinition features []*feature @@ -175,6 +179,8 @@ type Suite struct { stopOnFailure bool strict bool + scenarioInitializer scenarioInitializer + // suite event handlers beforeSuiteHandlers []func() beforeFeatureHandlers []func(*messages.GherkinDocument) @@ -201,6 +207,10 @@ type Suite struct { // If none of the *StepDefinition is matched, then // ErrUndefined error will be returned when // running steps. +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *ScenarioContext) Step instead. func (s *Suite) Step(expr interface{}, stepFunc interface{}) { var regex *regexp.Regexp @@ -254,6 +264,10 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { // // Use it to prepare the test suite for a spin. // Connect and prepare database for instance... +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *TestSuiteContext) BeforeSuite instead. func (s *Suite) BeforeSuite(fn func()) { s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, fn) } @@ -285,12 +299,20 @@ func (s *Suite) BeforeFeature(fn func(*messages.GherkinDocument)) { // It is a good practice to restore the default state // before every scenario so it would be isolated from // any kind of state. +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *ScenarioContext) BeforeScenario instead. func (s *Suite) BeforeScenario(fn func(*messages.Pickle)) { s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, fn) } // BeforeStep registers a function or method // to be run before every step. +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *ScenarioContext) BeforeStep instead. func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) { s.beforeStepHandlers = append(s.beforeStepHandlers, fn) } @@ -304,12 +326,20 @@ func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) { // // In some cases, for example when running a headless // browser, to take a screenshot after failure. +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *ScenarioContext) AfterStep instead. func (s *Suite) AfterStep(fn func(*messages.Pickle_PickleStep, error)) { s.afterStepHandlers = append(s.afterStepHandlers, fn) } // AfterScenario registers an function or method // to be run after every pickle. +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *ScenarioContext) AfterScenario instead. func (s *Suite) AfterScenario(fn func(*messages.Pickle, error)) { s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn) } @@ -326,6 +356,10 @@ func (s *Suite) AfterFeature(fn func(*messages.GherkinDocument)) { // AfterSuite registers a function or method // to be run once after suite runner +// +// Deprecated: The current Suite initializer will be removed and replaced by +// two initializers, one for the Test Suite and one for the Scenarios. Use +// func (ctx *TestSuiteContext) AfterSuite instead. func (s *Suite) AfterSuite(fn func()) { s.afterSuiteHandlers = append(s.afterSuiteHandlers, fn) } @@ -552,6 +586,11 @@ func (s *Suite) runFeature(f *feature) { }() for _, pickle := range f.pickles { + if s.scenarioInitializer != nil { + sc := ScenarioContext{suite: s} + s.scenarioInitializer(&sc) + } + err := s.runPickle(pickle) if s.shouldFail(err) { s.failed = true diff --git a/test_context.go b/test_context.go new file mode 100644 index 00000000..dfe3e618 --- /dev/null +++ b/test_context.go @@ -0,0 +1,125 @@ +package godog + +import "github.com/cucumber/messages-go/v10" + +// Scenario represents the executed scenario +type Scenario = messages.Pickle + +// Step represents the executed step +type Step = messages.Pickle_PickleStep + +// DocString represents the DocString argument made to a step definition +type DocString = messages.PickleStepArgument_PickleDocString + +// Table represents the Table argument made to a step definition +type Table = messages.PickleStepArgument_PickleTable + +// TestSuiteContext allows various contexts +// to register event handlers. +// +// When running a test suite, the instance of TestSuiteContext +// is passed to all functions (contexts), which +// have it as a first and only argument. +// +// Note that all event hooks does not catch panic errors +// in order to have a trace information +type TestSuiteContext struct { + beforeSuiteHandlers []func() + afterSuiteHandlers []func() +} + +// BeforeSuite registers a function or method +// to be run once before suite runner. +// +// Use it to prepare the test suite for a spin. +// Connect and prepare database for instance... +func (ctx *TestSuiteContext) BeforeSuite(fn func()) { + ctx.beforeSuiteHandlers = append(ctx.beforeSuiteHandlers, fn) +} + +// AfterSuite registers a function or method +// to be run once after suite runner +func (ctx *TestSuiteContext) AfterSuite(fn func()) { + ctx.afterSuiteHandlers = append(ctx.afterSuiteHandlers, fn) +} + +// ScenarioContext allows various contexts +// to register steps and event handlers. +// +// When running a scenario, the instance of ScenarioContext +// is passed to all functions (contexts), which +// have it as a first and only argument. +// +// Note that all event hooks does not catch panic errors +// in order to have a trace information. Only step +// executions are catching panic error since it may +// be a context specific error. +type ScenarioContext struct { + suite *Suite +} + +// BeforeScenario registers a function or method +// to be run before every scenario. +// +// It is a good practice to restore the default state +// before every scenario so it would be isolated from +// any kind of state. +func (ctx *ScenarioContext) BeforeScenario(fn func(sc *Scenario)) { + ctx.suite.BeforeScenario(fn) +} + +// AfterScenario registers an function or method +// to be run after every scenario. +func (ctx *ScenarioContext) AfterScenario(fn func(sc *Scenario, err error)) { + ctx.suite.AfterScenario(fn) +} + +// BeforeStep registers a function or method +// to be run before every step. +func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) { + ctx.suite.BeforeStep(fn) +} + +// AfterStep registers an function or method +// to be run after every step. +// +// It may be convenient to return a different kind of error +// in order to print more state details which may help +// in case of step failure +// +// In some cases, for example when running a headless +// browser, to take a screenshot after failure. +func (ctx *ScenarioContext) AfterStep(fn func(st *Step, err error)) { + ctx.suite.AfterStep(fn) +} + +// Step allows to register a *StepDefinition in the +// Godog feature suite, the definition will be applied +// to all steps matching the given Regexp expr. +// +// It will panic if expr is not a valid regular +// expression or stepFunc is not a valid step +// handler. +// +// The expression can be of type: *regexp.Regexp, string or []byte +// +// The stepFunc may accept one or several arguments of type: +// - int, int8, int16, int32, int64 +// - float32, float64 +// - string +// - []byte +// - *godog.DocString +// - *godog.Table +// +// The stepFunc need to return either an error or []string for multistep +// +// Note that if there are two definitions which may match +// the same step, then only the first matched handler +// will be applied. +// +// If none of the *StepDefinition is matched, then +// ErrUndefined error will be returned when +// running steps. +func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { + ctx.suite.Step(expr, stepFunc) +} diff --git a/test_context_test.go b/test_context_test.go new file mode 100644 index 00000000..0d22af36 --- /dev/null +++ b/test_context_test.go @@ -0,0 +1,49 @@ +package godog + +import ( + "bytes" + "strings" + "testing" + + "github.com/cucumber/gherkin-go/v11" + "github.com/cucumber/godog/colors" + "github.com/cucumber/messages-go/v10" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_TestContext(t *testing.T) { + const path = "any.feature" + + var buf bytes.Buffer + w := colors.Uncolored(&buf) + + gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId) + require.NoError(t, err) + + pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId) + + r := runner{ + fmt: progressFunc("progress", w), + features: []*feature{{GherkinDocument: gd, pickles: pickles}}, + testSuiteInitializer: nil, + scenarioInitializer: func(sc *ScenarioContext) { + sc.Step(`^one$`, func() error { return nil }) + sc.Step(`^two$`, func() error { return nil }) + }, + } + + failed := r.concurrent(1, func() Formatter { return progressFunc("progress", w) }) + require.False(t, failed) + + expected := `.. 2 + + +1 scenarios (1 passed) +2 steps (2 passed) +0s +` + + actual := buf.String() + assert.Equal(t, expected, actual) +}