Merge pull request #79 from DATA-DOG/multistep

implements convenient multistep definition
Этот коммит содержится в:
Gediminas Morkevicius 2017-04-29 23:12:28 +03:00 коммит произвёл GitHub
родитель 88d441e5ce f09230a6fd
коммит 38e3d1fb6d
20 изменённых файлов: 617 добавлений и 115 удалений

Просмотреть файл

@ -1,5 +1,11 @@
# Change LOG # Change LOG
**2017-04-29**
- added support for nested steps. From now on, it is possible to return
**godog.Steps** instead of an **error** in the step definition func.
This change introduced few minor changes in **Formatter** interface. Be
sure to adapt the changes if you have custom formatters.
**2017-04-27** **2017-04-27**
- added an option to randomize scenario execution order, so we could - added an option to randomize scenario execution order, so we could
ensure that scenarios do not depend on global state. ensure that scenarios do not depend on global state.

Просмотреть файл

@ -248,8 +248,11 @@ See implementation examples:
### FAQ ### FAQ
**Q:** Where can I configure common options globally? #### Configure common options for godog CLI
**A:** You can't. Alias your common or project based commands: `alias godog-wip="godog --format=progress --tags=@wip"`
There are no global options or configuration files. Alias your common or
project based commands: `alias godog-wip="godog --format=progress
--tags=@wip"`
### Contributions ### Contributions

Просмотреть файл

@ -20,6 +20,6 @@ Feature: get version
And the response should match json: And the response should match json:
""" """
{ {
"version": "v0.6.3" "version": "v0.7.0"
} }
""" """

Просмотреть файл

@ -14,7 +14,7 @@ Feature: event stream formatter
""" """
Scenario: should process simple scenario Scenario: should process simple scenario
Given a feature path "features/load.feature:22" Given a feature path "features/load.feature:23"
When I run feature suite with formatter "events" When I run feature suite with formatter "events"
Then the following events should be fired: Then the following events should be fired:
""" """
@ -35,7 +35,7 @@ Feature: event stream formatter
""" """
Scenario: should process outline scenario Scenario: should process outline scenario
Given a feature path "features/load.feature:30" Given a feature path "features/load.feature:31"
When I run feature suite with formatter "events" When I run feature suite with formatter "events"
Then the following events should be fired: Then the following events should be fired:
""" """

Просмотреть файл

@ -8,7 +8,7 @@ Savybė: užkrauti savybes
Scenarijus: savybių užkrovimas iš aplanko Scenarijus: savybių užkrovimas iš aplanko
Duota savybių aplankas "features" Duota savybių aplankas "features"
Kai aš išskaitau savybes Kai aš išskaitau savybes
Tada aš turėčiau turėti 9 savybių failus: Tada aš turėčiau turėti 10 savybių failus:
""" """
features/background.feature features/background.feature
features/events.feature features/events.feature
@ -16,6 +16,7 @@ Savybė: užkrauti savybes
features/formatter/events.feature features/formatter/events.feature
features/lang.feature features/lang.feature
features/load.feature features/load.feature
features/multistep.feature
features/outline.feature features/outline.feature
features/run.feature features/run.feature
features/snippets.feature features/snippets.feature

Просмотреть файл

@ -6,7 +6,7 @@ Feature: load features
Scenario: load features within path Scenario: load features within path
Given a feature path "features" Given a feature path "features"
When I parse features When I parse features
Then I should have 9 feature files: Then I should have 10 feature files:
""" """
features/background.feature features/background.feature
features/events.feature features/events.feature
@ -14,6 +14,7 @@ Feature: load features
features/formatter/events.feature features/formatter/events.feature
features/lang.feature features/lang.feature
features/load.feature features/load.feature
features/multistep.feature
features/outline.feature features/outline.feature
features/run.feature features/run.feature
features/snippets.feature features/snippets.feature

140
features/multistep.feature Обычный файл
Просмотреть файл

