Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -795,3 +795,92 @@ func Test_TestSuite_RetreiveFeatures(t *testing.T) {
})
}
}

// TestHookPanic verifies that panics in ScenarioContext.After, ScenarioContext.Before,
// StepContext.Before, and StepContext.After hooks are handled gracefully by the godog.
// If this test panics and fails to report gracefully, it means the panic handling in the hooks is insufficient.
func TestHookPanic(t *testing.T) {
// create a temporary file
f, err := os.CreateTemp("", "godog_test_hook_panic_*.feature")
if err != nil {
t.Fatalf("failed to create temporary file: %v", err)
}
defer func(name string) {
if err := os.Remove(f.Name()); err != nil {
t.Fatalf("failed to remove temporary file: %v", err)
}
}(f.Name())

_, err = f.Write([]byte(`
Feature: count

Scenario: count one two
When one
Then two

Scenario: count three four
When three
Then four
`))
if err != nil {
t.Fatalf("failed to write to temporary file: %v", err)
}
err = f.Close()
if err != nil {
t.Fatalf("failed to close temporary file: %v", err)
}
setupSteps := func(sc *ScenarioContext) {
sc.Step(`^one$`, func() {})
sc.Step(`^two$`, func() {})
sc.Step(`^three$`, func() {})
sc.Step(`^four$`, func() {})
}
for name, test := range map[string]struct {
initScenarios func(*ScenarioContext)
}{
"before scenario": {
func(ctx *ScenarioContext) {
ctx.Before(func(ctx context.Context, sc *Scenario) (context.Context, error) {
panic("before scenario")
})
setupSteps(ctx)
},
},
"after scenario": {
func(ctx *ScenarioContext) {
setupSteps(ctx)
ctx.After(func(ctx context.Context, sc *Scenario, err error) (context.Context, error) {
panic("after scenario")
})
},
},
"after step": {
func(ctx *ScenarioContext) {
setupSteps(ctx)
ctx.StepContext().After(func(_ context.Context, _ *Step, _ StepResultStatus, _ error) (context.Context, error) {
panic("after step")
})
},
},
"before step": {
func(ctx *ScenarioContext) {
setupSteps(ctx)
ctx.StepContext().Before(func(_ context.Context, _ *Step) (context.Context, error) {
panic("before step")
})
},
},
} {
t.Run(name, func(t *testing.T) {
opts := Options{
Format: "pretty",
Paths: []string{f.Name()},
}
TestSuite{
Name: "HookPanic!",
Options: &opts,
ScenarioInitializer: test.initScenarios,
}.Run()
})
}
}
48 changes: 36 additions & 12 deletions suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,25 +320,38 @@ func (s *suite) runBeforeStepHooks(ctx context.Context, step *Step, err error) (
return ctx, err
}

func (s *suite) runAfterStepHooks(ctx context.Context, step *Step, status StepResultStatus, err error) (context.Context, error) {
func (s *suite) runAfterStepHooks(ctx context.Context, step *Step, status StepResultStatus, err error) (rctx context.Context, rerr error) {
rerr = err
rctx = ctx
defer func() {
if r := recover(); r != nil {
pe := fmt.Errorf("panic: %v", r)
if rerr == nil {
rerr = pe
} else {
rerr = fmt.Errorf("%v; %w", pe, rerr)
}
}
}()

for _, f := range s.afterStepHandlers {
hctx, herr := f(ctx, step, status, err)
hctx, herr := f(rctx, step, status, rerr)

// Adding hook error to resulting error without breaking hooks loop.
if herr != nil {
if err == nil {
err = herr
if rerr == nil {
rerr = herr
} else {
err = fmt.Errorf("%v, %w", herr, err)
rerr = fmt.Errorf("%v, %w", herr, rerr)
}
}

if hctx != nil {
ctx = hctx
rctx = hctx
}
}

return ctx, err
return rctx, rerr
}

func (s *suite) runBeforeScenarioHooks(ctx context.Context, pickle *messages.Pickle) (context.Context, error) {
Expand Down Expand Up @@ -367,15 +380,26 @@ func (s *suite) runBeforeScenarioHooks(ctx context.Context, pickle *messages.Pic
return ctx, err
}

func (s *suite) runAfterScenarioHooks(ctx context.Context, pickle *messages.Pickle, lastStepErr error) (context.Context, error) {
err := lastStepErr
func (s *suite) runAfterScenarioHooks(ctx context.Context, pickle *messages.Pickle, lastStepErr error) (rctx context.Context, err error) {
rctx = ctx
err = lastStepErr
defer func() {
if r := recover(); r != nil {
pe := fmt.Errorf("panic: %v", r)
if err == nil {
err = pe
} else {
err = fmt.Errorf("%v; %w", pe, err)
}
}
}()

hooksFailed := false
isStepErr := true

// run after scenario handlers
for _, f := range s.afterScenarioHandlers {
hctx, herr := f(ctx, pickle, err)
hctx, herr := f(rctx, pickle, err)

// Adding hook error to resulting error without breaking hooks loop.
if herr != nil {
Expand All @@ -394,15 +418,15 @@ func (s *suite) runAfterScenarioHooks(ctx context.Context, pickle *messages.Pick
}

if hctx != nil {
ctx = hctx
rctx = hctx
}
}

if hooksFailed {
err = fmt.Errorf("after scenario hook failed: %w", err)
}

return ctx, err
return rctx, err
}

func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}, stepType messages.PickleStepType) (context.Context, []string, error) {
Expand Down
Loading