Skip to content

Commit 3e0f902

Browse files
authored
Improve hooks invocation flow (#568)
1 parent 51d164f commit 3e0f902

File tree

3 files changed

+346
-30
lines changed

3 files changed

+346
-30
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
1010
### Added
1111
- Support for reading feature files from an `fs.FS` ([550](https://github.com/cucumber/godog/pull/550) - [tigh-latte](https://github.com/tigh-latte))
1212
- Added keyword functions. ([509](https://github.com/cucumber/godog/pull/509) - [otrava7](https://github.com/otrava7))
13-
- prefer go test to use of godog cli in README ([548](https://github.com/cucumber/godog/pull/548) - [danielhelfand](https://github.com/danielhelfand)
13+
- Prefer go test to use of godog cli in README ([548](https://github.com/cucumber/godog/pull/548) - [danielhelfand](https://github.com/danielhelfand))
14+
15+
### Fixed
16+
- Improve hooks invocation flow ([568](https://github.com/cucumber/godog/pull/568) - [vearutop](https://github.com/vearutop))
1417

1518
## [v0.12.6]
1619
### Changed

suite.go

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package godog
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"reflect"
78
"strings"
@@ -27,6 +28,9 @@ var ErrUndefined = fmt.Errorf("step is undefined")
2728
// step implementation is pending
2829
var ErrPending = fmt.Errorf("step implementation is pending")
2930

31+
// ErrSkip should be returned by step definition or a hook if scenario and further steps are to be skipped.
32+
var ErrSkip = fmt.Errorf("skipped")
33+
3034
// StepResultStatus describes step result.
3135
type StepResultStatus = models.StepResultStatus
3236

@@ -72,34 +76,53 @@ func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition {
7276
return def
7377
}
7478

75-
func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevStepErr error, isFirst, isLast bool) (rctx context.Context, err error) {
79+
func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scenarioErr error, isFirst, isLast bool) (rctx context.Context, err error) {
7680
var (
7781
match *models.StepDefinition
78-
sr = models.PickleStepResult{Status: models.Undefined}
82+
sr = models.NewStepResult(pickle.Id, step.Id, match)
7983
)
8084

8185
rctx = ctx
86+
sr.Status = StepUndefined
8287

8388
// user multistep definitions may panic
8489
defer func() {
8590
if e := recover(); e != nil {
86-
err = &traceError{
87-
msg: fmt.Sprintf("%v", e),
88-
stack: callStack(),
91+
if err != nil {
92+
err = &traceError{
93+
msg: fmt.Sprintf("%s: %v", err.Error(), e),
94+
stack: callStack(),
95+
}
96+
} else {
97+
err = &traceError{
98+
msg: fmt.Sprintf("%v", e),
99+
stack: callStack(),
100+
}
89101
}
90102
}
91103

92-
earlyReturn := prevStepErr != nil || err == ErrUndefined
93-
94-
if !earlyReturn {
95-
sr = models.NewStepResult(pickle.Id, step.Id, match)
104+
earlyReturn := scenarioErr != nil || err == ErrUndefined
105+
106+
switch {
107+
case errors.Is(err, ErrPending):
108+
sr.Status = StepPending
109+
case errors.Is(err, ErrSkip) || (err == nil && scenarioErr != nil):
110+
sr.Status = StepSkipped
111+
case errors.Is(err, ErrUndefined):
112+
sr.Status = StepUndefined
113+
case err != nil:
114+
sr.Status = StepFailed
115+
case err == nil && scenarioErr == nil:
116+
sr.Status = StepPassed
96117
}
97118

98119
// Run after step handlers.
99120
rctx, err = s.runAfterStepHooks(ctx, step, sr.Status, err)
100121

122+
shouldFail := s.shouldFail(err)
123+
101124
// Trigger after scenario on failing or last step to attach possible hook error to step.
102-
if isLast || (sr.Status != StepSkipped && sr.Status != StepUndefined && err != nil) {
125+
if !s.shouldFail(scenarioErr) && (isLast || shouldFail) {
103126
rctx, err = s.runAfterScenarioHooks(rctx, pickle, err)
104127
}
105128

@@ -137,6 +160,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
137160

138161
match = s.matchStep(step)
139162
s.storage.MustInsertStepDefintionMatch(step.AstNodeIds[0], match)
163+
sr.Def = match
140164
s.fmt.Defined(pickle, step, match.GetInternalStepDefinition())
141165

142166
if err != nil {
@@ -162,6 +186,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
162186
Nested: match.Nested,
163187
Undefined: undef,
164188
}
189+
sr.Def = match
165190
}
166191

167192
sr = models.NewStepResult(pickle.Id, step.Id, match)
@@ -172,7 +197,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
172197
return ctx, ErrUndefined
173198
}
174199

175-
if prevStepErr != nil {
200+
if scenarioErr != nil {
176201
sr = models.NewStepResult(pickle.Id, step.Id, match)
177202
sr.Status = models.Skipped
178203
s.storage.MustInsertPickleStepResult(sr)
@@ -344,18 +369,53 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.
344369

345370
steps, ok := result.(Steps)
346371
if !ok {
347-
return ctx, fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result)
372+
return ctx, fmt.Errorf("unexpected error, should have been godog.Steps: %T - %+v", result, result)
348373
}
349374

350375
var err error
351376

352377
for _, text := range steps {
353378
if def := s.matchStepTextAndType(text, messages.PickleStepType_UNKNOWN); def == nil {
354379
return ctx, ErrUndefined
355-
} else if ctx, err = s.maybeSubSteps(def.Run(ctx)); err != nil {
356-
return ctx, fmt.Errorf("%s: %+v", text, err)
380+
} else {
381+
ctx, err = s.runSubStep(ctx, text, def)
382+
if err != nil {
383+
return ctx, err
384+
}
385+
}
386+
}
387+
return ctx, nil
388+
}
389+
390+
func (s *suite) runSubStep(ctx context.Context, text string, def *models.StepDefinition) (_ context.Context, err error) {
391+
st := &Step{}
392+
st.Text = text
393+
st.Type = messages.PickleStepType_ACTION
394+
395+
defer func() {
396+
status := StepPassed
397+
398+
switch {
399+
case errors.Is(err, ErrUndefined):
400+
status = StepUndefined
401+
case errors.Is(err, ErrPending):
402+
status = StepPending
403+
case err != nil:
404+
status = StepFailed
357405
}
406+
407+
ctx, err = s.runAfterStepHooks(ctx, st, status, err)
408+
}()
409+
410+
ctx, err = s.runBeforeStepHooks(ctx, st, nil)
411+
if err != nil {
412+
return ctx, fmt.Errorf("%s: %+v", text, err)
413+
}
414+
415+
if ctx, err = s.maybeSubSteps(def.Run(ctx)); err != nil {
416+
return ctx, fmt.Errorf("%s: %+v", text, err)
358417
}
418+
359419
return ctx, nil
360420
}
361421

@@ -405,32 +465,23 @@ func keywordMatches(k formatters.Keyword, stepType messages.PickleStepType) bool
405465

406466
func (s *suite) runSteps(ctx context.Context, pickle *Scenario, steps []*Step) (context.Context, error) {
407467
var (
408-
stepErr, err error
468+
stepErr, scenarioErr error
409469
)
410470

411471
for i, step := range steps {
412472
isLast := i == len(steps)-1
413473
isFirst := i == 0
414-
ctx, stepErr = s.runStep(ctx, pickle, step, err, isFirst, isLast)
415-
switch stepErr {
416-
case ErrUndefined:
417-
// do not overwrite failed error
418-
if err == ErrUndefined || err == nil {
419-
err = stepErr
420-
}
421-
case ErrPending:
422-
err = stepErr
423-
case nil:
424-
default:
425-
err = stepErr
474+
ctx, stepErr = s.runStep(ctx, pickle, step, scenarioErr, isFirst, isLast)
475+
if scenarioErr == nil || s.shouldFail(stepErr) {
476+
scenarioErr = stepErr
426477
}
427478
}
428479

429-
return ctx, err
480+
return ctx, scenarioErr
430481
}
431482

432483
func (s *suite) shouldFail(err error) bool {
433-
if err == nil {
484+
if err == nil || err == ErrSkip {
434485
return false
435486
}
436487

0 commit comments

Comments
 (0)