@ -0,0 +1,140 @@
Feature: run features with nested steps
In order to test multisteps
As a test suite
I need to be able to execute multisteps
Scenario: should run passing multistep successfully
Given a feature "normal.feature" file:
"""
Feature: normal feature
Scenario: run passing multistep
Given passing step
Then passing multistep
"""
When I run feature suite
Then the suite should have passed
And the following steps should be passed:
"""
passing step
passing multistep
"""
Scenario: should fail multistep
Given a feature "failed.feature" file:
"""
Feature: failed feature
Scenario: run failing multistep
Given passing step
When failing multistep
Then I should have 1 scenario registered
"""
When I run feature suite
Then the suite should have failed
And the following step should be failed:
"""
failing multistep
"""
And the following steps should be skipped:
"""
I should have 1 scenario registered
"""
And the following steps should be passed:
"""
passing step
"""
Scenario: should fail nested multistep
Given a feature "failed.feature" file:
"""
Feature: failed feature
Scenario: run failing nested multistep
Given failing nested multistep
When passing step
"""
When I run feature suite
Then the suite should have failed
And the following step should be failed:
"""
failing nested multistep
"""
And the following steps should be skipped:
"""
passing step
"""
Scenario: should skip steps after undefined multistep
Given a feature "undefined.feature" file:
"""
Feature: run undefined multistep
Scenario: run undefined multistep
Given passing step
When undefined multistep
Then passing multistep
"""
When I run feature suite
Then the suite should have passed
And the following step should be passed:
"""
passing step
"""
And the following step should be undefined:
"""
undefined multistep
"""
And the following step should be skipped:
"""
passing multistep
"""
Scenario: should match undefined steps in a row
Given a feature "undefined.feature" file:
"""
Feature: undefined feature
Scenario: parse a scenario
Given undefined step
When undefined multistep
Then I should have 1 scenario registered
"""
When I run feature suite
Then the suite should have passed
And the following steps should be undefined:
"""
undefined step
undefined multistep
"""
And the following step should be skipped:
"""
I should have 1 scenario registered
"""
Scenario: should mark undefined steps after pending
Given a feature "pending.feature" file:
"""
Feature: pending feature
Scenario: parse a scenario
Given pending step
When undefined step
Then undefined multistep
And I should have 1 scenario registered
"""
When I run feature suite
Then the suite should have passed
And the following steps should be undefined:
"""
undefined step
undefined multistep
"""
And the following step should be pending:
"""
pending step
"""
And the following step should be skipped:
"""
I should have 1 scenario registered
"""

24
fmt.go
Просмотреть файл

