
* 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
318 строки
9,4 КиБ
Go
318 строки
9,4 КиБ
Go
package godog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
|
|
"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.
|
|
type GherkinDocument = messages.GherkinDocument
|
|
|
|
// Scenario represents the executed scenario
|
|
type Scenario = messages.Pickle
|
|
|
|
// Step represents the executed step
|
|
type Step = messages.PickleStep
|
|
|
|
// Steps allows to nest steps
|
|
// instead of returning an error in step func
|
|
// it is possible to return combined steps:
|
|
//
|
|
// func multistep(name string) godog.Steps {
|
|
// return godog.Steps{
|
|
// fmt.Sprintf(`an user named "%s"`, name),
|
|
// fmt.Sprintf(`user "%s" is authenticated`, name),
|
|
// }
|
|
// }
|
|
//
|
|
// These steps will be matched and executed in
|
|
// sequential order. The first one which fails
|
|
// will result in main step failure.
|
|
type Steps []string
|
|
|
|
// StepDefinition is a registered step definition
|
|
// contains a StepHandler and regexp which
|
|
// is used to match a step. Args which
|
|
// were matched by last executed step
|
|
//
|
|
// This structure is passed to the formatter
|
|
// when step is matched and is either failed
|
|
// or successful
|
|
type StepDefinition = formatters.StepDefinition
|
|
|
|
// DocString represents the DocString argument made to a step definition
|
|
type DocString = messages.PickleDocString
|
|
|
|
// Table represents the Table argument made to a step definition
|
|
type Table = messages.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()
|
|
|
|
suite *suite
|
|
}
|
|
|
|
// 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 registering scenario hooks.
|
|
func (ctx *TestSuiteContext) ScenarioContext() *ScenarioContext {
|
|
return &ScenarioContext{
|
|
suite: ctx.suite,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// StepContext allows registering step hooks.
|
|
type StepContext struct {
|
|
suite *suite
|
|
}
|
|
|
|
// 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
|
|
// any kind of state.
|
|
func (ctx ScenarioContext) Before(h BeforeScenarioHook) {
|
|
ctx.suite.beforeScenarioHandlers = append(ctx.suite.beforeScenarioHandlers, h)
|
|
}
|
|
|
|
// BeforeScenarioHook defines a hook before scenario.
|
|
type BeforeScenarioHook func(ctx context.Context, sc *Scenario) (context.Context, error)
|
|
|
|
// After registers an function or method
|
|
// to be run after every scenario.
|
|
func (ctx ScenarioContext) After(h AfterScenarioHook) {
|
|
ctx.suite.afterScenarioHandlers = append(ctx.suite.afterScenarioHandlers, h)
|
|
}
|
|
|
|
// AfterScenarioHook defines a hook after scenario.
|
|
type AfterScenarioHook func(ctx context.Context, sc *Scenario, err error) (context.Context, error)
|
|
|
|
// StepContext exposes StepContext of a scenario.
|
|
func (ctx *ScenarioContext) StepContext() StepContext {
|
|
return StepContext{suite: ctx.suite}
|
|
}
|
|
|
|
// Before registers a function or method
|
|
// to be run before every step.
|
|
func (ctx StepContext) Before(h BeforeStepHook) {
|
|
ctx.suite.beforeStepHandlers = append(ctx.suite.beforeStepHandlers, h)
|
|
}
|
|
|
|
// BeforeStepHook defines a hook before step.
|
|
type BeforeStepHook func(ctx context.Context, st *Step) (context.Context, error)
|
|
|
|
// After registers a 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 StepContext) After(h AfterStepHook) {
|
|
ctx.suite.afterStepHandlers = append(ctx.suite.afterStepHandlers, h)
|
|
}
|
|
|
|
// AfterStepHook defines a hook after step.
|
|
type AfterStepHook func(ctx context.Context, st *Step, status StepResultStatus, err error) (context.Context, error)
|
|
|
|
// 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.
|
|
//
|
|
// Deprecated: use Before.
|
|
func (ctx *ScenarioContext) BeforeScenario(fn func(sc *Scenario)) {
|
|
ctx.Before(func(ctx context.Context, sc *Scenario) (context.Context, error) {
|
|
fn(sc)
|
|
|
|
return ctx, nil
|
|
})
|
|
}
|
|
|
|
// AfterScenario registers a function or method
|
|
// to be run after every scenario.
|
|
//
|
|
// Deprecated: use After.
|
|
func (ctx *ScenarioContext) AfterScenario(fn func(sc *Scenario, err error)) {
|
|
ctx.After(func(ctx context.Context, sc *Scenario, err error) (context.Context, error) {
|
|
fn(sc, err)
|
|
|
|
return ctx, nil
|
|
})
|
|
}
|
|
|
|
// BeforeStep registers a function or method
|
|
// to be run before every step.
|
|
//
|
|
// Deprecated: use ScenarioContext.StepContext() and StepContext.Before.
|
|
func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) {
|
|
ctx.StepContext().Before(func(ctx context.Context, st *Step) (context.Context, error) {
|
|
fn(st)
|
|
|
|
return ctx, nil
|
|
})
|
|
}
|
|
|
|
// AfterStep registers a 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.
|
|
//
|
|
// Deprecated: use ScenarioContext.StepContext() and StepContext.After.
|
|
func (ctx *ScenarioContext) AfterStep(fn func(st *Step, err error)) {
|
|
ctx.StepContext().After(func(ctx context.Context, st *Step, status StepResultStatus, err error) (context.Context, error) {
|
|
fn(st, err)
|
|
|
|
return ctx, nil
|
|
})
|
|
}
|
|
|
|
// 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{}) {
|
|
var regex *regexp.Regexp
|
|
|
|
switch t := expr.(type) {
|
|
case *regexp.Regexp:
|
|
regex = t
|
|
case string:
|
|
regex = regexp.MustCompile(t)
|
|
case []byte:
|
|
regex = regexp.MustCompile(string(t))
|
|
default:
|
|
panic(fmt.Sprintf("expecting expr to be a *regexp.Regexp or a string, got type: %T", expr))
|
|
}
|
|
|
|
v := reflect.ValueOf(stepFunc)
|
|
typ := v.Type()
|
|
if typ.Kind() != reflect.Func {
|
|
panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc))
|
|
}
|
|
|
|
if typ.NumOut() > 2 {
|
|
panic(fmt.Sprintf("expected handler to return either zero, one or two values, but it has: %d", typ.NumOut()))
|
|
}
|
|
|
|
def := &models.StepDefinition{
|
|
StepDefinition: formatters.StepDefinition{
|
|
Handler: stepFunc,
|
|
Expr: regex,
|
|
},
|
|
HandlerValue: v,
|
|
}
|
|
|
|
if typ.NumOut() == 1 {
|
|
typ = typ.Out(0)
|
|
switch typ.Kind() {
|
|
case reflect.Interface:
|
|
if !typ.Implements(errorInterface) && !typ.Implements(contextInterface) {
|
|
panic(fmt.Sprintf("expected handler to return an error or context.Context, but got: %s", typ.Kind()))
|
|
}
|
|
case reflect.Slice:
|
|
if typ.Elem().Kind() != reflect.String {
|
|
panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Elem().Kind()))
|
|
}
|
|
def.Nested = true
|
|
default:
|
|
panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind()))
|
|
}
|
|
}
|
|
|
|
ctx.suite.steps = append(ctx.suite.steps, def)
|
|
}
|
|
|
|
// Build creates a test package like go test command at given target path.
|
|
// If there are no go files in tested directory, then
|
|
// it simply builds a godog executable to scan features.
|
|
//
|
|
// If there are go test files, it first builds a test
|
|
// package with standard go test command.
|
|
//
|
|
// Finally it generates godog suite executable which
|
|
// registers exported godog contexts from the test files
|
|
// of tested package.
|
|
//
|
|
// Returns the path to generated executable
|
|
func Build(bin string) error {
|
|
return builder.Build(bin)
|
|
}
|