Skip to content

Commit 8cf3f41

Browse files
authored
Add scenario hook errors to first and last steps (cucumber#417)
1 parent f1ca5dc commit 8cf3f41

File tree

4 files changed

+226
-50
lines changed

4 files changed

+226
-50
lines changed

features/events.feature

+61
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,64 @@ Feature: suite events
9393
| AfterSuite | 1 |
9494

9595
And the suite should have failed
96+
97+
98+
Scenario: should add scenario hook errors to steps
99+
Given a feature "normal.feature" file:
100+
"""
101+
Feature: scenario hook errors
102+
103+
Scenario: failing before and after scenario
104+
Then adding step state to context
105+
And passing step
106+
107+
Scenario: failing before scenario
108+
Then adding step state to context
109+
And passing step
110+
111+
Scenario: failing after scenario
112+
Then adding step state to context
113+
And passing step
114+
115+
"""
116+
When I run feature suite with formatter "pretty"
117+
118+
Then the suite should have failed
119+
And the rendered output will be as follows:
120+
"""
121+
Feature: scenario hook errors
122+
123+
Scenario: failing before and after scenario # normal.feature:3
124+
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12
125+
after scenario hook failed: failed in after scenario hook, step error: before scenario hook failed: failed in before scenario hook
126+
And passing step # suite_context_test.go:0 -> InitializeScenario.func2
127+
128+
Scenario: failing before scenario # normal.feature:7
129+
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12
130+
before scenario hook failed: failed in before scenario hook
131+
And passing step # suite_context_test.go:0 -> InitializeScenario.func2
132+
133+
Scenario: failing after scenario # normal.feature:11
134+
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12
135+
And passing step # suite_context_test.go:0 -> InitializeScenario.func2
136+
after scenario hook failed: failed in after scenario hook
137+
138+
--- Failed steps:
139+
140+
Scenario: failing before and after scenario # normal.feature:3
141+
Then adding step state to context # normal.feature:4
142+
Error: after scenario hook failed: failed in after scenario hook, step error: before scenario hook failed: failed in before scenario hook
143+
144+
Scenario: failing before scenario # normal.feature:7
145+
Then adding step state to context # normal.feature:8
146+
Error: before scenario hook failed: failed in before scenario hook
147+
148+
Scenario: failing after scenario # normal.feature:11
149+
And passing step # normal.feature:13
150+
Error: after scenario hook failed: failed in after scenario hook
151+
152+
153+
3 scenarios (3 failed)
154+
6 steps (1 passed, 3 failed, 2 skipped)
155+
0s
156+
"""

run_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,11 @@ func Test_AllFeaturesRun(t *testing.T) {
414414
...................................................................... 140
415415
...................................................................... 210
416416
...................................................................... 280
417-
................................... 315
417+
........................................ 320
418418
419419
420-
82 scenarios (82 passed)
421-
315 steps (315 passed)
420+
83 scenarios (83 passed)
421+
320 steps (320 passed)
422422
0s
423423
`
424424

suite.go

+143-45
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@ func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition {
7070
return def
7171
}
7272

73-
func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevStepErr error) (rctx context.Context, err error) {
73+
func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevStepErr error, isFirst, isLast bool) (rctx context.Context, err error) {
7474
var (
7575
match *models.StepDefinition
7676
sr = models.PickleStepResult{Status: models.Undefined}
7777
)
7878

79+
rctx = ctx
80+
7981
// user multistep definitions may panic
8082
defer func() {
8183
if e := recover(); e != nil {
@@ -87,20 +89,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
8789

8890
defer func() {
8991
// run after step handlers
90-
for _, f := range s.afterStepHandlers {
91-
hctx, herr := f(rctx, step, sr.Status, err)
92-
93-
// Adding hook error to resulting error without breaking hooks loop.
94-
if herr != nil {
95-
if err == nil {
96-
err = herr
97-
} else {
98-
err = fmt.Errorf("%v: %w", herr, err)
99-
}
100-
}
101-
102-
rctx = hctx
103-
}
92+
rctx, err = s.runAfterStepHooks(ctx, step, sr.Status, err)
10493
}()
10594

10695
if prevStepErr != nil {
@@ -113,6 +102,11 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
113102

114103
sr = models.NewStepResult(pickle.Id, step.Id, match)
115104

105+
// Trigger after scenario on failing or last step to attach possible hook error to step.
106+
if (err == nil && isLast) || err != nil {
107+
rctx, err = s.runAfterScenarioHooks(rctx, pickle, err)
108+
}
109+
116110
switch err {
117111
case nil:
118112
sr.Status = models.Passed
@@ -133,18 +127,26 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
133127
}
134128
}()
135129

136-
// run before step handlers
137-
for _, f := range s.beforeStepHandlers {
138-
ctx, err = f(ctx, step)
139-
if err != nil {
140-
return ctx, err
141-
}
130+
// run before scenario handlers
131+
if isFirst {
132+
ctx, err = s.runBeforeScenarioHooks(ctx, pickle)
142133
}
143134

144135
match = s.matchStep(step)
145136
s.storage.MustInsertStepDefintionMatch(step.AstNodeIds[0], match)
146137
s.fmt.Defined(pickle, step, match.GetInternalStepDefinition())
147138

139+
// run before step handlers
140+
ctx, err = s.runBeforeStepHooks(ctx, step, err)
141+
142+
if err != nil {
143+
sr = models.NewStepResult(pickle.Id, step.Id, match)
144+
sr.Status = models.Failed
145+
s.storage.MustInsertPickleStepResult(sr)
146+
147+
return ctx, err
148+
}
149+
148150
if ctx, undef, err := s.maybeUndefined(ctx, step.Text, step.Argument); err != nil {
149151
return ctx, err
150152
} else if len(undef) > 0 {
@@ -183,6 +185,118 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
183185
return ctx, err
184186
}
185187

188+
func (s *suite) runBeforeStepHooks(ctx context.Context, step *Step, err error) (context.Context, error) {
189+
hooksFailed := false
190+
191+
for _, f := range s.beforeStepHandlers {
192+
hctx, herr := f(ctx, step)
193+
if herr != nil {
194+
hooksFailed = true
195+
196+
if err == nil {
197+
err = herr
198+
} else {
199+
err = fmt.Errorf("%v, %w", herr, err)
200+
}
201+
}
202+
203+
if hctx != nil {
204+
ctx = hctx
205+
}
206+
}
207+
208+
if hooksFailed {
209+
err = fmt.Errorf("before step hook failed: %w", err)
210+
}
211+
212+
return ctx, err
213+
}
214+
215+
func (s *suite) runAfterStepHooks(ctx context.Context, step *Step, status StepResultStatus, err error) (context.Context, error) {
216+
for _, f := range s.afterStepHandlers {
217+
hctx, herr := f(ctx, step, status, err)
218+
219+
// Adding hook error to resulting error without breaking hooks loop.
220+
if herr != nil {
221+
if err == nil {
222+
err = herr
223+
} else {
224+
err = fmt.Errorf("%v, %w", herr, err)
225+
}
226+
}
227+
228+
if hctx != nil {
229+
ctx = hctx
230+
}
231+
}
232+
233+
return ctx, err
234+
}
235+
236+
func (s *suite) runBeforeScenarioHooks(ctx context.Context, pickle *messages.Pickle) (context.Context, error) {
237+
var err error
238+
239+
// run before scenario handlers
240+
for _, f := range s.beforeScenarioHandlers {
241+
hctx, herr := f(ctx, pickle)
242+
if herr != nil {
243+
if err == nil {
244+
err = herr
245+
} else {
246+
err = fmt.Errorf("%v, %w", herr, err)
247+
}
248+
}
249+
250+
if hctx != nil {
251+
ctx = hctx
252+
}
253+
}
254+
255+
if err != nil {
256+
err = fmt.Errorf("before scenario hook failed: %w", err)
257+
}
258+
259+
return ctx, err
260+
}
261+
262+
func (s *suite) runAfterScenarioHooks(ctx context.Context, pickle *messages.Pickle, lastStepErr error) (context.Context, error) {
263+
err := lastStepErr
264+
265+
hooksFailed := false
266+
isStepErr := true
267+
268+
// run after scenario handlers
269+
for _, f := range s.afterScenarioHandlers {
270+
hctx, herr := f(ctx, pickle, err)
271+
272+
// Adding hook error to resulting error without breaking hooks loop.
273+
if herr != nil {
274+
hooksFailed = true
275+
276+
if err == nil {
277+
isStepErr = false
278+
err = herr
279+
} else {
280+
if isStepErr {
281+
err = fmt.Errorf("step error: %w", err)
282+
isStepErr = false
283+
}
284+
err = fmt.Errorf("%v, %w", herr, err)
285+
}
286+
}
287+
288+
if hctx != nil {
289+
ctx = hctx
290+
}
291+
}
292+
293+
if hooksFailed {
294+
err = fmt.Errorf("after scenario hook failed: %w", err)
295+
}
296+
297+
return ctx, err
298+
}
299+
186300
func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}) (context.Context, []string, error) {
187301
step := s.matchStepText(text)
188302
if nil == step {
@@ -271,8 +385,10 @@ func (s *suite) runSteps(ctx context.Context, pickle *Scenario, steps []*Step) (
271385
stepErr, err error
272386
)
273387

274-
for _, step := range steps {
275-
ctx, stepErr = s.runStep(ctx, pickle, step, err)
388+
for i, step := range steps {
389+
isLast := i == len(steps)-1
390+
isFirst := i == 0
391+
ctx, stepErr = s.runStep(ctx, pickle, step, err, isFirst, isLast)
276392
switch stepErr {
277393
case ErrUndefined:
278394
// do not overwrite failed error
@@ -326,13 +442,8 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) {
326442
return ErrUndefined
327443
}
328444

329-
// run before scenario handlers
330-
for _, f := range s.beforeScenarioHandlers {
331-
ctx, err = f(ctx, pickle)
332-
if err != nil {
333-
return err
334-
}
335-
}
445+
// Before scenario hooks are aclled in context of first evaluated step
446+
// so that error from handler can be added to step.
336447

337448
pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()}
338449
s.storage.MustInsertPickleResult(pr)
@@ -342,21 +453,8 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) {
342453
// scenario
343454
ctx, err = s.runSteps(ctx, pickle, pickle.Steps)
344455

345-
// run after scenario handlers
346-
for _, f := range s.afterScenarioHandlers {
347-
hctx, herr := f(ctx, pickle, err)
348-
349-
// Adding hook error to resulting error without breaking hooks loop.
350-
if herr != nil {
351-
if err == nil {
352-
err = herr
353-
} else {
354-
err = fmt.Errorf("%v: %w", herr, err)
355-
}
356-
}
357-
358-
ctx = hctx
359-
}
456+
// After scenario handlers are called in context of last evaluated step
457+
// so that error from handler can be added to step.
360458

361459
return err
362460
}

suite_context_test.go

+19-2
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,22 @@ func (tc *godogFeaturesScenario) iAmListeningToSuiteEvents() error {
426426
return context.WithValue(ctx, ctxKey("BeforeScenario"), true), nil
427427
})
428428

429+
scenarioContext.Before(func(ctx context.Context, sc *Scenario) (context.Context, error) {
430+
if sc.Name == "failing before and after scenario" || sc.Name == "failing before scenario" {
431+
return context.WithValue(ctx, ctxKey("AfterStep"), true), errors.New("failed in before scenario hook")
432+
}
433+
434+
return ctx, nil
435+
})
436+
437+
scenarioContext.After(func(ctx context.Context, sc *Scenario, err error) (context.Context, error) {
438+
if sc.Name == "failing before and after scenario" || sc.Name == "failing after scenario" {
439+
return ctx, errors.New("failed in after scenario hook")
440+
}
441+
442+
return ctx, nil
443+
})
444+
429445
scenarioContext.After(func(ctx context.Context, pickle *Scenario, err error) (context.Context, error) {
430446
tc.events = append(tc.events, &firedEvent{"AfterScenario", []interface{}{pickle, err}})
431447

@@ -678,14 +694,15 @@ func (tc *godogFeaturesScenario) theRenderOutputWillBe(docstring *DocString) err
678694
expected = suiteCtxPtrReg.ReplaceAllString(expected, "*godogFeaturesScenario")
679695

680696
actual := tc.out.String()
681-
actual = trimAllLines(actual)
682697
actual = actualSuiteCtxReg.ReplaceAllString(actual, "suite_context_test.go:0")
683698
actual = actualSuiteCtxFuncReg.ReplaceAllString(actual, "InitializeScenario.func$1")
699+
actualTrimmed := actual
700+
actual = trimAllLines(actual)
684701

685702
expectedRows := strings.Split(expected, "\n")
686703
actualRows := strings.Split(actual, "\n")
687704

688-
return assertExpectedAndActual(assert.ElementsMatch, expectedRows, actualRows)
705+
return assertExpectedAndActual(assert.ElementsMatch, expectedRows, actualRows, actualTrimmed)
689706
}
690707

691708
func (tc *godogFeaturesScenario) theRenderXMLWillBe(docstring *DocString) error {

0 commit comments

Comments
 (0)