godog/suite.go

496 строки
12 КиБ
Go

package godog
import (
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/DATA-DOG/godog/gherkin"
)
// Regexp is an unified type for regular expression
// it can be either a string or a *regexp.Regexp
type Regexp interface{}
// StepHandler is a func to handle the step
//
// The handler receives all arguments which
// will be matched according to the Regexp
// which is passed with a step registration.
//
// The error in return - represents a reason of failure.
// All consequent scenario steps are skipped.
//
// Returning signals that the step has finished and that
// the feature runner can move on to the next step.
type StepHandler func(...*Arg) error
// ErrUndefined is returned in case if step definition was not found
var ErrUndefined = fmt.Errorf("step is undefined")
// StepDef 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 StepDef struct {
Args []*Arg
Handler StepHandler
Expr *regexp.Regexp
}
// Suite is an interface which allows various contexts
// to register steps and event handlers.
//
// When running a test suite, this interface 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 Suite interface {
Run()
Step(expr Regexp, h StepHandler)
// suite events
BeforeSuite(f func())
BeforeScenario(f func(*gherkin.Scenario))
BeforeStep(f func(*gherkin.Step))
AfterStep(f func(*gherkin.Step, error))
AfterScenario(f func(*gherkin.Scenario, error))
AfterSuite(f func())
}
type suite struct {
stepHandlers []*StepDef
features []*gherkin.Feature
fmt Formatter
failed bool
// options
paths []string
format string
tags string
definitions bool
stopOnFailure bool
version bool
// suite event handlers
beforeSuiteHandlers []func()
beforeScenarioHandlers []func(*gherkin.Scenario)
beforeStepHandlers []func(*gherkin.Step)
afterStepHandlers []func(*gherkin.Step, error)
afterScenarioHandlers []func(*gherkin.Scenario, error)
afterSuiteHandlers []func()
}
// New initializes a Suite. The instance is passed around
// to all context initialization functions from *_test.go files
func New() Suite {
return &suite{}
}
// Step allows to register a StepHandler in Godog
// feature suite, the handler will be applied to all
// steps matching the given Regexp expr
//
// It will panic if expr is not a valid regular expression
//
// Note that if there are two handlers which may match
// the same step, then the only first matched handler
// will be applied.
//
// If none of the StepHandlers are matched, then
// ErrUndefined error will be returned.
func (s *suite) Step(expr Regexp, h StepHandler) {
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))
}
s.stepHandlers = append(s.stepHandlers, &StepDef{
Handler: h,
Expr: regex,
})
}
// 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 (s *suite) BeforeSuite(f func()) {
s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, f)
}
// 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.
func (s *suite) BeforeScenario(f func(*gherkin.Scenario)) {
s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f)
}
// BeforeStep registers a function or method
// to be run before every scenario
func (s *suite) BeforeStep(f func(*gherkin.Step)) {
s.beforeStepHandlers = append(s.beforeStepHandlers, f)
}
// AfterStep registers an function or method
// to be run after every scenario
//
// 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 (s *suite) AfterStep(f func(*gherkin.Step, error)) {
s.afterStepHandlers = append(s.afterStepHandlers, f)
}
// AfterScenario registers an function or method
// to be run after every scenario
func (s *suite) AfterScenario(f func(*gherkin.Scenario, error)) {
s.afterScenarioHandlers = append(s.afterScenarioHandlers, f)
}
// AfterSuite registers a function or method
// to be run once after suite runner
func (s *suite) AfterSuite(f func()) {
s.afterSuiteHandlers = append(s.afterSuiteHandlers, f)
}
// Run starts the Godog feature suite
func (s *suite) Run() {
flagSet := flags(s)
fatal(flagSet.Parse(os.Args[1:]))
s.paths = flagSet.Args()
// check the default path
if len(s.paths) == 0 {
inf, err := os.Stat("features")
if err == nil && inf.IsDir() {
s.paths = []string{"features"}
}
}
// validate formatter
var names []string
for _, f := range formatters {
if f.name == s.format {
s.fmt = f.fmt
break
}
names = append(names, f.name)
}
if s.fmt == nil {
fatal(fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, s.format, strings.Join(names, ", ")))
}
// check if we need to just show something first
switch {
case s.version:
fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow))
return
case s.definitions:
s.printStepDefinitions()
return
}
fatal(s.parseFeatures())
// run a feature suite
s.run()
if s.failed {
os.Exit(1)
}
}
func (s *suite) run() {
// run before suite handlers
for _, f := range s.beforeSuiteHandlers {
f()
}
// run features
for _, f := range s.features {
s.runFeature(f)
if s.failed && s.stopOnFailure {
// stop on first failure
break
}
}
// run after suite handlers
for _, f := range s.afterSuiteHandlers {
f()
}
s.fmt.Summary()
}
func (s *suite) matchStep(step *gherkin.Step) *StepDef {
for _, h := range s.stepHandlers {
if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 {
var args []*Arg
for _, a := range m[1:] {
args = append(args, &Arg{value: a})
}
if step.Table != nil {
args = append(args, &Arg{value: step.Table})
}
if step.PyString != nil {
args = append(args, &Arg{value: step.PyString})
}
h.Args = args
return h
}
}
return nil
}
func (s *suite) runStep(step *gherkin.Step) (err error) {
match := s.matchStep(step)
if match == nil {
s.fmt.Undefined(step)
return ErrUndefined
}
defer func() {
if e := recover(); e != nil {
err = e.(error)
s.fmt.Failed(step, match, err)
}
}()
if err = match.Handler(match.Args...); err != nil {
s.fmt.Failed(step, match, err)
} else {
s.fmt.Passed(step, match)
}
return
}
func (s *suite) runSteps(steps []*gherkin.Step) (err error) {
for _, step := range steps {
if err != nil {
s.fmt.Skipped(step)
continue
}
// run before step handlers
for _, f := range s.beforeStepHandlers {
f(step)
}
err = s.runStep(step)
// run after step handlers
for _, f := range s.afterStepHandlers {
f(step, err)
}
}
return
}
func (s *suite) skipSteps(steps []*gherkin.Step) {
for _, step := range steps {
s.fmt.Skipped(step)
}
}
func (s *suite) runOutline(scenario *gherkin.Scenario) (err error) {
placeholders := scenario.Outline.Examples.Rows[0]
examples := scenario.Outline.Examples.Rows[1:]
for _, example := range examples {
var steps []*gherkin.Step
for _, step := range scenario.Outline.Steps {
text := step.Text
for i, placeholder := range placeholders {
text = strings.Replace(text, "<"+placeholder+">", example[i], -1)
}
// clone a step
cloned := &gherkin.Step{
Token: step.Token,
Text: text,
Type: step.Type,
PyString: step.PyString,
Table: step.Table,
Background: step.Background,
Scenario: scenario,
}
steps = append(steps, cloned)
}
// set steps to scenario
scenario.Steps = steps
if err = s.runScenario(scenario); err != nil && err != ErrUndefined {
s.failed = true
if s.stopOnFailure {
return
}
}
}
return
}
func (s *suite) runFeature(f *gherkin.Feature) {
s.fmt.Node(f)
for _, scenario := range f.Scenarios {
var err error
// handle scenario outline differently
if scenario.Outline != nil {
err = s.runOutline(scenario)
} else {
err = s.runScenario(scenario)
}
if err != nil && err != ErrUndefined {
s.failed = true
if s.stopOnFailure {
return
}
}
}
}
func (s *suite) runScenario(scenario *gherkin.Scenario) (err error) {
// run before scenario handlers
for _, f := range s.beforeScenarioHandlers {
f(scenario)
}
// background
if scenario.Feature.Background != nil {
s.fmt.Node(scenario.Feature.Background)
err = s.runSteps(scenario.Feature.Background.Steps)
}
// scenario
s.fmt.Node(scenario)
switch err {
case ErrUndefined:
s.skipSteps(scenario.Steps)
case nil:
err = s.runSteps(scenario.Steps)
default:
s.skipSteps(scenario.Steps)
}
// run after scenario handlers
for _, f := range s.afterScenarioHandlers {
f(scenario, err)
}
return
}
func (s *suite) printStepDefinitions() {
var longest int
for _, def := range s.stepHandlers {
if longest < len(def.Expr.String()) {
longest = len(def.Expr.String())
}
}
for _, def := range s.stepHandlers {
location := runtime.FuncForPC(reflect.ValueOf(def.Handler).Pointer()).Name()
spaces := strings.Repeat(" ", longest-len(def.Expr.String()))
fmt.Println(cl(def.Expr.String(), yellow)+spaces, cl("# "+location, black))
}
if len(s.stepHandlers) == 0 {
fmt.Println("there were no contexts registered, could not find any step definition..")
}
}
func (s *suite) parseFeatures() (err error) {
for _, pat := range s.paths {
// check if line number is specified
parts := strings.Split(pat, ":")
path := parts[0]
line := -1
if len(parts) > 1 {
line, err = strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("line number should follow after colon path delimiter")
}
}
// parse features
err = filepath.Walk(path, func(p string, f os.FileInfo, err error) error {
if err == nil && !f.IsDir() && strings.HasSuffix(p, ".feature") {
ft, err := gherkin.ParseFile(p)
switch {
case err == gherkin.ErrEmpty:
// its ok, just skip it
case err != nil:
return err
default:
s.features = append(s.features, ft)
}
// filter scenario by line number
if line != -1 {
var scenarios []*gherkin.Scenario
for _, s := range ft.Scenarios {
if s.Token.Line == line {
scenarios = append(scenarios, s)
break
}
}
ft.Scenarios = scenarios
}
s.applyTagFilter(ft)
}
return err
})
// check error
switch {
case os.IsNotExist(err):
return fmt.Errorf(`feature path "%s" is not available`, path)
case os.IsPermission(err):
return fmt.Errorf(`feature path "%s" is not accessible`, path)
case err != nil:
return err
}
}
return
}
func (s *suite) applyTagFilter(ft *gherkin.Feature) {
if len(s.tags) == 0 {
return
}
for _, tag := range strings.Split(s.tags, " ") {
var scenarios []*gherkin.Scenario
var inverse bool
if tag[0] == '!' {
tag = tag[1:]
inverse = true
}
for _, scenario := range ft.Scenarios {
if inverse && !scenario.Tags.Has(gherkin.Tag(tag)) {
scenarios = append(scenarios, scenario)
} else if !inverse && scenario.Tags.Has(gherkin.Tag(tag)) {
scenarios = append(scenarios, scenario)
}
}
ft.Scenarios = scenarios
}
}