@ -105,8 +105,8 @@ type Formatter interface {
Defined(*gherkin.Step, *StepDef) Defined(*gherkin.Step, *StepDef)
Failed(*gherkin.Step, *StepDef, error) Failed(*gherkin.Step, *StepDef, error)
Passed(*gherkin.Step, *StepDef) Passed(*gherkin.Step, *StepDef)
Skipped(*gherkin.Step) Skipped(*gherkin.Step, *StepDef)
Undefined(*gherkin.Step) Undefined(*gherkin.Step, *StepDef)
Pending(*gherkin.Step, *StepDef) Pending(*gherkin.Step, *StepDef)
Summary() Summary()
} }
@ -210,21 +210,23 @@ func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) {
f.passed = append(f.passed, s) f.passed = append(f.passed, s)
} }
func (f *basefmt) Skipped(step *gherkin.Step) { func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) {
s := &stepResult{ s := &stepResult{
owner: f.owner, owner: f.owner,
feature: f.features[len(f.features)-1], feature: f.features[len(f.features)-1],
step: step, step: step,
def: match,
typ: skipped, typ: skipped,
} }
f.skipped = append(f.skipped, s) f.skipped = append(f.skipped, s)
} }
func (f *basefmt) Undefined(step *gherkin.Step) { func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) {
s := &stepResult{ s := &stepResult{
owner: f.owner, owner: f.owner,
feature: f.features[len(f.features)-1], feature: f.features[len(f.features)-1],
step: step, step: step,
def: match,
typ: undefined, typ: undefined,
} }
f.undefined = append(f.undefined, s) f.undefined = append(f.undefined, s)
@ -394,12 +396,19 @@ func (f *basefmt) snippets() string {
var snips []*undefinedSnippet var snips []*undefinedSnippet
// build snippets // build snippets
for _, u := range f.undefined { for _, u := range f.undefined {
expr := snippetExprCleanup.ReplaceAllString(u.step.Text, "\\$1") steps := []string{u.step.Text}
arg := u.step.Argument
if u.def != nil {
steps = u.def.undefined
arg = nil
}
for _, step := range steps {
expr := snippetExprCleanup.ReplaceAllString(step, "\\$1")
expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)")
expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2")
expr = "^" + strings.TrimSpace(expr) + "$" expr = "^" + strings.TrimSpace(expr) + "$"
name := snippetNumbers.ReplaceAllString(u.step.Text, " ") name := snippetNumbers.ReplaceAllString(step, " ")
name = snippetExprQuoted.ReplaceAllString(name, " ") name = snippetExprQuoted.ReplaceAllString(name, " ")
name = snippetMethodName.ReplaceAllString(name, "") name = snippetMethodName.ReplaceAllString(name, "")
var words []string var words []string
@ -425,7 +434,8 @@ func (f *basefmt) snippets() string {
} }
} }
if !found { if !found {
snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: u.step.Argument}) snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg})
}
} }
} }

Просмотреть файл

@ -297,16 +297,16 @@ func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) {
f.step(f.passed[len(f.passed)-1]) f.step(f.passed[len(f.passed)-1])
} }
func (f *cukefmt) Skipped(step *gherkin.Step) { func (f *cukefmt) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step) f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1]) f.step(f.skipped[len(f.skipped)-1])
// no duration reported for skipped. // no duration reported for skipped.
f.curStep.Result.Duration = nil f.curStep.Result.Duration = nil
} }
func (f *cukefmt) Undefined(step *gherkin.Step) { func (f *cukefmt) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step) f.basefmt.Undefined(step, match)
f.stat = undefined f.stat = undefined
f.step(f.undefined[len(f.undefined)-1]) f.step(f.undefined[len(f.undefined)-1])

Просмотреть файл

@ -245,13 +245,13 @@ func (f *events) Passed(step *gherkin.Step, match *StepDef) {
f.step(f.passed[len(f.passed)-1]) f.step(f.passed[len(f.passed)-1])
} }
func (f *events) Skipped(step *gherkin.Step) { func (f *events) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step) f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1]) f.step(f.skipped[len(f.skipped)-1])
} }
func (f *events) Undefined(step *gherkin.Step) { func (f *events) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step) f.basefmt.Undefined(step, match)
f.stat = undefined f.stat = undefined
f.step(f.undefined[len(f.undefined)-1]) f.step(f.undefined[len(f.undefined)-1])
} }

Просмотреть файл

@ -101,7 +101,7 @@ func (j *junitFormatter) Passed(step *gherkin.Step, match *StepDef) {
tcase.Status = "passed" tcase.Status = "passed"
} }
func (j *junitFormatter) Skipped(step *gherkin.Step) { func (j *junitFormatter) Skipped(step *gherkin.Step, match *StepDef) {
suite := j.current() suite := j.current()
tcase := suite.current() tcase := suite.current()
@ -112,7 +112,7 @@ func (j *junitFormatter) Skipped(step *gherkin.Step) {
}) })
} }
func (j *junitFormatter) Undefined(step *gherkin.Step) { func (j *junitFormatter) Undefined(step *gherkin.Step, match *StepDef) {
suite := j.current() suite := j.current()
tcase := suite.current() tcase := suite.current()
if tcase.Status != "undefined" { if tcase.Status != "undefined" {

Просмотреть файл

@ -334,13 +334,13 @@ func (f *pretty) Passed(step *gherkin.Step, match *StepDef) {
f.printStepKind(f.passed[len(f.passed)-1]) f.printStepKind(f.passed[len(f.passed)-1])
} }
func (f *pretty) Skipped(step *gherkin.Step) { func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step) f.basefmt.Skipped(step, match)
f.printStepKind(f.skipped[len(f.skipped)-1]) f.printStepKind(f.skipped[len(f.skipped)-1])
} }
func (f *pretty) Undefined(step *gherkin.Step) { func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step) f.basefmt.Undefined(step, match)
f.printStepKind(f.undefined[len(f.undefined)-1]) f.printStepKind(f.undefined[len(f.undefined)-1])
} }

