godog/suite.go
gedi e9c3d69e2d be compatible with gherkin library changes
the downside is that if outline examples are empty
it won't be pretty printed as node. But that does not make
much difference anyway
2016-03-04 12:59:56 +02:00

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

package godog
import (
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"gopkg.in/cucumber/gherkin-go.v3"
)
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
var typeOfBytes = reflect.TypeOf([]byte(nil))
type feature struct {
*gherkin.Feature
Path string `json:"path"`
}
// ErrUndefined is returned in case if step definition was not found
var ErrUndefined = fmt.Errorf("step is undefined")
// ErrPending should be returned by step definition if
// step implementation is pending
var ErrPending = fmt.Errorf("step implementation is pending")
// Suite allows various contexts
// to register steps and event handlers.
//
// When running a test suite, the instance of Suite
// 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 struct {
steps []*StepDef
features []*feature
fmt Formatter
failed bool
stopOnFailure bool
// suite event handlers
beforeSuiteHandlers []func()
beforeScenarioHandlers []func(interface{})
beforeStepHandlers []func(*gherkin.Step)
afterStepHandlers []func(*gherkin.Step, error)
afterScenarioHandlers []func(interface{}, error)
afterSuiteHandlers []func()
}
// Step allows to register a *StepDef in 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.
//
// 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 *StepDef is matched, then
// ErrUndefined error will be returned when
// running steps.
func (s *Suite) Step(expr interface{}, 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() != 1 {
panic(fmt.Sprintf("expected handler to return an error, but it has more values in return: %d", typ.NumOut()))
}
if typ.Out(0).Kind() != reflect.Interface || !typ.Out(0).Implements(errorInterface) {
panic(fmt.Sprintf("expected handler to return an error interface, but we have: %s", typ.Out(0).Kind()))
}
s.steps = append(s.steps, &StepDef{
Handler: stepFunc,
Expr: regex,
hv: v,
})
}
// 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 or scenario outline.
//
// The interface argument may be *gherkin.Scenario
// or *gherkin.ScenarioOutline
//
// 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(interface{})) {
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 or scenario outline
//
// The interface argument may be *gherkin.Scenario
// or *gherkin.ScenarioOutline
func (s *Suite) AfterScenario(f func(interface{}, 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)
}
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()
}
}
func (s *Suite) matchStep(step *gherkin.Step) *StepDef {
for _, h := range s.steps {
if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 {
var args []interface{}
for _, m := range m[1:] {
args = append(args, m)
}
if step.Argument != nil {
args = append(args, step.Argument)
}
h.args = args
return h
}
}
return nil
}
func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) {
match := s.matchStep(step)
if match == nil {
s.fmt.Undefined(step)
return ErrUndefined
}
if prevStepErr != nil {
s.fmt.Skipped(step)
return nil
}
// run before step handlers
for _, f := range s.beforeStepHandlers {
f(step)
}
defer func() {
if e := recover(); e != nil {
err, ok := e.(error)
if !ok {
err = fmt.Errorf(e.(string))
}
s.fmt.Failed(step, match, err)
}
}()
err = match.run()
switch err {
case nil:
s.fmt.Passed(step, match)
case ErrPending:
s.fmt.Pending(step, match)
default:
s.fmt.Failed(step, match, err)
}
// run after step handlers
for _, f := range s.afterStepHandlers {
f(step, err)
}
return
}
func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) {
err = prevErr
for _, step := range steps {
stepErr := s.runStep(step, err)
switch stepErr {
case ErrUndefined:
err = stepErr
case ErrPending:
err = stepErr
case nil:
default:
err = stepErr
}
}
return
}
func (s *Suite) skipSteps(steps []*gherkin.Step) {
for _, step := range steps {
s.fmt.Skipped(step)
}
}
func (s *Suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Background) (failErr error) {
s.fmt.Node(outline)
for _, ex := range outline.Examples {
example, hasExamples := examples(ex)
if !hasExamples {
// @TODO: may need to print empty example node, but
// for backward compatibility, cannot cast to *gherkin.ExamplesBase
// at the moment
continue
}
s.fmt.Node(example)
placeholders := example.TableHeader.Cells
groups := example.TableBody
for _, group := range groups {
for _, f := range s.beforeScenarioHandlers {
f(outline)
}
var steps []*gherkin.Step
for _, outlineStep := range outline.Steps {
text := outlineStep.Text
for i, placeholder := range placeholders {
text = strings.Replace(text, "<"+placeholder.Value+">", group.Cells[i].Value, -1)
}
// clone a step
step := &gherkin.Step{
Node: outlineStep.Node,
Text: text,
Keyword: outlineStep.Keyword,
Argument: outlineStep.Argument,
}
steps = append(steps, step)
}
// run example table row
s.fmt.Node(group)
// run background
var err error
if b != nil {
err = s.runSteps(b.Steps, err)
}
err = s.runSteps(steps, err)
for _, f := range s.afterScenarioHandlers {
f(outline, err)
}
if err != nil && err != ErrUndefined && err != ErrPending {
failErr = err
if s.stopOnFailure {
return
}
}
}
}
return
}
func (s *Suite) runFeature(f *feature) {
s.fmt.Feature(f.Feature, f.Path)
for _, scenario := range f.ScenarioDefinitions {
var err error
if f.Background != nil {
s.fmt.Node(f.Background)
}
switch t := scenario.(type) {
case *gherkin.ScenarioOutline:
err = s.runOutline(t, f.Background)
case *gherkin.Scenario:
err = s.runScenario(t, f.Background)
}
if err != nil && err != ErrUndefined && err != ErrPending {
s.failed = true
if s.stopOnFailure {
return
}
}
}
}
func (s *Suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (err error) {
// run before scenario handlers
for _, f := range s.beforeScenarioHandlers {
f(scenario)
}
s.fmt.Node(scenario)
// background
if b != nil {
err = s.runSteps(b.Steps, err)
}
// scenario
err = s.runSteps(scenario.Steps, err)
// run after scenario handlers
for _, f := range s.afterScenarioHandlers {
f(scenario, err)
}
return
}
func (s *Suite) printStepDefinitions() {
var longest int
for _, def := range s.steps {
if longest < len(def.Expr.String()) {
longest = len(def.Expr.String())
}
}
for _, def := range s.steps {
location := runtime.FuncForPC(def.hv.Pointer()).Name()
spaces := strings.Repeat(" ", longest-len(def.Expr.String()))
fmt.Println(cl(def.Expr.String(), yellow)+spaces, cl("# "+location, black))
}
if len(s.steps) == 0 {
fmt.Println("there were no contexts registered, could not find any step definition..")
}
}
func parseFeatures(filter string, paths []string) (features []*feature, err error) {
for _, pat := range 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 features, 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") {
reader, err := os.Open(p)
if err != nil {
return err
}
ft, err := gherkin.ParseFeature(reader)
reader.Close()
if err != nil {
return err
}
features = append(features, &feature{Path: p, Feature: ft})
// filter scenario by line number
if line != -1 {
var scenarios []interface{}
for _, def := range ft.ScenarioDefinitions {
var ln int
switch t := def.(type) {
case *gherkin.Scenario:
ln = t.Location.Line
case *gherkin.ScenarioOutline:
ln = t.Location.Line
}
if ln == line {
scenarios = append(scenarios, def)
break
}
}
ft.ScenarioDefinitions = scenarios
}
applyTagFilter(filter, ft)
}
return err
})
// check error
switch {
case os.IsNotExist(err):
return features, fmt.Errorf(`feature path "%s" is not available`, path)
case os.IsPermission(err):
return features, fmt.Errorf(`feature path "%s" is not accessible`, path)
case err != nil:
return features, err
}
}
return
}
func applyTagFilter(tags string, ft *gherkin.Feature) {
if len(tags) == 0 {
return
}
var scenarios []interface{}
for _, scenario := range ft.ScenarioDefinitions {
if matchesTags(tags, allTags(ft, scenario)) {
scenarios = append(scenarios, scenario)
}
}
ft.ScenarioDefinitions = scenarios
}
func allTags(nodes ...interface{}) []string {
var tags, tmp []string
for _, node := range nodes {
var gr []*gherkin.Tag
switch t := node.(type) {
case *gherkin.Feature:
gr = t.Tags
case *gherkin.ScenarioOutline:
gr = t.Tags
case *gherkin.Scenario:
gr = t.Tags
case *gherkin.Examples:
gr = t.Tags
}
for _, gtag := range gr {
tag := strings.TrimSpace(gtag.Name)
if tag[0] == '@' {
tag = tag[1:]
}
copy(tmp, tags)
var found bool
for _, tg := range tmp {
if tg == tag {
found = true
break
}
}
if !found {
tags = append(tags, tag)
}
}
}
return tags
}
func hasTag(tags []string, tag string) bool {
for _, t := range tags {
if t == tag {
return true
}
}
return false
}
// based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters
func matchesTags(filter string, tags []string) (ok bool) {
ok = true
for _, andTags := range strings.Split(filter, "&&") {
var okComma bool
for _, tag := range strings.Split(andTags, ",") {
tag = strings.Replace(strings.TrimSpace(tag), "@", "", -1)
if tag[0] == '~' {
tag = tag[1:]
okComma = !hasTag(tags, tag) || okComma
} else {
okComma = hasTag(tags, tag) || okComma
}
}
ok = (false != okComma && ok && okComma) || false
}
return
}