diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ea0f1..048f484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 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** - added an option to randomize scenario execution order, so we could ensure that scenarios do not depend on global state. diff --git a/README.md b/README.md index cf32125..882d2c2 100644 --- a/README.md +++ b/README.md @@ -248,8 +248,11 @@ See implementation examples: ### FAQ -**Q:** Where can I configure common options globally? -**A:** You can't. Alias your common or project based commands: `alias godog-wip="godog --format=progress --tags=@wip"` +#### Configure common options for godog CLI + +There are no global options or configuration files. Alias your common or +project based commands: `alias godog-wip="godog --format=progress +--tags=@wip"` ### Contributions diff --git a/examples/api/version.feature b/examples/api/version.feature index 8136d93..3664fa2 100644 --- a/examples/api/version.feature +++ b/examples/api/version.feature @@ -20,6 +20,6 @@ Feature: get version And the response should match json: """ { - "version": "v0.6.3" + "version": "v0.7.0" } """ diff --git a/features/formatter/events.feature b/features/formatter/events.feature index cd99b33..5cfa8cf 100644 --- a/features/formatter/events.feature +++ b/features/formatter/events.feature @@ -14,7 +14,7 @@ Feature: event stream formatter """ 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" Then the following events should be fired: """ @@ -35,7 +35,7 @@ Feature: event stream formatter """ 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" Then the following events should be fired: """ diff --git a/features/lang.feature b/features/lang.feature index 829a939..594786e 100644 --- a/features/lang.feature +++ b/features/lang.feature @@ -8,7 +8,7 @@ Savybė: užkrauti savybes Scenarijus: savybių užkrovimas iš aplanko Duota savybių aplankas "features" 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/events.feature @@ -16,6 +16,7 @@ Savybė: užkrauti savybes features/formatter/events.feature features/lang.feature features/load.feature + features/multistep.feature features/outline.feature features/run.feature features/snippets.feature diff --git a/features/load.feature b/features/load.feature index 22ed683..7b64dec 100644 --- a/features/load.feature +++ b/features/load.feature @@ -6,7 +6,7 @@ Feature: load features Scenario: load features within path Given a feature path "features" When I parse features - Then I should have 9 feature files: + Then I should have 10 feature files: """ features/background.feature features/events.feature @@ -14,6 +14,7 @@ Feature: load features features/formatter/events.feature features/lang.feature features/load.feature + features/multistep.feature features/outline.feature features/run.feature features/snippets.feature diff --git a/features/multistep.feature b/features/multistep.feature new file mode 100644 index 0000000..95885dd --- /dev/null +++ b/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 + """ diff --git a/fmt.go b/fmt.go index 8aff4aa..0123969 100644 --- a/fmt.go +++ b/fmt.go @@ -105,8 +105,8 @@ type Formatter interface { Defined(*gherkin.Step, *StepDef) Failed(*gherkin.Step, *StepDef, error) Passed(*gherkin.Step, *StepDef) - Skipped(*gherkin.Step) - Undefined(*gherkin.Step) + Skipped(*gherkin.Step, *StepDef) + Undefined(*gherkin.Step, *StepDef) Pending(*gherkin.Step, *StepDef) Summary() } @@ -210,21 +210,23 @@ func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) { f.passed = append(f.passed, s) } -func (f *basefmt) Skipped(step *gherkin.Step) { +func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) { s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, + def: match, typ: skipped, } f.skipped = append(f.skipped, s) } -func (f *basefmt) Undefined(step *gherkin.Step) { +func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) { s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, + def: match, typ: undefined, } f.undefined = append(f.undefined, s) @@ -394,38 +396,46 @@ func (f *basefmt) snippets() string { var snips []*undefinedSnippet // build snippets for _, u := range f.undefined { - expr := snippetExprCleanup.ReplaceAllString(u.step.Text, "\\$1") - expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") - expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") - expr = "^" + strings.TrimSpace(expr) + "$" + 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 = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") + expr = "^" + strings.TrimSpace(expr) + "$" - name := snippetNumbers.ReplaceAllString(u.step.Text, " ") - name = snippetExprQuoted.ReplaceAllString(name, " ") - name = snippetMethodName.ReplaceAllString(name, "") - var words []string - for i, w := range strings.Split(name, " ") { - if i != 0 { - w = strings.Title(w) - } else { - w = string(unicode.ToLower(rune(w[0]))) + w[1:] + name := snippetNumbers.ReplaceAllString(step, " ") + name = snippetExprQuoted.ReplaceAllString(name, " ") + name = snippetMethodName.ReplaceAllString(name, "") + var words []string + for i, w := range strings.Split(name, " ") { + if i != 0 { + w = strings.Title(w) + } else { + w = string(unicode.ToLower(rune(w[0]))) + w[1:] + } + words = append(words, w) + } + name = strings.Join(words, "") + if len(name) == 0 { + index++ + name = fmt.Sprintf("stepDefinition%d", index) } - words = append(words, w) - } - name = strings.Join(words, "") - if len(name) == 0 { - index++ - name = fmt.Sprintf("stepDefinition%d", index) - } - var found bool - for _, snip := range snips { - if snip.Expr == expr { - found = true - break + var found bool + for _, snip := range snips { + if snip.Expr == expr { + found = true + break + } + } + if !found { + snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg}) } - } - if !found { - snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: u.step.Argument}) } } diff --git a/fmt_cucumber.go b/fmt_cucumber.go index b0afe03..19897ef 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -297,16 +297,16 @@ func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) { f.step(f.passed[len(f.passed)-1]) } -func (f *cukefmt) Skipped(step *gherkin.Step) { - f.basefmt.Skipped(step) +func (f *cukefmt) Skipped(step *gherkin.Step, match *StepDef) { + f.basefmt.Skipped(step, match) f.step(f.skipped[len(f.skipped)-1]) // no duration reported for skipped. f.curStep.Result.Duration = nil } -func (f *cukefmt) Undefined(step *gherkin.Step) { - f.basefmt.Undefined(step) +func (f *cukefmt) Undefined(step *gherkin.Step, match *StepDef) { + f.basefmt.Undefined(step, match) f.stat = undefined f.step(f.undefined[len(f.undefined)-1]) diff --git a/fmt_events.go b/fmt_events.go index 275b021..17e92ff 100644 --- a/fmt_events.go +++ b/fmt_events.go @@ -245,13 +245,13 @@ func (f *events) Passed(step *gherkin.Step, match *StepDef) { f.step(f.passed[len(f.passed)-1]) } -func (f *events) Skipped(step *gherkin.Step) { - f.basefmt.Skipped(step) +func (f *events) Skipped(step *gherkin.Step, match *StepDef) { + f.basefmt.Skipped(step, match) f.step(f.skipped[len(f.skipped)-1]) } -func (f *events) Undefined(step *gherkin.Step) { - f.basefmt.Undefined(step) +func (f *events) Undefined(step *gherkin.Step, match *StepDef) { + f.basefmt.Undefined(step, match) f.stat = undefined f.step(f.undefined[len(f.undefined)-1]) } diff --git a/fmt_junit.go b/fmt_junit.go index de488f6..8762cbe 100644 --- a/fmt_junit.go +++ b/fmt_junit.go @@ -101,7 +101,7 @@ func (j *junitFormatter) Passed(step *gherkin.Step, match *StepDef) { tcase.Status = "passed" } -func (j *junitFormatter) Skipped(step *gherkin.Step) { +func (j *junitFormatter) Skipped(step *gherkin.Step, match *StepDef) { suite := j.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() tcase := suite.current() if tcase.Status != "undefined" { diff --git a/fmt_pretty.go b/fmt_pretty.go index f6788c6..7b713df 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -334,13 +334,13 @@ func (f *pretty) Passed(step *gherkin.Step, match *StepDef) { f.printStepKind(f.passed[len(f.passed)-1]) } -func (f *pretty) Skipped(step *gherkin.Step) { - f.basefmt.Skipped(step) +func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) { + f.basefmt.Skipped(step, match) f.printStepKind(f.skipped[len(f.skipped)-1]) } -func (f *pretty) Undefined(step *gherkin.Step) { - f.basefmt.Undefined(step) +func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) { + f.basefmt.Undefined(step, match) f.printStepKind(f.undefined[len(f.undefined)-1]) } diff --git a/fmt_progress.go b/fmt_progress.go index 6e1cc1a..7bb8650 100644 --- a/fmt_progress.go +++ b/fmt_progress.go @@ -91,17 +91,17 @@ func (f *progress) Passed(step *gherkin.Step, match *StepDef) { 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() defer f.Unlock() - f.basefmt.Skipped(step) + f.basefmt.Skipped(step, match) 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() defer f.Unlock() - f.basefmt.Undefined(step) + f.basefmt.Undefined(step, match) f.step(f.undefined[len(f.undefined)-1]) } diff --git a/fmt_progress_test.go b/fmt_progress_test.go index eb32f48..d722fee 100644 --- a/fmt_progress_test.go +++ b/fmt_progress_test.go @@ -34,7 +34,6 @@ func TestProgressFormatterOutput(t *testing.T) { }, } - // var zeroDuration time.Duration expected := ` ...F-.P-.UU.....F..P..U 23 @@ -88,3 +87,177 @@ func trimAllLines(s string) string { } 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) + } +} diff --git a/godog.go b/godog.go index a37f679..80d8fd0 100644 --- a/godog.go +++ b/godog.go @@ -42,4 +42,4 @@ Godog was inspired by Behat and Cucumber the above description is taken from it' package godog // Version of package - based on Semantic Versioning 2.0.0 http://semver.org/ -const Version = "v0.6.3" +const Version = "v0.7.0" diff --git a/run.go b/run.go index 965906b..9e934b3 100644 --- a/run.go +++ b/run.go @@ -54,7 +54,7 @@ func (r *runner) concurrent(rate int) (failed bool) { return } -func (r *runner) run() (failed bool) { +func (r *runner) run() bool { suite := &Suite{ fmt: r.fmt, randomSeed: r.randomSeed, @@ -160,13 +160,11 @@ func Run(suite string, contextInitializer func(suite *Suite)) int { func supportsConcurrency(format string) bool { switch format { case "events": - return false case "junit": - return false case "pretty": - return false case "cucumber": - return false + default: + return true // supports concurrency } return true // all custom formatters are treated as supporting concurrency diff --git a/run_test.go b/run_test.go index 06d898c..6910d2c 100644 --- a/run_test.go +++ b/run_test.go @@ -2,10 +2,13 @@ package godog import ( "bytes" + "fmt" + "io/ioutil" "strings" "testing" "github.com/DATA-DOG/godog/colors" + "github.com/DATA-DOG/godog/gherkin" ) func okStep() error { @@ -50,3 +53,43 @@ func TestPrintsNoStepDefinitionsIfNoneFound(t *testing.T) { 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") + } +} diff --git a/stepdef.go b/stepdef.go index 4ba849d..1241b55 100644 --- a/stepdef.go +++ b/stepdef.go @@ -14,6 +14,22 @@ import ( 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 // contains a StepHandler and regexp which // is used to match a step. Args which @@ -27,6 +43,10 @@ type StepDef struct { hv reflect.Value Expr *regexp.Regexp Handler interface{} + + // multistep related + nested bool + undefined []string } func (sd *StepDef) definitionID() string { @@ -53,7 +73,7 @@ func (sd *StepDef) definitionID() string { // run a step with the matched arguments using // reflect -func (sd *StepDef) run() error { +func (sd *StepDef) run() interface{} { typ := sd.hv.Type() 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)) @@ -171,12 +191,7 @@ func (sd *StepDef) run() error { return fmt.Errorf("the argument %d type %s is not supported", i, param.Kind()) } } - ret := sd.hv.Call(values)[0].Interface() - if nil == ret { - return nil - } - - return ret.(error) + return sd.hv.Call(values)[0].Interface() } func (sd *StepDef) shouldBeString(idx int) (string, error) { diff --git a/suite.go b/suite.go index 19a70ae..74ed3f6 100644 --- a/suite.go +++ b/suite.go @@ -95,17 +95,33 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { 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())) + 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())) - } - s.steps = append(s.steps, &StepDef{ + + def := &StepDef{ Handler: stepFunc, Expr: regex, 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 @@ -184,41 +200,18 @@ func (s *Suite) run() { } 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 - } + def := s.matchStepText(step.Text) + if def != nil && step.Argument != nil { + def.args = append(def.args, step.Argument) } - // @TODO can handle ambiguous - return nil + return def } func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { match := s.matchStep(step) 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() { if e := recover(); e != nil { err = &traceError{ @@ -226,6 +219,15 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { stack: callStack(), } } + + if prevStepErr != nil { + return + } + + if err == ErrUndefined { + return + } + switch err { case nil: 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 } +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) { err = prevErr 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) { for _, step := range steps { - s.fmt.Skipped(step) + s.fmt.Skipped(step, s.matchStep(step)) } } diff --git a/suite_test.go b/suite_test.go index d27174c..29db82f 100644 --- a/suite_test.go +++ b/suite_test.go @@ -20,21 +20,28 @@ func TestMain(m *testing.M) { format := "progress" // non verbose mode concurrency := 4 + var specific bool for _, arg := range os.Args[1:] { if arg == "-test.v=true" { // go test transforms -v option - verbose mode format = "pretty" concurrency = 1 break } + if strings.Index(arg, "-test.run") == 0 { + specific = true + } + } + var status int + if !specific { + status = RunWithOptions("godog", func(s *Suite) { + SuiteContext(s) + }, Options{ + Format: format, // pretty format for verbose mode, otherwise - progress + Paths: []string{"features"}, + Concurrency: concurrency, // concurrency for verbose mode is 1 + Randomize: time.Now().UnixNano(), // randomize scenario execution order + }) } - status := RunWithOptions("godog", func(s *Suite) { - SuiteContext(s) - }, Options{ - Format: format, // pretty format for verbose mode, otherwise - progress - Paths: []string{"features"}, - Concurrency: concurrency, // concurrency for verbose mode is 1 - Randomize: time.Now().UnixNano(), // randomize scenario execution order - }) if st := m.Run(); st > status { status = st @@ -89,6 +96,21 @@ func SuiteContext(s *Suite) { // Introduced to test formatter/cucumber.feature 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 {