Просмотреть файл

@ -91,17 +91,17 @@ func (f *progress) Passed(step *gherkin.Step, match *StepDef) {
f.step(f.passed[len(f.passed)-1]) f.step(f.passed[len(f.passed)-1])
} }
func (f *progress) Skipped(step *gherkin.Step) { func (f *progress) Skipped(step *gherkin.Step, match *StepDef) {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
f.basefmt.Skipped(step) f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1]) f.step(f.skipped[len(f.skipped)-1])
} }
func (f *progress) Undefined(step *gherkin.Step) { func (f *progress) Undefined(step *gherkin.Step, match *StepDef) {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
f.basefmt.Undefined(step) f.basefmt.Undefined(step, match)
f.step(f.undefined[len(f.undefined)-1]) f.step(f.undefined[len(f.undefined)-1])
} }

Просмотреть файл

@ -34,7 +34,6 @@ func TestProgressFormatterOutput(t *testing.T) {
}, },
} }
// var zeroDuration time.Duration
expected := ` expected := `
...F-.P-.UU.....F..P..U 23 ...F-.P-.UU.....F..P..U 23
@ -88,3 +87,177 @@ func trimAllLines(s string) string {
} }
return strings.Join(lines, "\n") return strings.Join(lines, "\n")
} }
var basicGherkinFeature = `
Feature: basic
Scenario: passing scenario
When one
Then two
`
func TestProgressFormatterWhenStepPanics(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { panic("omg") })
},
}
if !r.run() {
t.Fatal("the suite should have failed")
}
out := buf.String()
if idx := strings.Index(out, "github.com/DATA-DOG/godog/fmt_progress_test.go:116"); idx == -1 {
t.Fatal("expected to find panic stacktrace")
}
}
func TestProgressFormatterWithPassingMultisteps(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return nil })
s.Step(`^sub2$`, func() Steps { return Steps{"sub-sub", "sub1", "one"} })
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() Steps { return Steps{"sub1", "sub2"} })
},
}
if r.run() {
t.Fatal("the suite should have passed")
}
}
func TestProgressFormatterWithFailingMultisteps(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return fmt.Errorf("errored") })
s.Step(`^sub2$`, func() Steps { return Steps{"sub-sub", "sub1", "one"} })
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() Steps { return Steps{"sub1", "sub2"} })
},
}
if !r.run() {
t.Fatal("the suite should have failed")
}
}
func TestProgressFormatterWithPanicInMultistep(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return nil })
s.Step(`^sub2$`, func() []string { return []string{"sub-sub", "sub1", "one"} })
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() []string { return []string{"sub1", "sub2"} })
},
}
if !r.run() {
t.Fatal("the suite should have failed")
}
}
func TestProgressFormatterMultistepTemplates(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub-sub$`, func() error { return nil })
s.Step(`^substep$`, func() Steps { return Steps{"sub-sub", `unavailable "John" cost 5`, "one", "three"} })
s.Step(`^one$`, func() error { return nil })
s.Step(`^(t)wo$`, func(s string) Steps { return Steps{"undef", "substep"} })
},
}
if r.run() {
t.Fatal("the suite should have passed")
}
expected := `
.U 2
1 scenarios (1 undefined)
2 steps (1 passed, 1 undefined)
%s
Randomized with seed: %s
You can implement step definitions for undefined steps with these snippets:
func undef() error {
return godog.ErrPending
}
func unavailableCost(arg1 string, arg2 int) error {
return godog.ErrPending
}
func three() error {
return godog.ErrPending
}
func FeatureContext(s *godog.Suite) {
s.Step(` + "`^undef$`" + `, undef)
s.Step(` + "`^unavailable \"([^\"]*)\" cost (\\d+)$`" + `, unavailableCost)
s.Step(` + "`^three$`" + `, three)
}
`
var zeroDuration time.Duration
expected = fmt.Sprintf(expected, zeroDuration.String(), os.Getenv("GODOG_SEED"))
expected = trimAllLines(expected)
actual := trimAllLines(buf.String())
if actual != expected {
t.Fatalf("expected output does not match: %s", actual)
}
}

Просмотреть файл

@ -42,4 +42,4 @@ Godog was inspired by Behat and Cucumber the above description is taken from it'
package godog package godog
// Version of package - based on Semantic Versioning 2.0.0 http://semver.org/ // Version of package - based on Semantic Versioning 2.0.0 http://semver.org/
const Version = "v0.6.3" const Version = "v0.7.0"

8
run.go
Просмотреть файл

@ -54,7 +54,7 @@ func (r *runner) concurrent(rate int) (failed bool) {
return return
} }
func (r *runner) run() (failed bool) { func (r *runner) run() bool {
suite := &Suite{ suite := &Suite{
fmt: r.fmt, fmt: r.fmt,
randomSeed: r.randomSeed, randomSeed: r.randomSeed,
@ -160,13 +160,11 @@ func Run(suite string, contextInitializer func(suite *Suite)) int {
func supportsConcurrency(format string) bool { func supportsConcurrency(format string) bool {
switch format { switch format {
case "events": case "events":
return false
case "junit": case "junit":
return false
case "pretty": case "pretty":
return false
case "cucumber": case "cucumber":
return false default:
return true // supports concurrency
} }
return true // all custom formatters are treated as supporting concurrency return true // all custom formatters are treated as supporting concurrency

Просмотреть файл

@ -2,10 +2,13 @@ package godog
import ( import (
"bytes" "bytes"
"fmt"
"io/ioutil"
"strings" "strings"
"testing" "testing"
"github.com/DATA-DOG/godog/colors" "github.com/DATA-DOG/godog/colors"
"github.com/DATA-DOG/godog/gherkin"
) )
func okStep() error { func okStep() error {
@ -50,3 +53,43 @@ func TestPrintsNoStepDefinitionsIfNoneFound(t *testing.T) {
t.Fatalf("expected output does not match to: %s", out) t.Fatalf("expected output does not match to: %s", out)
} }
} }
func TestShouldNotFailWhenHasPendingSteps(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
r := runner{
fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { return ErrPending })
},
}
if r.run() {
t.Fatal("the suite should have passed")
}
}
func TestShouldFailOnError(t *testing.T) {
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
r := runner{
fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { return fmt.Errorf("error") })
},
}
if !r.run() {
t.Fatal("the suite should have failed")
}
}

Просмотреть файл

@ -14,6 +14,22 @@ import (
var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`) var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
// 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
// StepDef is a registered step definition // StepDef is a registered step definition
// contains a StepHandler and regexp which // contains a StepHandler and regexp which
// is used to match a step. Args which // is used to match a step. Args which
@ -27,6 +43,10 @@ type StepDef struct {
hv reflect.Value hv reflect.Value
Expr *regexp.Regexp Expr *regexp.Regexp
Handler interface{} Handler interface{}
// multistep related
nested bool
undefined []string
} }
func (sd *StepDef) definitionID() string { func (sd *StepDef) definitionID() string {
@ -53,7 +73,7 @@ func (sd *StepDef) definitionID() string {
// run a step with the matched arguments using // run a step with the matched arguments using
// reflect // reflect
func (sd *StepDef) run() error { func (sd *StepDef) run() interface{} {
typ := sd.hv.Type() typ := sd.hv.Type()
if len(sd.args) < typ.NumIn() { if len(sd.args) < typ.NumIn() {
return fmt.Errorf("func expects %d arguments, which is more than %d matched from step", typ.NumIn(), len(sd.args)) return fmt.Errorf("func expects %d arguments, which is more than %d matched from step", typ.NumIn(), len(sd.args))
@ -171,12 +191,7 @@ func (sd *StepDef) run() error {
return fmt.Errorf("the argument %d type %s is not supported", i, param.Kind()) return fmt.Errorf("the argument %d type %s is not supported", i, param.Kind())
} }
} }
ret := sd.hv.Call(values)[0].Interface() return sd.hv.Call(values)[0].Interface()
if nil == ret {
return nil
}
return ret.(error)
} }
func (sd *StepDef) shouldBeString(idx int) (string, error) { func (sd *StepDef) shouldBeString(idx int) (string, error) {

162
suite.go
Просмотреть файл

@ -95,17 +95,33 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) {
if typ.Kind() != reflect.Func { if typ.Kind() != reflect.Func {
panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc)) panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc))
} }
if typ.NumOut() != 1 { if typ.NumOut() != 1 {
panic(fmt.Sprintf("expected handler to return an error, but it has more values in return: %d", typ.NumOut())) panic(fmt.Sprintf("expected handler to return only one value, but it has: %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())) def := &StepDef{
}
s.steps = append(s.steps, &StepDef{
Handler: stepFunc, Handler: stepFunc,
Expr: regex, Expr: regex,
hv: v, hv: v,
}) }
typ = typ.Out(0)
switch typ.Kind() {
case reflect.Interface:
if !typ.Implements(errorInterface) {
panic(fmt.Sprintf("expected handler to return an error, 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.Kind()))
}
def.nested = true
default:
panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind()))
}
s.steps = append(s.steps, def)
} }
// BeforeSuite registers a function or method // BeforeSuite registers a function or method
@ -184,41 +200,18 @@ func (s *Suite) run() {
} }
func (s *Suite) matchStep(step *gherkin.Step) *StepDef { func (s *Suite) matchStep(step *gherkin.Step) *StepDef {
for _, h := range s.steps { def := s.matchStepText(step.Text)
if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 { if def != nil && step.Argument != nil {
var args []interface{} def.args = append(def.args, step.Argument)
for _, m := range m[1:] {
args = append(args, m)
} }
if step.Argument != nil { return def
args = append(args, step.Argument)
}
h.args = args
return h
}
}
// @TODO can handle ambiguous
return nil
} }
func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) {
match := s.matchStep(step) match := s.matchStep(step)
s.fmt.Defined(step, match) s.fmt.Defined(step, match)
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)
}
// user multistep definitions may panic
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
err = &traceError{ err = &traceError{
@ -226,6 +219,15 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) {
stack: callStack(), stack: callStack(),
} }
} }
if prevStepErr != nil {
return
}
if err == ErrUndefined {
return
}
switch err { switch err {
case nil: case nil:
s.fmt.Passed(step, match) s.fmt.Passed(step, match)
@ -241,10 +243,98 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) {
} }
}() }()
err = match.run() if undef := s.maybeUndefined(step.Text); len(undef) > 0 {
if match != nil {
match = &StepDef{
args: match.args,
hv: match.hv,
Expr: match.Expr,
Handler: match.Handler,
nested: match.nested,
undefined: undef,
}
}
s.fmt.Undefined(step, match)
return ErrUndefined
}
if prevStepErr != nil {
s.fmt.Skipped(step, match)
return nil
}
// run before step handlers
for _, f := range s.beforeStepHandlers {
f(step)
}
err = s.maybeSubSteps(match.run())
return return
} }
func (s *Suite) maybeUndefined(text string) (undefined []string) {
step := s.matchStepText(text)
if nil == step {
undefined = append(undefined, text)
return
}
if !step.nested {
return
}
for _, next := range step.run().(Steps) {
undefined = append(undefined, s.maybeUndefined(next)...)
}
return
}
func (s *Suite) maybeSubSteps(result interface{}) error {
if nil == result {
return nil
}
if err, ok := result.(error); ok {
return err
}
steps, ok := result.(Steps)
if !ok {
return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result)
}
for _, text := range steps {
if def := s.matchStepText(text); def == nil {
return ErrUndefined
} else if err := s.maybeSubSteps(def.run()); err != nil {
return err
}
}
return nil
}
func (s *Suite) matchStepText(text string) *StepDef {
for _, h := range s.steps {
if m := h.Expr.FindStringSubmatch(text); len(m) > 0 {
var args []interface{}
for _, m := range m[1:] {
args = append(args, m)
}
// since we need to assign arguments
// better to copy the step definition
return &StepDef{
args: args,
hv: h.hv,
Expr: h.Expr,
Handler: h.Handler,
nested: h.nested,
}
}
}
return nil
}
func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) {
err = prevErr err = prevErr
for _, step := range steps { for _, step := range steps {
@ -264,7 +354,7 @@ func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) {
func (s *Suite) skipSteps(steps []*gherkin.Step) { func (s *Suite) skipSteps(steps []*gherkin.Step) {
for _, step := range steps { for _, step := range steps {
s.fmt.Skipped(step) s.fmt.Skipped(step, s.matchStep(step))
} }
} }

Просмотреть файл

@ -20,14 +20,20 @@ func TestMain(m *testing.M) {
format := "progress" // non verbose mode format := "progress" // non verbose mode
concurrency := 4 concurrency := 4
var specific bool
for _, arg := range os.Args[1:] { for _, arg := range os.Args[1:] {
if arg == "-test.v=true" { // go test transforms -v option - verbose mode if arg == "-test.v=true" { // go test transforms -v option - verbose mode
format = "pretty" format = "pretty"
concurrency = 1 concurrency = 1
break break
} }
if strings.Index(arg, "-test.run") == 0 {
specific = true
} }
status := RunWithOptions("godog", func(s *Suite) { }
var status int
if !specific {
status = RunWithOptions("godog", func(s *Suite) {
SuiteContext(s) SuiteContext(s)
}, Options{ }, Options{
Format: format, // pretty format for verbose mode, otherwise - progress Format: format, // pretty format for verbose mode, otherwise - progress
@ -35,6 +41,7 @@ func TestMain(m *testing.M) {
Concurrency: concurrency, // concurrency for verbose mode is 1 Concurrency: concurrency, // concurrency for verbose mode is 1
Randomize: time.Now().UnixNano(), // randomize scenario execution order Randomize: time.Now().UnixNano(), // randomize scenario execution order
}) })
}
if st := m.Run(); st > status { if st := m.Run(); st > status {
status = st status = st
@ -89,6 +96,21 @@ func SuiteContext(s *Suite) {
// Introduced to test formatter/cucumber.feature // Introduced to test formatter/cucumber.feature
s.Step(`^the rendered json will be as follows:$`, c.theRenderJSONWillBe) s.Step(`^the rendered json will be as follows:$`, c.theRenderJSONWillBe)
s.Step(`^failing multistep$`, func() Steps {
return Steps{"passing step", "failing step"}
})
s.Step(`^undefined multistep$`, func() Steps {
return Steps{"passing step", "undefined step", "passing step"}
})
s.Step(`^passing multistep$`, func() Steps {
return Steps{"passing step", "passing step", "passing step"}
})
s.Step(`^failing nested multistep$`, func() Steps {
return Steps{"passing step", "passing multistep", "failing multistep"}
})
} }
type firedEvent struct { type firedEvent struct {