From ba80ebcd332b23ef55a8515f7a6176045f89fe59 Mon Sep 17 00:00:00 2001 From: gedi Date: Wed, 26 Apr 2017 20:33:13 +0300 Subject: [PATCH 1/5] implements multistep - initial attempt --- stepdef.go | 9 ++------ suite.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/stepdef.go b/stepdef.go index 4ba849d..8f9e3ab 100644 --- a/stepdef.go +++ b/stepdef.go @@ -53,7 +53,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 +171,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..8066856 100644 --- a/suite.go +++ b/suite.go @@ -95,12 +95,25 @@ 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())) + + 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())) + } + default: + panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind())) } + s.steps = append(s.steps, &StepDef{ Handler: stepFunc, Expr: regex, @@ -241,10 +254,51 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { } }() - err = match.run() + err = s.maybeSubSteps(match.run()) return } +func (s *Suite) maybeSubSteps(result interface{}) error { + if nil == result { + return nil + } + + if err, ok := result.(error); ok { + return err + } + + steps, ok := result.([]string) + if !ok { + return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) + } + + for _, step := range steps { + var def *StepDef + for _, h := range s.steps { + if m := h.Expr.FindStringSubmatch(step); len(m) > 0 { + var args []interface{} + for _, m := range m[1:] { + args = append(args, m) + } + h.args = args + def = h + break + } + } + + if def == nil { + return ErrUndefined + } + + // @TODO: step hooks only take gherkin.Step + // @TODO: cannot call formatter to register step execution either + if err := s.maybeSubSteps(def.run()); err != nil { + return err + } + } + return nil +} + func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { err = prevErr for _, step := range steps { From 703b40de76ce6dfd0c25588071aeada1ae22698b Mon Sep 17 00:00:00 2001 From: gedi Date: Thu, 27 Apr 2017 23:48:54 +0300 Subject: [PATCH 2/5] adds scenarios to test nested steps --- features/lang.feature | 3 +- features/load.feature | 3 +- features/multistep.feature | 140 +++++++++++++++++++++++++++++++++++++ run.go | 6 +- stepdef.go | 17 +++++ suite.go | 104 +++++++++++++++------------ suite_test.go | 15 ++++ 7 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 features/multistep.feature 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/run.go b/run.go index 965906b..1167744 100644 --- a/run.go +++ b/run.go @@ -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/stepdef.go b/stepdef.go index 8f9e3ab..6233a17 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,7 @@ type StepDef struct { hv reflect.Value Expr *regexp.Regexp Handler interface{} + nested bool } func (sd *StepDef) definitionID() string { diff --git a/suite.go b/suite.go index 8066856..a1d7b86 100644 --- a/suite.go +++ b/suite.go @@ -100,6 +100,12 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return only one value, but it has: %d", typ.NumOut())) } + def := &StepDef{ + Handler: stepFunc, + Expr: regex, + hv: v, + } + typ = typ.Out(0) switch typ.Kind() { case reflect.Interface: @@ -107,6 +113,7 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) } case reflect.Slice: + def.nested = true if typ.Elem().Kind() != reflect.String { panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Kind())) } @@ -114,11 +121,7 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind())) } - s.steps = append(s.steps, &StepDef{ - Handler: stepFunc, - Expr: regex, - hv: v, - }) + s.steps = append(s.steps, def) } // BeforeSuite registers a function or method @@ -197,27 +200,20 @@ 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 { + + // @TODO custom undefined err here to pass step text for snippet + // @TODO user multistep definitions may panic + if s.maybeUndefined(match) { s.fmt.Undefined(step) return ErrUndefined } @@ -227,11 +223,6 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { return nil } - // run before step handlers - for _, f := range s.beforeStepHandlers { - f(step) - } - defer func() { if e := recover(); e != nil { err = &traceError{ @@ -254,10 +245,32 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { } }() + // run before step handlers + for _, f := range s.beforeStepHandlers { + f(step) + } + err = s.maybeSubSteps(match.run()) return } +func (s *Suite) maybeUndefined(step *StepDef) bool { + if nil == step { + return true + } + + if !step.nested { + return false + } + + for _, text := range step.run().(Steps) { + if s.maybeUndefined(s.matchStepText(text)) { + return true + } + } + return false +} + func (s *Suite) maybeSubSteps(result interface{}) error { if nil == result { return nil @@ -267,38 +280,37 @@ func (s *Suite) maybeSubSteps(result interface{}) error { return err } - steps, ok := result.([]string) + steps, ok := result.(Steps) if !ok { return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) } - for _, step := range steps { - var def *StepDef - for _, h := range s.steps { - if m := h.Expr.FindStringSubmatch(step); len(m) > 0 { - var args []interface{} - for _, m := range m[1:] { - args = append(args, m) - } - h.args = args - def = h - break - } - } - - if def == nil { + for _, text := range steps { + if def := s.matchStepText(text); def == nil { return ErrUndefined - } - - // @TODO: step hooks only take gherkin.Step - // @TODO: cannot call formatter to register step execution either - if err := s.maybeSubSteps(def.run()); err != nil { + } 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) + } + + // @TODO copy step def + h.args = args + return h + } + } + return nil +} + func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { err = prevErr for _, step := range steps { diff --git a/suite_test.go b/suite_test.go index d27174c..90601f3 100644 --- a/suite_test.go +++ b/suite_test.go @@ -89,6 +89,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 { From 5bfd57218aec0d4102f83c4b41fa64674014fe46 Mon Sep 17 00:00:00 2001 From: gedi Date: Sat, 29 Apr 2017 00:13:14 +0300 Subject: [PATCH 3/5] adds more tests for multistep execution --- features/formatter/events.feature | 4 +- fmt_progress_test.go | 111 +++++++++++++++++++++++++++++- run.go | 2 +- suite.go | 45 ++++++++---- suite_test.go | 23 ++++--- 5 files changed, 158 insertions(+), 27 deletions(-) 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/fmt_progress_test.go b/fmt_progress_test.go index eb32f48..114e913 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,113 @@ func trimAllLines(s string) string { } return strings.Join(lines, "\n") } + +var basicGherkinFeature = ` +Feature: basic + + Scenario: passing scenario + When step1 + Then step2 +` + +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(`^step1$`, func() error { return nil }) + s.Step(`^step2$`, 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", "step1"} }) + s.Step(`^step1$`, func() error { return nil }) + s.Step(`^step2$`, 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", "step1"} }) + s.Step(`^step1$`, func() error { return nil }) + s.Step(`^step2$`, 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", "step1"} }) + s.Step(`^step1$`, func() error { return nil }) + s.Step(`^step2$`, func() []string { return []string{"sub1", "sub2"} }) + }, + } + + if !r.run() { + t.Fatal("the suite should have failed") + } +} diff --git a/run.go b/run.go index 1167744..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, diff --git a/suite.go b/suite.go index a1d7b86..0182650 100644 --- a/suite.go +++ b/suite.go @@ -211,18 +211,7 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { match := s.matchStep(step) s.fmt.Defined(step, match) - // @TODO custom undefined err here to pass step text for snippet - // @TODO user multistep definitions may panic - if s.maybeUndefined(match) { - s.fmt.Undefined(step) - return ErrUndefined - } - - if prevStepErr != nil { - s.fmt.Skipped(step) - return nil - } - + // user multistep definitions may panic defer func() { if e := recover(); e != nil { err = &traceError{ @@ -230,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) @@ -245,6 +243,17 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { } }() + // @TODO custom undefined err here to pass step text for snippet + if s.maybeUndefined(match) { + 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) @@ -303,9 +312,15 @@ func (s *Suite) matchStepText(text string) *StepDef { args = append(args, m) } - // @TODO copy step def - h.args = args - return h + // 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 diff --git a/suite_test.go b/suite_test.go index 90601f3..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 From e4ed3b9a9c89e36b1656271b5e6c36abf5574ce5 Mon Sep 17 00:00:00 2001 From: gedi Date: Sat, 29 Apr 2017 22:44:40 +0300 Subject: [PATCH 4/5] handles undefined step templates for nested steps --- fmt.go | 74 ++++++++++++++++++++---------------- fmt_cucumber.go | 8 ++-- fmt_events.go | 8 ++-- fmt_junit.go | 4 +- fmt_pretty.go | 8 ++-- fmt_progress.go | 8 ++-- fmt_progress_test.go | 90 +++++++++++++++++++++++++++++++++++++------- run_test.go | 43 +++++++++++++++++++++ stepdef.go | 5 ++- suite.go | 37 +++++++++++------- 10 files changed, 207 insertions(+), 78 deletions(-) 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 114e913..d722fee 100644 --- a/fmt_progress_test.go +++ b/fmt_progress_test.go @@ -92,8 +92,8 @@ var basicGherkinFeature = ` Feature: basic Scenario: passing scenario - When step1 - Then step2 + When one + Then two ` func TestProgressFormatterWhenStepPanics(t *testing.T) { @@ -108,8 +108,8 @@ func TestProgressFormatterWhenStepPanics(t *testing.T) { fmt: progressFunc("progress", w), features: []*feature{&feature{Feature: feat}}, initializer: func(s *Suite) { - s.Step(`^step1$`, func() error { return nil }) - s.Step(`^step2$`, func() error { panic("omg") }) + s.Step(`^one$`, func() error { return nil }) + s.Step(`^two$`, func() error { panic("omg") }) }, } @@ -137,9 +137,9 @@ func TestProgressFormatterWithPassingMultisteps(t *testing.T) { 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", "step1"} }) - s.Step(`^step1$`, func() error { return nil }) - s.Step(`^step2$`, func() Steps { return Steps{"sub1", "sub2"} }) + 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"} }) }, } @@ -162,9 +162,9 @@ func TestProgressFormatterWithFailingMultisteps(t *testing.T) { 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", "step1"} }) - s.Step(`^step1$`, func() error { return nil }) - s.Step(`^step2$`, func() Steps { return Steps{"sub1", "sub2"} }) + 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"} }) }, } @@ -187,9 +187,9 @@ func TestProgressFormatterWithPanicInMultistep(t *testing.T) { 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", "step1"} }) - s.Step(`^step1$`, func() error { return nil }) - s.Step(`^step2$`, func() []string { return []string{"sub1", "sub2"} }) + 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"} }) }, } @@ -197,3 +197,67 @@ func TestProgressFormatterWithPanicInMultistep(t *testing.T) { 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/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 6233a17..1241b55 100644 --- a/stepdef.go +++ b/stepdef.go @@ -43,7 +43,10 @@ type StepDef struct { hv reflect.Value Expr *regexp.Regexp Handler interface{} - nested bool + + // multistep related + nested bool + undefined []string } func (sd *StepDef) definitionID() string { diff --git a/suite.go b/suite.go index 0182650..74ed3f6 100644 --- a/suite.go +++ b/suite.go @@ -113,10 +113,10 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) } case reflect.Slice: - def.nested = true 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())) } @@ -243,14 +243,23 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { } }() - // @TODO custom undefined err here to pass step text for snippet - if s.maybeUndefined(match) { - s.fmt.Undefined(step) + 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) + s.fmt.Skipped(step, match) return nil } @@ -263,21 +272,21 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { return } -func (s *Suite) maybeUndefined(step *StepDef) bool { +func (s *Suite) maybeUndefined(text string) (undefined []string) { + step := s.matchStepText(text) if nil == step { - return true + undefined = append(undefined, text) + return } if !step.nested { - return false + return } - for _, text := range step.run().(Steps) { - if s.maybeUndefined(s.matchStepText(text)) { - return true - } + for _, next := range step.run().(Steps) { + undefined = append(undefined, s.maybeUndefined(next)...) } - return false + return } func (s *Suite) maybeSubSteps(result interface{}) error { @@ -345,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)) } } From f09230a6fd4e614d00299300f53aad59f082b4d0 Mon Sep 17 00:00:00 2001 From: gedi Date: Sat, 29 Apr 2017 23:04:48 +0300 Subject: [PATCH 5/5] updates readme and changelog --- CHANGELOG.md | 6 ++++++ README.md | 7 +++++-- examples/api/version.feature | 2 +- godog.go | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) 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/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"