From 9baac0fdfa7b26ba4570a8e4b6adabf4c9fd2b7c Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Wed, 12 Jan 2022 23:40:41 +0100 Subject: [PATCH] Allow suite-level configuration of steps and hooks (#453) * Allow suite-level configuration of steps and hooks * Fix a few typos * Update CHANGELOG.md * Add test * Run scenario in same goroutine to preserve stack when concurrency is disabled * Remove redundant check --- CHANGELOG.md | 11 +++++++++++ run.go | 35 +++++++++++++++++++++++------------ run_test.go | 18 ++++++++++++++++++ test_context.go | 22 +++++++++++++++------- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66bd31d..282fddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt --- +## [Unreleased] + +### Added + +- Allow suite-level configuration of steps and hooks ([453](https://github.com/cucumber/godog/pull/453) - [vearutop]) + +## Changed + +- Run scenarios in the same goroutine if concurrency is disabled (([453](https://github.com/cucumber/godog/pull/453) - [vearutop])) + + ## [v0.12.3] ### Added diff --git a/run.go b/run.go index 7d2999f..5fa6e15 100644 --- a/run.go +++ b/run.go @@ -57,7 +57,16 @@ func (r *runner) concurrent(rate int) (failed bool) { fmt.SetStorage(r.storage) } - testSuiteContext := TestSuiteContext{} + testSuiteContext := TestSuiteContext{ + suite: &suite{ + fmt: r.fmt, + randomSeed: r.randomSeed, + strict: r.strict, + storage: r.storage, + defaultContext: r.defaultContext, + testingT: r.testingT, + }, + } if r.testSuiteInitializer != nil { r.testSuiteInitializer(&testSuiteContext) } @@ -93,7 +102,7 @@ func (r *runner) concurrent(rate int) (failed bool) { r.fmt.Feature(ft.GherkinDocument, ft.Uri, ft.Content) } - go func(fail *bool, pickle *messages.Pickle) { + runPickle := func(fail *bool, pickle *messages.Pickle) { defer func() { <-queue // free a space in queue }() @@ -102,17 +111,11 @@ func (r *runner) concurrent(rate int) (failed bool) { return } - suite := &suite{ - fmt: r.fmt, - randomSeed: r.randomSeed, - strict: r.strict, - storage: r.storage, - defaultContext: r.defaultContext, - testingT: r.testingT, - } + // Copy base suite. + suite := *testSuiteContext.suite if r.scenarioInitializer != nil { - sc := ScenarioContext{suite: suite} + sc := ScenarioContext{suite: &suite} r.scenarioInitializer(&sc) } @@ -122,7 +125,15 @@ func (r *runner) concurrent(rate int) (failed bool) { *fail = true copyLock.Unlock() } - }(&failed, &pickle) + } + + if rate == 1 { + // Running within the same goroutine for concurrency 1 + // to preserve original stacks and simplify debugging. + runPickle(&failed, &pickle) + } else { + go runPickle(&failed, &pickle) + } } } diff --git a/run_test.go b/run_test.go index 9d3a8c1..cdbcf76 100644 --- a/run_test.go +++ b/run_test.go @@ -2,6 +2,7 @@ package godog import ( "bytes" + "context" "fmt" "io" "io/ioutil" @@ -77,9 +78,22 @@ func Test_FailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) { ft := models.Feature{GherkinDocument: gd} ft.Pickles = gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId) + var beforeScenarioFired, afterScenarioFired int + r := runner{ 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) { + beforeScenarioFired++ + return ctx, nil + }) + + ctx.ScenarioContext().After(func(ctx context.Context, sc *Scenario, err error) (context.Context, error) { + afterScenarioFired++ + return ctx, nil + }) + }, scenarioInitializer: func(ctx *ScenarioContext) { ctx.Step(`^one$`, func() error { return nil }) ctx.Step(`^two$`, func() error { return ErrPending }) @@ -94,10 +108,14 @@ func Test_FailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) { failed := r.concurrent(1) require.False(t, failed) + assert.Equal(t, 1, beforeScenarioFired) + assert.Equal(t, 1, afterScenarioFired) r.strict = true failed = r.concurrent(1) require.True(t, failed) + assert.Equal(t, 2, beforeScenarioFired) + assert.Equal(t, 2, afterScenarioFired) } func Test_FailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) { diff --git a/test_context.go b/test_context.go index e88225e..862280e 100644 --- a/test_context.go +++ b/test_context.go @@ -6,11 +6,10 @@ import ( "reflect" "regexp" - "github.com/cucumber/messages-go/v16" - "github.com/cucumber/godog/formatters" "github.com/cucumber/godog/internal/builder" "github.com/cucumber/godog/internal/models" + "github.com/cucumber/messages-go/v16" ) // GherkinDocument represents gherkin document. @@ -66,6 +65,8 @@ type Table = messages.PickleTable type TestSuiteContext struct { beforeSuiteHandlers []func() afterSuiteHandlers []func() + + suite *suite } // BeforeSuite registers a function or method @@ -83,6 +84,13 @@ func (ctx *TestSuiteContext) AfterSuite(fn func()) { ctx.afterSuiteHandlers = append(ctx.afterSuiteHandlers, fn) } +// ScenarioContext allows registering scenario hooks. +func (ctx *TestSuiteContext) ScenarioContext() *ScenarioContext { + return &ScenarioContext{ + suite: ctx.suite, + } +} + // ScenarioContext allows various contexts // to register steps and event handlers. // @@ -103,11 +111,11 @@ type StepContext struct { suite *suite } -// Before registers a a function or method +// Before 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 +// before every scenario, so it would be isolated from // any kind of state. func (ctx ScenarioContext) Before(h BeforeScenarioHook) { ctx.suite.beforeScenarioHandlers = append(ctx.suite.beforeScenarioHandlers, h) @@ -139,7 +147,7 @@ func (ctx StepContext) Before(h BeforeStepHook) { // BeforeStepHook defines a hook before step. type BeforeStepHook func(ctx context.Context, st *Step) (context.Context, error) -// After registers an function or method +// After registers a function or method // to be run after every step. // // It may be convenient to return a different kind of error @@ -171,7 +179,7 @@ func (ctx *ScenarioContext) BeforeScenario(fn func(sc *Scenario)) { }) } -// AfterScenario registers an function or method +// AfterScenario registers a function or method // to be run after every scenario. // // Deprecated: use After. @@ -195,7 +203,7 @@ func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) { }) } -// AfterStep registers an function or method +// AfterStep registers a function or method // to be run after every step. // // It may be convenient to return a different kind of error