diff --git a/.travis.yml b/.travis.yml index e0af103..a9066b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ script: # pull all external dependencies # remove them at all if possible + - go get github.com/cucumber/gherkin-go - go get golang.org/x/tools/imports - go get github.com/shiena/ansicolor diff --git a/Makefile b/Makefile index 7b5768a..41605e9 100644 --- a/Makefile +++ b/Makefile @@ -10,5 +10,6 @@ test: # updates dependencies deps: + go get -u github.com/cucumber/gherkin-go go get -u golang.org/x/tools/imports go get -u github.com/shiena/ansicolor diff --git a/README.md b/README.md index 541fe08..7f9ca8e 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Now when you run the `godog godog.feature` again, you should see: ### Documentation -See [godoc][godoc] and [gherkin godoc][godoc_gherkin] for general API details. +See [godoc][godoc] for general API details. See **.travis.yml** for supported **go** versions. The public API is stable enough, but it may break until **1.0.0** version, see `godog --version`. @@ -137,7 +137,6 @@ All package dependencies are **MIT** or **BSD** licensed. **Godog** is licensed under the [three clause BSD license][license] [godoc]: http://godoc.org/github.com/DATA-DOG/godog "Documentation on godoc" -[godoc_gherkin]: http://godoc.org/github.com/DATA-DOG/godog/gherkin "Documentation on godoc for gherkin" [golang]: https://golang.org/ "GO programming language" [behat]: http://docs.behat.org/ "Behavior driven development framework for PHP" [cucumber]: https://cucumber.io/ "Behavior driven development framework for Ruby" diff --git a/arguments.go b/arguments.go index 1f7b18f..024b9ab 100644 --- a/arguments.go +++ b/arguments.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/DATA-DOG/godog/gherkin" + "github.com/cucumber/gherkin-go" ) // Arg is an argument for StepHandler parsed from @@ -122,17 +122,17 @@ func (a *Arg) Bytes() []byte { return []byte(s) } -// PyString converts an argument to *gherkin.PyString node -func (a *Arg) PyString() *gherkin.PyString { - s, ok := a.value.(*gherkin.PyString) - a.must(ok, "*gherkin.PyString") +// DocString converts an argument to *gherkin.DocString node +func (a *Arg) DocString() *gherkin.DocString { + s, ok := a.value.(*gherkin.DocString) + a.must(ok, "*gherkin.DocString") return s } -// Table converts an argument to *gherkin.Table node -func (a *Arg) Table() *gherkin.Table { - s, ok := a.value.(*gherkin.Table) - a.must(ok, "*gherkin.Table") +// DataTable converts an argument to *gherkin.DataTable node +func (a *Arg) DataTable() *gherkin.DataTable { + s, ok := a.value.(*gherkin.DataTable) + a.must(ok, "*gherkin.DataTable") return s } diff --git a/example/ls_test.go b/example/ls_test.go index f38ae8f..55ab370 100644 --- a/example/ls_test.go +++ b/example/ls_test.go @@ -46,7 +46,7 @@ func (f *lsFeature) iHaveFileOrDirectoryNamed(args ...*godog.Arg) (err error) { } func (f *lsFeature) iShouldGetOutput(args ...*godog.Arg) error { - expected := args[0].PyString().Lines + expected := strings.Split(args[0].DocString().Content, "\n") actual := strings.Split(strings.TrimSpace(f.buf.String()), "\n") if len(expected) != len(actual) { return fmt.Errorf("number of expected output lines %d, does not match actual: %d", len(expected), len(actual)) diff --git a/features/lang.feature b/features/lang.feature new file mode 100644 index 0000000..ccd3745 --- /dev/null +++ b/features/lang.feature @@ -0,0 +1,17 @@ +# language: lt +@lang +Savybė: užkrauti savybes + Kad būtų galima paleisti savybių testus + Kaip testavimo įrankis + Aš turiu galėti užregistruoti savybes + + Scenarijus: savybių užkrovimas iš aplanko + Duota savybių aplankas "features" + Kai aš išskaitau savybes + Tada aš turėčiau turėti 4 savybių failus: + """ + features/events.feature + features/lang.feature + features/load.feature + features/run.feature + """ diff --git a/features/load.feature b/features/load.feature index a0feef2..2f120e8 100644 --- a/features/load.feature +++ b/features/load.feature @@ -6,9 +6,10 @@ Feature: load features Scenario: load features within path Given a feature path "features" When I parse features - Then I should have 3 feature files: + Then I should have 4 feature files: """ features/events.feature + features/lang.feature features/load.feature features/run.feature """ diff --git a/features/run.feature b/features/run.feature index a10a7c0..2abf5d0 100644 --- a/features/run.feature +++ b/features/run.feature @@ -81,7 +81,7 @@ Feature: run features Then I should have 1 scenario registered """ When I run feature suite - Then the suite should have passed # we do not treat undefined scenarios as fails + Then the suite should have passed And the following step should be passed: """ a feature path "features/load.feature:6" diff --git a/fmt.go b/fmt.go index 4b58b5a..f964a36 100644 --- a/fmt.go +++ b/fmt.go @@ -3,7 +3,7 @@ package godog import ( "fmt" - "github.com/DATA-DOG/godog/gherkin" + "github.com/cucumber/gherkin-go" ) type registeredFormatter struct { @@ -33,6 +33,7 @@ func RegisterFormatter(name, description string, f Formatter) { // formatters needs to be registered with a // RegisterFormatter function call type Formatter interface { + Feature(*gherkin.Feature, string) Node(interface{}) Failed(*gherkin.Step, *StepDef, error) Passed(*gherkin.Step, *StepDef) @@ -44,39 +45,38 @@ type Formatter interface { // failed represents a failed step data structure // with all necessary references type failed struct { - step *gherkin.Step - def *StepDef - err error + feature *feature + owner interface{} + step *gherkin.Step + def *StepDef + err error } func (f failed) line() string { - var tok *gherkin.Token - var ft *gherkin.Feature - if f.step.Scenario != nil { - tok = f.step.Scenario.Token - ft = f.step.Scenario.Feature - } else { - tok = f.step.Background.Token - ft = f.step.Background.Feature - } - return fmt.Sprintf("%s:%d", ft.Path, tok.Line) + return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line) } // passed represents a successful step data structure // with all necessary references type passed struct { - step *gherkin.Step - def *StepDef + feature *feature + owner interface{} + step *gherkin.Step + def *StepDef } // skipped represents a skipped step data structure // with all necessary references type skipped struct { - step *gherkin.Step + feature *feature + owner interface{} + step *gherkin.Step } // undefined represents a pending step data structure // with all necessary references type undefined struct { - step *gherkin.Step + feature *feature + owner interface{} + step *gherkin.Step } diff --git a/fmt_pretty.go b/fmt_pretty.go index 59791b4..aeeadc3 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -9,12 +9,13 @@ import ( "strings" "time" - "github.com/DATA-DOG/godog/gherkin" + "github.com/cucumber/gherkin-go" ) func init() { RegisterFormatter("pretty", "Prints every feature with runtime statuses.", &pretty{ started: time.Now(), + indent: 2, }) } @@ -22,18 +23,20 @@ var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>") // a built in default pretty formatter type pretty struct { - feature *gherkin.Feature + scope interface{} + indent int commentPos int backgroundSteps int // outline - outlineExamples int - outlineNumSteps int - outlineSteps []interface{} + outline *gherkin.ScenarioOutline + outlineSteps []interface{} + outlineNumExample int + outlineNumExamples int // summary started time.Time - features []*gherkin.Feature + features []*feature failed []*failed passed []*passed skipped []*skipped @@ -41,44 +44,65 @@ type pretty struct { } // a line number representation in feature file -func (f *pretty) line(tok *gherkin.Token) string { - return cl(fmt.Sprintf("# %s:%d", f.feature.Path, tok.Line), black) +func (f *pretty) line(loc *gherkin.Location) string { + return cl(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line), black) +} + +func (f *pretty) length(node interface{}) int { + switch t := node.(type) { + case *gherkin.Background: + return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) + case *gherkin.Step: + return f.indent*2 + len(strings.TrimSpace(t.Keyword)+" "+t.Text) + case *gherkin.Scenario: + return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) + case *gherkin.ScenarioOutline: + return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) + } + panic(fmt.Sprintf("unexpected node %T to determine length", node)) +} + +func (f *pretty) Feature(ft *gherkin.Feature, p string) { + if len(f.features) != 0 { + // not a first feature, add a newline + fmt.Println("") + } + f.features = append(f.features, &feature{Path: p, Feature: ft}) + fmt.Println(bcl(ft.Keyword+": ", white) + ft.Name) + if strings.TrimSpace(ft.Description) != "" { + for _, line := range strings.Split(ft.Description, "\n") { + fmt.Println(s(f.indent) + strings.TrimSpace(line)) + } + } + if ft.Background != nil { + f.commentPos = f.longestStep(ft.Background.Steps, f.length(ft.Background)) + f.backgroundSteps = len(ft.Background.Steps) + fmt.Println("\n" + s(f.indent) + bcl(ft.Background.Keyword+": "+ft.Background.Name, white)) + } } // Node takes a gherkin node for formatting func (f *pretty) Node(node interface{}) { switch t := node.(type) { - case *gherkin.Feature: - if f.feature != nil { - // not a first feature, add a newline - fmt.Println("") - } - f.feature = t - f.features = append(f.features, t) - // print feature header - fmt.Println(bcl(t.Token.Keyword+": ", white) + t.Title) - fmt.Println(t.Description) - // print background header - if t.Background != nil { - f.commentPos = longestStep(t.Background.Steps, t.Background.Token.Length()) - f.backgroundSteps = len(t.Background.Steps) - fmt.Println("\n" + s(t.Background.Token.Indent) + bcl(t.Background.Token.Keyword+":", white)) - } + case *gherkin.Examples: + f.outlineNumExamples = len(t.TableBody) + f.outlineNumExample++ + case *gherkin.Background: + f.scope = t case *gherkin.Scenario: - f.commentPos = longestStep(t.Steps, t.Token.Length()) - if t.Outline != nil { - f.outlineSteps = []interface{}{} // reset steps list - f.commentPos = longestStep(t.Outline.Steps, t.Token.Length()) - if f.outlineExamples == 0 { - f.outlineNumSteps = len(t.Outline.Steps) - f.outlineExamples = len(t.Outline.Examples.Rows) - 1 - } else { - return // already printed an outline - } - } - text := s(t.Token.Indent) + bcl(t.Token.Keyword+": ", white) + t.Title - text += s(f.commentPos-t.Token.Length()+1) + f.line(t.Token) + f.scope = t + f.commentPos = f.longestStep(t.Steps, f.length(t)) + text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name + text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) fmt.Println("\n" + text) + case *gherkin.ScenarioOutline: + f.scope = t + f.outline = t + f.commentPos = f.longestStep(t.Steps, f.length(t)) + text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name + text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) + fmt.Println("\n" + text) + f.outlineNumExample = -1 } } @@ -87,7 +111,10 @@ func (f *pretty) Summary() { // failed steps on background are not scenarios var failedScenarios []*failed for _, fail := range f.failed { - if fail.step.Scenario != nil { + switch fail.owner.(type) { + case *gherkin.Scenario: + failedScenarios = append(failedScenarios, fail) + case *gherkin.ScenarioOutline: failedScenarios = append(failedScenarios, fail) } } @@ -113,7 +140,16 @@ func (f *pretty) Summary() { } var total, passed int for _, ft := range f.features { - total += len(ft.Scenarios) + for _, def := range ft.ScenarioDefinitions { + switch t := def.(type) { + case *gherkin.Scenario: + total++ + case *gherkin.ScenarioOutline: + for _, ex := range t.Examples { + total += len(ex.TableBody) + } + } + } } passed = total @@ -156,18 +192,17 @@ func (f *pretty) Summary() { fmt.Println(elapsed) } -func (f *pretty) printOutlineExample(scenario *gherkin.Scenario) { +func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) { var failed error clr := green - tbl := scenario.Outline.Examples - firstExample := f.outlineExamples == len(tbl.Rows)-1 + example := outline.Examples[f.outlineNumExample] + firstExample := f.outlineNumExamples == len(example.TableBody) + printSteps := firstExample && f.outlineNumExample == 0 + + // var replace make(map[]) for i, act := range f.outlineSteps { - var c color - var def *StepDef - var err error - - _, def, c, err = f.stepDetails(act) + _, _, def, c, err := f.stepDetails(act) // determine example row status switch { case err != nil: @@ -178,10 +213,10 @@ func (f *pretty) printOutlineExample(scenario *gherkin.Scenario) { case c == cyan && clr == green: clr = cyan } - if firstExample { + if printSteps { // in first example, we need to print steps var text string - ostep := scenario.Outline.Steps[i] + ostep := outline.Steps[i] if def != nil { if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 { var pos int @@ -197,45 +232,43 @@ func (f *pretty) printOutlineExample(scenario *gherkin.Scenario) { } // use reflect to get step handler function name name := runtime.FuncForPC(reflect.ValueOf(def.Handler).Pointer()).Name() - text += s(f.commentPos-ostep.Token.Length()+1) + cl(fmt.Sprintf("# %s", name), black) + text += s(f.commentPos-f.length(ostep)+1) + cl(fmt.Sprintf("# %s", name), black) } else { text = cl(ostep.Text, cyan) } // print the step outline - fmt.Println(s(ostep.Token.Indent) + cl(ostep.Token.Keyword, cyan) + " " + text) + fmt.Println(s(f.indent*2) + cl(strings.TrimSpace(ostep.Keyword), cyan) + " " + text) } } - cols := make([]string, len(tbl.Rows[0])) - max := longest(tbl) + cells := make([]string, len(example.TableHeader.Cells)) + max := longest(example) // an example table header if firstExample { - out := scenario.Outline fmt.Println("") - fmt.Println(s(out.Token.Indent) + bcl(out.Token.Keyword+":", white)) - row := tbl.Rows[0] + fmt.Println(s(f.indent*2) + bcl(example.Keyword+": ", white) + example.Name) - for i, col := range row { - cols[i] = cl(col, cyan) + s(max[i]-len(col)) + for i, cell := range example.TableHeader.Cells { + cells[i] = cl(cell.Value, cyan) + s(max[i]-len(cell.Value)) } - fmt.Println(s(tbl.Token.Indent) + "| " + strings.Join(cols, " | ") + " |") + fmt.Println(s(f.indent*3) + "| " + strings.Join(cells, " | ") + " |") } // an example table row - row := tbl.Rows[len(tbl.Rows)-f.outlineExamples] - for i, col := range row { - cols[i] = cl(col, clr) + s(max[i]-len(col)) + row := example.TableBody[len(example.TableBody)-f.outlineNumExamples] + for i, cell := range row.Cells { + cells[i] = cl(cell.Value, clr) + s(max[i]-len(cell.Value)) } - fmt.Println(s(tbl.Token.Indent) + "| " + strings.Join(cols, " | ") + " |") + fmt.Println(s(f.indent*3) + "| " + strings.Join(cells, " | ") + " |") // if there is an error if failed != nil { - fmt.Println(s(tbl.Token.Indent) + bcl(failed, red)) + fmt.Println(s(f.indent*3) + bcl(failed, red)) } } func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) { - text := s(step.Token.Indent) + cl(step.Token.Keyword, c) + " " + text := s(f.indent*2) + cl(strings.TrimSpace(step.Keyword), c) + " " switch { case def != nil: if m := (def.Expr.FindStringSubmatchIndex(step.Text))[2:]; len(m) > 0 { @@ -254,38 +287,44 @@ func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) { } // use reflect to get step handler function name name := runtime.FuncForPC(reflect.ValueOf(def.Handler).Pointer()).Name() - text += s(f.commentPos-step.Token.Length()+1) + cl(fmt.Sprintf("# %s", name), black) + text += s(f.commentPos-f.length(step)+1) + cl(fmt.Sprintf("# %s", name), black) default: text += cl(step.Text, c) } fmt.Println(text) - if step.PyString != nil { - fmt.Println(s(step.Token.Indent+2) + cl(`"""`, c)) - fmt.Println(cl(step.PyString.Raw, c)) - fmt.Println(s(step.Token.Indent+2) + cl(`"""`, c)) - } - if step.Table != nil { - f.printTable(step.Table, c) + switch t := step.Argument.(type) { + case *gherkin.DataTable: + f.printTable(t, c) + case *gherkin.DocString: + fmt.Println(s(f.indent*3) + cl(t.Delimitter, c)) // @TODO: content type + for _, ln := range strings.Split(t.Content, "\n") { + fmt.Println(s(f.indent*3) + cl(ln, c)) + } + fmt.Println(s(f.indent*3) + cl(t.Delimitter, c)) } } -func (f *pretty) stepDetails(stepAction interface{}) (step *gherkin.Step, def *StepDef, c color, err error) { +func (f *pretty) stepDetails(stepAction interface{}) (owner interface{}, step *gherkin.Step, def *StepDef, c color, err error) { switch typ := stepAction.(type) { case *passed: step = typ.step def = typ.def + owner = typ.owner c = green case *failed: step = typ.step def = typ.def + owner = typ.owner err = typ.err c = red case *skipped: step = typ.step + owner = typ.owner c = cyan case *undefined: step = typ.step + owner = typ.owner c = yellow default: fatal(fmt.Errorf("unexpected step type received: %T", typ)) @@ -294,94 +333,101 @@ func (f *pretty) stepDetails(stepAction interface{}) (step *gherkin.Step, def *S } func (f *pretty) printStepKind(stepAction interface{}) { - var c color - var step *gherkin.Step - var def *StepDef - var err error - - step, def, c, err = f.stepDetails(stepAction) + owner, step, def, c, err := f.stepDetails(stepAction) // do not print background more than once - switch { - case step.Background != nil && f.backgroundSteps == 0: - return - case step.Background != nil && f.backgroundSteps > 0: - f.backgroundSteps-- + if _, ok := owner.(*gherkin.Background); ok { + switch { + case f.backgroundSteps == 0: + return + case f.backgroundSteps > 0: + f.backgroundSteps-- + } } - if f.outlineExamples != 0 { + if outline, ok := owner.(*gherkin.ScenarioOutline); ok { f.outlineSteps = append(f.outlineSteps, stepAction) - if len(f.outlineSteps) == f.outlineNumSteps { + if len(f.outlineSteps) == len(outline.Steps) { // an outline example steps has went through - f.printOutlineExample(step.Scenario) - f.outlineExamples-- + f.printOutlineExample(outline) + f.outlineSteps = []interface{}{} + f.outlineNumExamples-- } - return // wait till example steps + return } f.printStep(step, def, c) if err != nil { - fmt.Println(s(step.Token.Indent) + bcl(err, red)) + fmt.Println(s(f.indent*2) + bcl(err, red)) } } // print table with aligned table cells -func (f *pretty) printTable(t *gherkin.Table, c color) { +func (f *pretty) printTable(t *gherkin.DataTable, c color) { var l = longest(t) - var cols = make([]string, len(t.Rows[0])) + var cols = make([]string, len(t.Rows[0].Cells)) for _, row := range t.Rows { - for i, col := range row { - cols[i] = col + s(l[i]-len(col)) + for i, cell := range row.Cells { + cols[i] = cell.Value + s(l[i]-len(cell.Value)) } - fmt.Println(s(t.Token.Indent) + cl("| "+strings.Join(cols, " | ")+" |", c)) + fmt.Println(s(f.indent*3) + cl("| "+strings.Join(cols, " | ")+" |", c)) } } // Passed is called to represent a passed step func (f *pretty) Passed(step *gherkin.Step, match *StepDef) { - s := &passed{step: step, def: match} + s := &passed{owner: f.scope, feature: f.features[len(f.features)-1], step: step, def: match} f.printStepKind(s) f.passed = append(f.passed, s) } // Skipped is called to represent a passed step func (f *pretty) Skipped(step *gherkin.Step) { - s := &skipped{step: step} + s := &skipped{owner: f.scope, feature: f.features[len(f.features)-1], step: step} f.printStepKind(s) f.skipped = append(f.skipped, s) } // Undefined is called to represent a pending step func (f *pretty) Undefined(step *gherkin.Step) { - s := &undefined{step: step} + s := &undefined{owner: f.scope, feature: f.features[len(f.features)-1], step: step} f.printStepKind(s) f.undefined = append(f.undefined, s) } // Failed is called to represent a failed step func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) { - s := &failed{step: step, def: match, err: err} + s := &failed{owner: f.scope, feature: f.features[len(f.features)-1], step: step, def: match, err: err} f.printStepKind(s) f.failed = append(f.failed, s) } // longest gives a list of longest columns of all rows in Table -func longest(t *gherkin.Table) []int { - var longest = make([]int, len(t.Rows[0])) - for _, row := range t.Rows { - for i, col := range row { - if longest[i] < len(col) { - longest[i] = len(col) +func longest(tbl interface{}) []int { + var rows []*gherkin.TableRow + switch t := tbl.(type) { + case *gherkin.Examples: + rows = append(rows, t.TableHeader) + rows = append(rows, t.TableBody...) + case *gherkin.DataTable: + rows = append(rows, t.Rows...) + } + + longest := make([]int, len(rows[0].Cells)) + for _, row := range rows { + for i, cell := range row.Cells { + if longest[i] < len(cell.Value) { + longest[i] = len(cell.Value) } } } return longest } -func longestStep(steps []*gherkin.Step, base int) int { +func (f *pretty) longestStep(steps []*gherkin.Step, base int) int { ret := base for _, step := range steps { - length := step.Token.Length() + length := f.length(step) if length > ret { ret = length } diff --git a/fmt_progress.go b/fmt_progress.go index bdd51da..cf748e3 100644 --- a/fmt_progress.go +++ b/fmt_progress.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/DATA-DOG/godog/gherkin" + "github.com/cucumber/gherkin-go" ) func init() { @@ -20,7 +20,9 @@ type progress struct { stepsPerRow int started time.Time steps int - features []*gherkin.Feature + + features []*feature + owner interface{} failed []*failed passed []*passed @@ -28,10 +30,18 @@ type progress struct { undefined []*undefined } -func (f *progress) Node(node interface{}) { - switch t := node.(type) { - case *gherkin.Feature: - f.features = append(f.features, t) +func (f *progress) Feature(ft *gherkin.Feature, p string) { + f.features = append(f.features, &feature{Path: p, Feature: ft}) +} + +func (f *progress) Node(n interface{}) { + switch t := n.(type) { + case *gherkin.ScenarioOutline: + f.owner = t + case *gherkin.Scenario: + f.owner = t + case *gherkin.Background: + f.owner = t } } @@ -49,13 +59,22 @@ func (f *progress) Summary() { if len(f.failed) > 0 { fmt.Println("\n--- " + cl("Failed steps:", red) + "\n") for _, fail := range f.failed { - fmt.Println(s(4) + cl(fail.step.Token.Keyword+" "+fail.step.Text, red) + cl(" # "+fail.line(), black)) + fmt.Println(s(4) + cl(fail.step.Keyword+" "+fail.step.Text, red) + cl(" # "+fail.line(), black)) fmt.Println(s(6) + cl("Error: ", red) + bcl(fail.err, red) + "\n") } } var total, passed int for _, ft := range f.features { - total += len(ft.Scenarios) + for _, def := range ft.ScenarioDefinitions { + switch t := def.(type) { + case *gherkin.Scenario: + total++ + case *gherkin.ScenarioOutline: + for _, ex := range t.Examples { + total += len(ex.TableBody) + } + } + } } passed = total @@ -116,25 +135,25 @@ func (f *progress) step(step interface{}) { } func (f *progress) Passed(step *gherkin.Step, match *StepDef) { - s := &passed{step: step, def: match} + s := &passed{owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match} f.passed = append(f.passed, s) f.step(s) } func (f *progress) Skipped(step *gherkin.Step) { - s := &skipped{step: step} + s := &skipped{owner: f.owner, feature: f.features[len(f.features)-1], step: step} f.skipped = append(f.skipped, s) f.step(s) } func (f *progress) Undefined(step *gherkin.Step) { - s := &undefined{step: step} + s := &undefined{owner: f.owner, feature: f.features[len(f.features)-1], step: step} f.undefined = append(f.undefined, s) f.step(s) } func (f *progress) Failed(step *gherkin.Step, match *StepDef, err error) { - s := &failed{step: step, def: match, err: err} + s := &failed{owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, err: err} f.failed = append(f.failed, s) f.step(s) } diff --git a/fmt_test.go b/fmt_test.go index 489e433..84921e7 100644 --- a/fmt_test.go +++ b/fmt_test.go @@ -1,10 +1,11 @@ package godog -import "github.com/DATA-DOG/godog/gherkin" +import "github.com/cucumber/gherkin-go" type testFormatter struct { - features []*gherkin.Feature - scenarios []*gherkin.Scenario + owner interface{} + features []*feature + scenarios []interface{} failed []*failed passed []*passed @@ -12,29 +13,37 @@ type testFormatter struct { undefined []*undefined } +func (f *testFormatter) Feature(ft *gherkin.Feature, p string) { + f.features = append(f.features, &feature{Path: p, Feature: ft}) +} + func (f *testFormatter) Node(node interface{}) { switch t := node.(type) { - case *gherkin.Feature: - f.features = append(f.features, t) case *gherkin.Scenario: f.scenarios = append(f.scenarios, t) + f.owner = t + case *gherkin.ScenarioOutline: + f.scenarios = append(f.scenarios, t) + f.owner = t + case *gherkin.Background: + f.owner = t } } func (f *testFormatter) Summary() {} func (f *testFormatter) Passed(step *gherkin.Step, match *StepDef) { - f.passed = append(f.passed, &passed{step: step, def: match}) + f.passed = append(f.passed, &passed{owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match}) } func (f *testFormatter) Skipped(step *gherkin.Step) { - f.skipped = append(f.skipped, &skipped{step: step}) + f.skipped = append(f.skipped, &skipped{owner: f.owner, feature: f.features[len(f.features)-1], step: step}) } func (f *testFormatter) Undefined(step *gherkin.Step) { - f.undefined = append(f.undefined, &undefined{step: step}) + f.undefined = append(f.undefined, &undefined{owner: f.owner, feature: f.features[len(f.features)-1], step: step}) } func (f *testFormatter) Failed(step *gherkin.Step, match *StepDef, err error) { - f.failed = append(f.failed, &failed{step: step, def: match, err: err}) + f.failed = append(f.failed, &failed{owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, err: err}) } diff --git a/gherkin/LICENSE b/gherkin/LICENSE deleted file mode 100644 index f767265..0000000 --- a/gherkin/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) - -Copyright (c) 2015, DataDog.lt team -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* The name DataDog.lt may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/gherkin/README.md b/gherkin/README.md deleted file mode 100644 index 89d40fa..0000000 --- a/gherkin/README.md +++ /dev/null @@ -1,50 +0,0 @@ -[![Build Status](https://travis-ci.org/DATA-DOG/godog.png)](https://travis-ci.org/DATA-DOG/godog) -[![GoDoc](https://godoc.org/github.com/DATA-DOG/godog/gherkin?status.svg)](https://godoc.org/github.com/DATA-DOG/godog/gherkin) - -# Gherkin Parser for GO - -Package gherkin is a gherkin language parser based on [specification][gherkin] -specification. It parses a feature file into the it's structural representation. It also -creates an AST tree of gherkin Tokens read from the file. - -With gherkin language you can describe your application behavior as features in -human-readable and machine friendly language. - -``` go -package main - -import ( - "log" - "os" - - "github.com/DATA-DOG/godog/gherkin" -) - -func main() { - feature, err := gherkin.ParseFile("ls.feature") - switch { - case err == gherkin.ErrEmpty: - log.Println("the feature file is empty and does not describe any feature") - return - case err != nil: - log.Fatalln("the feature file is incorrect or could not be read:", err) - } - log.Println("have parsed a feature:", feature.Title, "with", len(feature.Scenarios), "scenarios") -} -``` - -### Documentation - -See [godoc][godoc]. - -The public API is stable enough, but it may break until **1.0.0** version, see `godog --version`. - -Has no external dependencies. - -### License - -Licensed under the [three clause BSD license][license] - -[godoc]: http://godoc.org/github.com/DATA-DOG/godog/gherkin "Documentation on godoc for gherkin" -[gherkin]: https://cucumber.io/docs/reference "Gherkin feature file language" -[license]: http://en.wikipedia.org/wiki/BSD_licenses "The three clause BSD license" diff --git a/gherkin/feature_test.go b/gherkin/feature_test.go deleted file mode 100644 index 83d4586..0000000 --- a/gherkin/feature_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package gherkin - -import ( - "strings" - "testing" -) - -var testFeatureSamples = map[string]string{ - "feature": `Feature: gherkin parser - in order to run features - as gherkin lexer - I need to be able to parse a feature`, - "only_title": `Feature: gherkin`, - "empty": ``, - "invalid": `some text`, - "starts_with_newlines": ` - - Feature: gherkin`, -} - -func (f *Feature) assertTitle(title string, t *testing.T) { - if f.Title != title { - t.Fatalf("expected feature title to be '%s', but got '%s'", title, f.Title) - } -} - -func (f *Feature) assertHasNumScenarios(n int, t *testing.T) { - if len(f.Scenarios) != n { - t.Fatalf("expected feature to have '%d' scenarios, but got '%d'", n, len(f.Scenarios)) - } -} - -func Test_parse_normal_feature(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testFeatureSamples["feature"])), - path: "some.feature", - } - ft, err := p.parseFeature() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if ft.Title != "gherkin parser" { - t.Fatalf("the feature title '%s' was not expected", ft.Title) - } - if len(ft.Description) == 0 { - t.Fatalf("expected a feature description to be available") - } - - p.assertMatchesTypes([]TokenType{ - FEATURE, - TEXT, - TEXT, - TEXT, - }, t) -} - -func Test_parse_feature_without_description(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testFeatureSamples["only_title"])), - path: "some.feature", - } - ft, err := p.parseFeature() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if ft.Title != "gherkin" { - t.Fatalf("the feature title '%s' was not expected", ft.Title) - } - if len(ft.Description) > 0 { - t.Fatalf("feature description was not expected") - } - - p.assertMatchesTypes([]TokenType{ - FEATURE, - }, t) -} - -func Test_parse_empty_feature_file(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testFeatureSamples["empty"])), - path: "some.feature", - } - _, err := p.parseFeature() - if err != ErrEmpty { - t.Fatalf("expected an empty file error, but got none") - } -} - -func Test_parse_invalid_feature_with_random_text(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testFeatureSamples["invalid"])), - path: "some.feature", - } - _, err := p.parseFeature() - if err == nil { - t.Fatalf("expected an error but got none") - } - p.assertMatchesTypes([]TokenType{ - TEXT, - }, t) -} - -func Test_parse_feature_with_newlines(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testFeatureSamples["starts_with_newlines"])), - path: "some.feature", - } - ft, err := p.parseFeature() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if ft.Title != "gherkin" { - t.Fatalf("the feature title '%s' was not expected", ft.Title) - } - if len(ft.Description) > 0 { - t.Fatalf("feature description was not expected") - } - - p.assertMatchesTypes([]TokenType{ - NEWLINE, - NEWLINE, - FEATURE, - }, t) -} diff --git a/gherkin/gherkin.go b/gherkin/gherkin.go deleted file mode 100644 index a92f626..0000000 --- a/gherkin/gherkin.go +++ /dev/null @@ -1,420 +0,0 @@ -/* -Package gherkin is a gherkin language parser based on https://cucumber.io/docs/reference -specification. It parses a feature file into the it's structural representation. It also -creates an AST tree of gherkin Tokens read from the file. - -With gherkin language you can describe your application behavior as features in -human-readable and machine friendly language. - -For example, imagine you’re about to create the famous UNIX ls command. -Before you begin, you describe how the feature should work, see the example below.. - -Example: - Feature: ls - In order to see the directory structure - As a UNIX user - I need to be able to list the current directory's contents - - Scenario: - Given I am in a directory "test" - And I have a file named "foo" - And I have a file named "bar" - When I run "ls" - Then I should get: - """ - bar - foo - """ - -As a developer, your work is done as soon as you’ve made the ls command behave as -described in the Scenario. - -To read the feature in the example above.. - -Example: - package main - - import ( - "log" - "os" - - "github.com/DATA-DOG/godog/gherkin" - ) - - func main() { - feature, err := gherkin.Parse("ls.feature") - switch { - case err == gherkin.ErrEmpty: - log.Println("the feature file is empty and does not describe any feature") - return - case err != nil: - log.Println("the feature file is incorrect or could not be read:", err) - os.Exit(1) - } - log.Println("have parsed a feature:", feature.Title, "with", len(feature.Scenarios), "scenarios") - } - -Now the feature is available in the structure. -*/ -package gherkin - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - "unicode" -) - -// Tag is gherkin feature or scenario tag. -// it may be used to filter scenarios. -// -// tags may be set for a feature, in that case it will -// be merged with all scenario tags. or specifically -// to a single scenario -type Tag string - -// Tags is an array of tags -type Tags []Tag - -// Has checks whether the tag list has a tag -func (t Tags) Has(tag Tag) bool { - for _, tg := range t { - if tg == tag { - return true - } - } - return false -} - -// Outline is a scenario outline with an -// example table. Steps are listed with -// placeholders which are replaced with -// each example table row -type Outline struct { - *Token - Scenario *Scenario - Steps []*Step - Examples *Table -} - -// Scenario describes the scenario details -// -// if Examples table is not nil, then it -// means that this is an outline scenario -// with a table of examples to be run for -// each and every row -// -// Scenario may have tags which later may -// be used to filter out or run specific -// initialization tasks -type Scenario struct { - *Token - Title string - Steps []*Step - Tags Tags - Outline *Outline - Feature *Feature -} - -// Background steps are run before every scenario -type Background struct { - *Token - Steps []*Step - Feature *Feature -} - -// Step describes a Scenario or Background step -type Step struct { - *Token - Text string - Type string - PyString *PyString - Table *Table - Scenario *Scenario - Background *Background -} - -// Feature describes the whole feature -type Feature struct { - *Token - Path string - Tags Tags - Description string - Title string - Background *Background - Scenarios []*Scenario - AST []*Token -} - -// PyString is a multiline text object used with step definition -type PyString struct { - *Token - Raw string // raw multiline string body - Lines []string // trimmed lines - Step *Step -} - -// String returns raw multiline string -func (p *PyString) String() string { - return p.Raw -} - -// Table is a row group object used with -// step definition or outline scenario -type Table struct { - *Token - Step *Step - Rows [][]string -} - -var allSteps = []TokenType{ - GIVEN, - WHEN, - THEN, - AND, - BUT, -} - -// ErrEmpty is returned in case if feature file -// is completely empty. May be ignored in some use cases -var ErrEmpty = errors.New("the feature file is empty") - -type parser struct { - lx *lexer - path string - ast []*Token - peeked *Token -} - -// ParseFile parses a feature file on the given -// path into the Feature struct -// Returns a Feature struct and error if there is any -func ParseFile(path string) (*Feature, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - return Parse(file, path) -} - -// Parse the feature as a given name to the Feature struct -// Returns a Feature struct and error if there is any -func Parse(in io.Reader, name string) (*Feature, error) { - return (&parser{ - lx: newLexer(in), - path: name, - }).parseFeature() -} - -// reads tokens into AST and skips comments or new lines -func (p *parser) next() *Token { - if len(p.ast) > 0 && p.ast[len(p.ast)-1].Type == EOF { - return p.ast[len(p.ast)-1] // has reached EOF, do not record it more than once - } - tok := p.peek() - p.ast = append(p.ast, tok) - p.peeked = nil - return tok -} - -// peaks into next token, skips comments or new lines -func (p *parser) peek() *Token { - if p.peeked != nil { - return p.peeked - } - - for p.peeked = p.lx.read(); p.peeked.OfType(COMMENT, NEWLINE); p.peeked = p.lx.read() { - p.ast = append(p.ast, p.peeked) // record comments and newlines - } - - return p.peeked -} - -func (p *parser) err(s string, l int) error { - return fmt.Errorf("%s on %s:%d", s, p.path, l) -} - -func (p *parser) parseFeature() (ft *Feature, err error) { - - ft = &Feature{Path: p.path, AST: p.ast} - switch p.peek().Type { - case EOF: - return ft, ErrEmpty - case TAGS: - ft.Tags = p.parseTags() - } - - tok := p.next() - if tok.Type != FEATURE { - return ft, p.err("expected a file to begin with a feature definition, but got '"+tok.Type.String()+"' instead", tok.Line) - } - ft.Title = tok.Value - ft.Token = tok - - var desc []string - for ; p.peek().Type == TEXT; tok = p.next() { - desc = append(desc, p.peek().Text) - } - ft.Description = strings.Join(desc, "\n") - - for tok = p.peek(); tok.Type != EOF; tok = p.peek() { - // there may be a background - if tok.Type == BACKGROUND { - if ft.Background != nil { - return ft, p.err("there can only be a single background section, but found another", tok.Line) - } - - ft.Background = &Background{Token: tok, Feature: ft} - p.next() // jump to background steps - if ft.Background.Steps, err = p.parseSteps(); err != nil { - return ft, err - } - for _, step := range ft.Background.Steps { - step.Background = ft.Background - } - tok = p.peek() // peek to scenario or tags - } - - // there may be tags before scenario - var tags Tags - tags = append(tags, ft.Tags...) - if tok.Type == TAGS { - for _, t := range p.parseTags() { - if !tags.Has(t) { - tags = append(tags, t) - } - } - tok = p.peek() - } - - // there must be a scenario or scenario outline otherwise - if !tok.OfType(SCENARIO, OUTLINE) { - if tok.Type == EOF { - return ft, nil // there may not be a scenario defined after background - } - return ft, p.err("expected a scenario or scenario outline, but got '"+tok.Type.String()+"' instead", tok.Line) - } - - scenario, err := p.parseScenario() - if err != nil { - return ft, err - } - - scenario.Tags = tags - scenario.Feature = ft - ft.Scenarios = append(ft.Scenarios, scenario) - } - - return ft, nil -} - -func (p *parser) parseScenario() (s *Scenario, err error) { - tok := p.next() - s = &Scenario{Title: tok.Value, Token: tok} - if s.Steps, err = p.parseSteps(); err != nil { - return s, err - } - for _, step := range s.Steps { - step.Scenario = s - } - if examples := p.peek(); examples.Type == EXAMPLES { - p.next() // jump over the peeked token - peek := p.peek() - if peek.Type != TABLEROW { - return s, p.err(strings.Join([]string{ - "expected a table row,", - "but got '" + peek.Type.String() + "' instead, for scenario outline examples", - }, " "), examples.Line) - } - s.Outline = &Outline{ - Token: examples, - Scenario: s, - Steps: s.Steps, - } - s.Steps = []*Step{} // move steps to outline - if s.Outline.Examples, err = p.parseTable(); err != nil { - return s, err - } - if len(s.Outline.Examples.Rows) < 2 { - return s, p.err("expected an example table to have at least two rows: header and at least one example", examples.Line) - } - } - return s, nil -} - -func (p *parser) parseSteps() (steps []*Step, err error) { - for tok := p.peek(); tok.OfType(allSteps...); tok = p.peek() { - step := &Step{Text: tok.Value, Token: tok} - - p.next() // have read a peeked step - if step.Text[len(step.Text)-1] == ':' { - tok = p.peek() - switch tok.Type { - case PYSTRING: - if step.PyString, err = p.parsePystring(); err != nil { - return steps, err - } - step.PyString.Step = step - case TABLEROW: - if step.Table, err = p.parseTable(); err != nil { - return steps, err - } - step.Table.Step = step - default: - return steps, p.err("pystring or table row was expected, but got: '"+tok.Type.String()+"' instead", tok.Line) - } - } - - steps = append(steps, step) - } - - return steps, nil -} - -func (p *parser) parsePystring() (*PyString, error) { - var tok *Token - started := p.next() // skip the start of pystring - var lines, trimmed []string - for tok = p.next(); !tok.OfType(EOF, PYSTRING); tok = p.next() { - lines = append(lines, tok.Text) - trimmed = append(trimmed, strings.TrimSpace(tok.Text)) - } - if tok.Type == EOF { - return nil, fmt.Errorf("pystring which was opened on %s:%d was not closed", p.path, started.Line) - } - return &PyString{ - Raw: strings.Join(lines, "\n"), - Lines: trimmed, - }, nil -} - -func (p *parser) parseTable() (*Table, error) { - tbl := &Table{Token: p.peek()} - for row := p.peek(); row.Type == TABLEROW; row = p.peek() { - var cols []string - for _, r := range strings.Split(strings.Trim(row.Value, "|"), "|") { - cols = append(cols, strings.TrimFunc(r, unicode.IsSpace)) - } - // ensure the same colum number for each row - if len(tbl.Rows) > 0 && len(tbl.Rows[0]) != len(cols) { - return tbl, p.err("table row has not the same number of columns compared to previous row", row.Line) - } - tbl.Rows = append(tbl.Rows, cols) - p.next() // jump over the peeked token - } - return tbl, nil -} - -func (p *parser) parseTags() (tags Tags) { - for _, tag := range strings.Split(p.next().Value, " ") { - t := Tag(strings.Trim(tag, "@ ")) - if len(t) > 0 && !tags.Has(t) { - tags = append(tags, t) - } - } - return -} diff --git a/gherkin/lexer.go b/gherkin/lexer.go deleted file mode 100644 index d44f880..0000000 --- a/gherkin/lexer.go +++ /dev/null @@ -1,217 +0,0 @@ -package gherkin - -import ( - "bufio" - "io" - "regexp" - "strings" - "unicode" -) - -var matchers = map[string]*regexp.Regexp{ - "feature": regexp.MustCompile("^(\\s*)Feature:\\s*([^#]*)(#.*)?"), - "scenario": regexp.MustCompile("^(\\s*)Scenario:\\s*([^#]*)(#.*)?"), - "scenario_outline": regexp.MustCompile("^(\\s*)Scenario Outline:\\s*([^#]*)(#.*)?"), - "examples": regexp.MustCompile("^(\\s*)Examples:(\\s*#.*)?"), - "background": regexp.MustCompile("^(\\s*)Background:(\\s*#.*)?"), - "step": regexp.MustCompile("^(\\s*)(Given|When|Then|And|But)\\s+([^#]*)(#.*)?"), - "comment": regexp.MustCompile("^(\\s*)#(.+)"), - "pystring": regexp.MustCompile("^(\\s*)\\\"\\\"\\\""), - "tags": regexp.MustCompile("^(\\s*)@([^#]*)(#.*)?"), - "table_row": regexp.MustCompile("^(\\s*)\\|([^#]*)(#.*)?"), -} - -// for now only english language is supported -var keywords = map[TokenType]string{ - // special - ILLEGAL: "Illegal", - EOF: "End of file", - NEWLINE: "New line", - TAGS: "Tags", - COMMENT: "Comment", - PYSTRING: "PyString", - TABLEROW: "Table row", - TEXT: "Text", - // general - GIVEN: "Given", - WHEN: "When", - THEN: "Then", - AND: "And", - BUT: "But", - FEATURE: "Feature", - BACKGROUND: "Background", - SCENARIO: "Scenario", - OUTLINE: "Scenario Outline", - EXAMPLES: "Examples", -} - -type lexer struct { - reader *bufio.Reader - lines int -} - -func newLexer(r io.Reader) *lexer { - return &lexer{ - reader: bufio.NewReader(r), - } -} - -func (l *lexer) read() *Token { - line, err := l.reader.ReadString(byte('\n')) - if err != nil && len(line) == 0 { - return &Token{ - Type: EOF, - Line: l.lines + 1, - Keyword: keywords[EOF], - } - } - l.lines++ - line = strings.TrimRightFunc(line, unicode.IsSpace) - // newline - if len(line) == 0 { - return &Token{ - Type: NEWLINE, - Line: l.lines, - Keyword: keywords[NEWLINE], - } - } - // comment - if m := matchers["comment"].FindStringSubmatch(line); len(m) > 0 { - comment := strings.TrimSpace(m[2]) - return &Token{ - Type: COMMENT, - Indent: len(m[1]), - Line: l.lines, - Value: comment, - Text: line, - Comment: comment, - Keyword: keywords[COMMENT], - } - } - // pystring - if m := matchers["pystring"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: PYSTRING, - Indent: len(m[1]), - Line: l.lines, - Text: line, - Keyword: keywords[PYSTRING], - } - } - // step - if m := matchers["step"].FindStringSubmatch(line); len(m) > 0 { - tok := &Token{ - Indent: len(m[1]), - Line: l.lines, - Value: strings.TrimSpace(m[3]), - Text: line, - Comment: strings.Trim(m[4], " #"), - } - switch m[2] { - case "Given": - tok.Type = GIVEN - case "When": - tok.Type = WHEN - case "Then": - tok.Type = THEN - case "And": - tok.Type = AND - case "But": - tok.Type = BUT - } - tok.Keyword = keywords[tok.Type] - return tok - } - // scenario - if m := matchers["scenario"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: SCENARIO, - Indent: len(m[1]), - Line: l.lines, - Value: strings.TrimSpace(m[2]), - Text: line, - Comment: strings.Trim(m[3], " #"), - Keyword: keywords[SCENARIO], - } - } - // background - if m := matchers["background"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: BACKGROUND, - Indent: len(m[1]), - Line: l.lines, - Text: line, - Comment: strings.Trim(m[2], " #"), - Keyword: keywords[BACKGROUND], - } - } - // feature - if m := matchers["feature"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: FEATURE, - Indent: len(m[1]), - Line: l.lines, - Value: strings.TrimSpace(m[2]), - Text: line, - Comment: strings.Trim(m[3], " #"), - Keyword: keywords[FEATURE], - } - } - // tags - if m := matchers["tags"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: TAGS, - Indent: len(m[1]), - Line: l.lines, - Value: strings.TrimSpace(m[2]), - Text: line, - Comment: strings.Trim(m[3], " #"), - Keyword: keywords[TAGS], - } - } - // table row - if m := matchers["table_row"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: TABLEROW, - Indent: len(m[1]), - Line: l.lines, - Value: strings.TrimSpace(m[2]), - Text: line, - Comment: strings.Trim(m[3], " #"), - Keyword: keywords[TABLEROW], - } - } - // scenario outline - if m := matchers["scenario_outline"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: OUTLINE, - Indent: len(m[1]), - Line: l.lines, - Value: strings.TrimSpace(m[2]), - Text: line, - Comment: strings.Trim(m[3], " #"), - Keyword: keywords[OUTLINE], - } - } - // examples - if m := matchers["examples"].FindStringSubmatch(line); len(m) > 0 { - return &Token{ - Type: EXAMPLES, - Indent: len(m[1]), - Line: l.lines, - Text: line, - Comment: strings.Trim(m[2], " #"), - Keyword: keywords[EXAMPLES], - } - } - // text - text := strings.TrimLeftFunc(line, unicode.IsSpace) - return &Token{ - Type: TEXT, - Line: l.lines, - Value: text, - Indent: len(line) - len(text), - Text: line, - Keyword: keywords[TEXT], - } -} diff --git a/gherkin/lexer_test.go b/gherkin/lexer_test.go deleted file mode 100644 index 31ab79d..0000000 --- a/gherkin/lexer_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package gherkin - -import ( - "strings" - "testing" -) - -var testLexerSamples = map[string]string{ - "feature": `Feature: gherkin lexer - in order to run features - as gherkin lexer - I need to be able to parse a feature`, - - "background": `Background:`, - - "scenario": "Scenario: tokenize feature file", - - "step_given": `Given a feature file`, - - "step_when": `When I try to read it`, - - "comment": `# an important comment`, - - "step_then": `Then it should give me tokens`, - - "step_given_table": `Given there are users: - | name | lastname | num | - | Jack | Sparrow | 4 | - | John | Doe | 79 |`, - - "scenario_outline_with_examples": `Scenario Outline: ls supports kinds of options - Given I am in a directory "test" - And I have a file named "foo" - And I have a file named "bar" - When I run "ls" with options "" - Then I should see "" - - Examples: - | options | result | - | -t | bar foo | - | -tr | foo bar |`, -} - -func Test_feature_read(t *testing.T) { - l := newLexer(strings.NewReader(testLexerSamples["feature"])) - tok := l.read() - if tok.Type != FEATURE { - t.Fatalf("Expected a 'feature' type, but got: '%s'", tok.Type) - } - val := "gherkin lexer" - if tok.Value != val { - t.Fatalf("Expected a token value to be '%s', but got: '%s'", val, tok.Value) - } - if tok.Line != 1 { - t.Fatalf("Expected a token line to be '1', but got: '%d'", tok.Line) - } - if tok.Indent != 0 { - t.Fatalf("Expected a token identation to be '0', but got: '%d'", tok.Indent) - } - - tok = l.read() - if tok.Type != TEXT { - t.Fatalf("Expected a 'text' type, but got: '%s'", tok.Type) - } - val = "in order to run features" - if tok.Value != val { - t.Fatalf("Expected a token value to be '%s', but got: '%s'", val, tok.Value) - } - if tok.Line != 2 { - t.Fatalf("Expected a token line to be '2', but got: '%d'", tok.Line) - } - if tok.Indent != 2 { - t.Fatalf("Expected a token identation to be '2', but got: '%d'", tok.Indent) - } - - tok = l.read() - if tok.Type != TEXT { - t.Fatalf("Expected a 'text' type, but got: '%s'", tok.Type) - } - val = "as gherkin lexer" - if tok.Value != val { - t.Fatalf("Expected a token value to be '%s', but got: '%s'", val, tok.Value) - } - if tok.Line != 3 { - t.Fatalf("Expected a token line to be '3', but got: '%d'", tok.Line) - } - if tok.Indent != 2 { - t.Fatalf("Expected a token identation to be '2', but got: '%d'", tok.Indent) - } - - tok = l.read() - if tok.Type != TEXT { - t.Fatalf("Expected a 'text' type, but got: '%s'", tok.Type) - } - val = "I need to be able to parse a feature" - if tok.Value != val { - t.Fatalf("Expected a token value to be '%s', but got: '%s'", val, tok.Value) - } - if tok.Line != 4 { - t.Fatalf("Expected a token line to be '4', but got: '%d'", tok.Line) - } - if tok.Indent != 2 { - t.Fatalf("Expected a token identation to be '2', but got: '%d'", tok.Indent) - } - - tok = l.read() - if tok.Type != EOF { - t.Fatalf("Expected an 'eof' type, but got: '%s'", tok.Type) - } -} - -func Test_minimal_feature(t *testing.T) { - file := strings.Join([]string{ - testLexerSamples["feature"] + "\n", - - indent(2, testLexerSamples["background"]), - indent(4, testLexerSamples["step_given"]) + "\n", - - indent(2, testLexerSamples["comment"]), - indent(2, testLexerSamples["scenario"]), - indent(4, testLexerSamples["step_given"]), - indent(4, testLexerSamples["step_when"]), - indent(4, testLexerSamples["step_then"]), - }, "\n") - l := newLexer(strings.NewReader(file)) - - var tokens []TokenType - for tok := l.read(); tok.Type != EOF; tok = l.read() { - tokens = append(tokens, tok.Type) - } - expected := []TokenType{ - FEATURE, - TEXT, - TEXT, - TEXT, - NEWLINE, - - BACKGROUND, - GIVEN, - NEWLINE, - - COMMENT, - SCENARIO, - GIVEN, - WHEN, - THEN, - } - for i := 0; i < len(expected); i++ { - if expected[i] != tokens[i] { - t.Fatalf("expected token '%s' at position: %d, is not the same as actual token: '%s'", expected[i], i, tokens[i]) - } - } -} - -func Test_table_row_reading(t *testing.T) { - file := strings.Join([]string{ - indent(2, testLexerSamples["background"]), - indent(4, testLexerSamples["step_given_table"]), - indent(4, testLexerSamples["step_given"]), - }, "\n") - l := newLexer(strings.NewReader(file)) - - var types []TokenType - var values []string - var indents []int - for tok := l.read(); tok.Type != EOF; tok = l.read() { - types = append(types, tok.Type) - values = append(values, tok.Value) - indents = append(indents, tok.Indent) - } - expectedTypes := []TokenType{ - BACKGROUND, - GIVEN, - TABLEROW, - TABLEROW, - TABLEROW, - GIVEN, - } - expectedIndents := []int{2, 4, 6, 6, 6, 4} - for i := 0; i < len(expectedTypes); i++ { - if expectedTypes[i] != types[i] { - t.Fatalf("expected token type '%s' at position: %d, is not the same as actual: '%s'", expectedTypes[i], i, types[i]) - } - } - for i := 0; i < len(expectedIndents); i++ { - if expectedIndents[i] != indents[i] { - t.Fatalf("expected token indentation '%d' at position: %d, is not the same as actual: '%d'", expectedIndents[i], i, indents[i]) - } - } - if values[2] != "name | lastname | num |" { - t.Fatalf("table row value '%s' was not expected", values[2]) - } -} - -func Test_lexing_of_scenario_outline(t *testing.T) { - l := newLexer(strings.NewReader(testLexerSamples["scenario_outline_with_examples"])) - - var tokens []TokenType - for tok := l.read(); tok.Type != EOF; tok = l.read() { - tokens = append(tokens, tok.Type) - } - expected := []TokenType{ - OUTLINE, - GIVEN, - AND, - AND, - WHEN, - THEN, - NEWLINE, - - EXAMPLES, - TABLEROW, - TABLEROW, - TABLEROW, - } - for i := 0; i < len(expected); i++ { - if expected[i] != tokens[i] { - t.Fatalf("expected token '%s' at position: %d, is not the same as actual token: '%s'", expected[i], i, tokens[i]) - } - } -} diff --git a/gherkin/parse_test.go b/gherkin/parse_test.go deleted file mode 100644 index 9f72a42..0000000 --- a/gherkin/parse_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package gherkin - -import ( - "strings" - "testing" -) - -func (a *parser) assertMatchesTypes(expected []TokenType, t *testing.T) { - key := -1 - for _, tok := range a.ast { - key++ - if len(expected) <= key { - t.Fatalf("there are more tokens in AST then expected, next is '%s'", tok.Type) - } - if expected[key] != tok.Type { - t.Fatalf("expected ast token '%s', but got '%s' at position: %d", expected[key], tok.Type, key) - } - } - if len(expected)-1 != key { - t.Fatalf("expected ast length %d, does not match actual: %d", len(expected), key+1) - } -} - -func (s *Scenario) assertHasTag(tag string, t *testing.T) { - if !s.Tags.Has(Tag(tag)) { - t.Fatalf("expected scenario '%s' to have '%s' tag, but it did not", s.Title, tag) - } -} - -func (s *Scenario) assertHasNumTags(n int, t *testing.T) { - if len(s.Tags) != n { - t.Fatalf("expected scenario '%s' to have '%d' tags, but it has '%d'", s.Title, n, len(s.Tags)) - } -} - -func Test_parse_feature_file(t *testing.T) { - - content := strings.Join([]string{ - // feature - "@global-one @cust", - testFeatureSamples["feature"] + "\n", - // background - indent(2, "Background:"), - testStepSamples["given_table_hash"] + "\n", - // scenario - normal without tags - indent(2, "Scenario: user is able to register"), - testStepSamples["step_group"] + "\n", - // scenario - repeated tag, one extra - indent(2, "@user @cust"), - indent(2, "Scenario: password is required to login"), - testStepSamples["step_group_another"] + "\n", - // scenario - no steps yet - indent(2, "@todo"), // cust - tag is repeated - indent(2, "Scenario: user is able to reset his password") + "\n", - // scenario outline - testLexerSamples["scenario_outline_with_examples"], - }, "\n") - - p := &parser{ - lx: newLexer(strings.NewReader(content)), - path: "usual.feature", - } - ft, err := p.parseFeature() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - ft.assertTitle("gherkin parser", t) - - p.assertMatchesTypes([]TokenType{ - TAGS, - FEATURE, - TEXT, - TEXT, - TEXT, - NEWLINE, - - BACKGROUND, - GIVEN, - TABLEROW, - NEWLINE, - - SCENARIO, - GIVEN, - AND, - WHEN, - THEN, - NEWLINE, - - TAGS, - SCENARIO, - GIVEN, - AND, - WHEN, - THEN, - NEWLINE, - - TAGS, - SCENARIO, - NEWLINE, - - OUTLINE, - GIVEN, - AND, - AND, - WHEN, - THEN, - NEWLINE, - EXAMPLES, - TABLEROW, - TABLEROW, - TABLEROW, - }, t) - - ft.assertHasNumScenarios(4, t) - - ft.Scenarios[0].assertHasNumTags(2, t) - ft.Scenarios[0].assertHasTag("global-one", t) - ft.Scenarios[0].assertHasTag("cust", t) - - ft.Scenarios[1].assertHasNumTags(3, t) - ft.Scenarios[1].assertHasTag("global-one", t) - ft.Scenarios[1].assertHasTag("cust", t) - ft.Scenarios[1].assertHasTag("user", t) - - ft.Scenarios[2].assertHasNumTags(3, t) - ft.Scenarios[2].assertHasTag("global-one", t) - ft.Scenarios[2].assertHasTag("cust", t) - ft.Scenarios[2].assertHasTag("todo", t) - - ft.Scenarios[3].assertHasNumTags(2, t) -} diff --git a/gherkin/scenario_test.go b/gherkin/scenario_test.go deleted file mode 100644 index 3b6a119..0000000 --- a/gherkin/scenario_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package gherkin - -import ( - "strings" - "testing" -) - -func (s *Scenario) assertTitle(title string, t *testing.T) { - if s.Title != title { - t.Fatalf("expected scenario title to be '%s', but got '%s'", title, s.Title) - } -} - -func (s *Scenario) assertOutlineStep(text string, t *testing.T) *Step { - for _, stp := range s.Outline.Steps { - if stp.Text == text { - return stp - } - } - t.Fatalf("expected scenario '%s' to have step: '%s', but it did not", s.Title, text) - return nil -} - -func (s *Scenario) assertStep(text string, t *testing.T) *Step { - for _, stp := range s.Steps { - if stp.Text == text { - return stp - } - } - t.Fatalf("expected scenario '%s' to have step: '%s', but it did not", s.Title, text) - return nil -} - -func (s *Scenario) assertExampleRow(t *testing.T, num int, cols ...string) { - if s.Outline.Examples == nil { - t.Fatalf("outline scenario '%s' has no examples", s.Title) - } - if len(s.Outline.Examples.Rows) <= num { - t.Fatalf("outline scenario '%s' table has no row: %d", s.Title, num) - } - if len(s.Outline.Examples.Rows[num]) != len(cols) { - t.Fatalf("outline scenario '%s' table row length, does not match expected: %d", s.Title, len(cols)) - } - for i, col := range s.Outline.Examples.Rows[num] { - if col != cols[i] { - t.Fatalf("outline scenario '%s' table row %d, column %d - value '%s', does not match expected: %s", s.Title, num, i, col, cols[i]) - } - } -} - -func Test_parse_scenario_outline(t *testing.T) { - - p := &parser{ - lx: newLexer(strings.NewReader(testLexerSamples["scenario_outline_with_examples"])), - path: "usual.feature", - } - s, err := p.parseScenario() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - s.assertTitle("ls supports kinds of options", t) - - p.assertMatchesTypes([]TokenType{ - OUTLINE, - GIVEN, - AND, - AND, - WHEN, - THEN, - NEWLINE, - EXAMPLES, - TABLEROW, - TABLEROW, - TABLEROW, - }, t) - - s.assertOutlineStep(`I am in a directory "test"`, t) - s.assertOutlineStep(`I have a file named "foo"`, t) - s.assertOutlineStep(`I have a file named "bar"`, t) - s.assertOutlineStep(`I run "ls" with options ""`, t) - s.assertOutlineStep(`I should see ""`, t) - - s.assertExampleRow(t, 0, "options", "result") - s.assertExampleRow(t, 1, "-t", "bar foo") - s.assertExampleRow(t, 2, "-tr", "foo bar") -} diff --git a/gherkin/steps_test.go b/gherkin/steps_test.go deleted file mode 100644 index 6bf4cbc..0000000 --- a/gherkin/steps_test.go +++ /dev/null @@ -1,309 +0,0 @@ -package gherkin - -import ( - "strings" - "testing" -) - -var testStepSamples = map[string]string{ - "given": indent(4, `Given I'm a step`), - - "given_table_hash": `Given there are users: - | name | John Doe |`, - - "step_comment": `Given I'm an admin # sets admin permissions`, - - "given_table": `Given there are users: - | name | lastname | - | John | Doe | - | Jane | Doe |`, - - "then_pystring": `Then there should be text: - """ - Some text - And more - """`, - - "when_pystring_empty": `When I do request with body: - """ - """`, - - "when_pystring_unclosed": `When I do request with body: - """ - {"json": "data"} - ""`, - - "step_group": `Given there are conditions - And there are more conditions - When I do something - Then something should happen`, - - "step_group_another": `Given an admin user "John Doe" - And user "John Doe" belongs to user group "editors" - When I do something - Then I expect the result`, -} - -func (s *Step) assertText(text string, t *testing.T) { - if s.Text != text { - t.Fatalf("expected step text to be '%s', but got '%s'", text, s.Text) - } -} - -func (s *Step) assertPyString(text string, t *testing.T) { - if s.PyString == nil { - t.Fatalf("step '%s %s' has no pystring", s.Type, s.Text) - } - if s.PyString.Raw != text { - t.Fatalf("expected step pystring body to be '%s', but got '%s'", text, s.PyString.Raw) - } -} - -func (s *Step) assertComment(comment string, t *testing.T) { - if s.Token.Comment != comment { - t.Fatalf("expected step '%s' comment to be '%s', but got '%s'", s.Text, comment, s.Token.Comment) - } -} - -func (s *Step) assertTableRow(t *testing.T, num int, cols ...string) { - if s.Table == nil { - t.Fatalf("step '%s %s' has no table", s.Type, s.Text) - } - if len(s.Table.Rows) <= num { - t.Fatalf("step '%s %s' table has no row: %d", s.Type, s.Text, num) - } - if len(s.Table.Rows[num]) != len(cols) { - t.Fatalf("step '%s %s' table row length, does not match expected: %d", s.Type, s.Text, len(cols)) - } - for i, col := range s.Table.Rows[num] { - if col != cols[i] { - t.Fatalf("step '%s %s' table row %d, column %d - value '%s', does not match expected: %s", s.Type, s.Text, num, i, col, cols[i]) - } - } -} - -func Test_parse_basic_given_step(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["given"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 1 { - t.Fatalf("expected one step to be parsed") - } - - steps[0].assertText("I'm a step", t) - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - GIVEN, - EOF, - }, t) -} - -func Test_parse_step_with_comment(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["step_comment"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 1 { - t.Fatalf("expected one step to be parsed") - } - - steps[0].assertText("I'm an admin", t) - steps[0].assertComment("sets admin permissions", t) - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - GIVEN, - EOF, - }, t) -} - -func Test_parse_hash_table_given_step(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["given_table_hash"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 1 { - t.Fatalf("expected one step to be parsed") - } - - steps[0].assertText("there are users:", t) - steps[0].assertTableRow(t, 0, "name", "John Doe") - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - GIVEN, - TABLEROW, - EOF, - }, t) -} - -func Test_parse_table_given_step(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["given_table"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 1 { - t.Fatalf("expected one step to be parsed") - } - - steps[0].assertText("there are users:", t) - steps[0].assertTableRow(t, 0, "name", "lastname") - steps[0].assertTableRow(t, 1, "John", "Doe") - steps[0].assertTableRow(t, 2, "Jane", "Doe") - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - GIVEN, - TABLEROW, - TABLEROW, - TABLEROW, - EOF, - }, t) -} - -func Test_parse_pystring_step(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["then_pystring"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 1 { - t.Fatalf("expected one step to be parsed") - } - - steps[0].assertText("there should be text:", t) - steps[0].assertPyString(strings.Join([]string{ - indent(4, "Some text"), - indent(4, "And more"), - }, "\n"), t) - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - THEN, - PYSTRING, - TEXT, - AND, // we do not care what we parse inside PYSTRING even if its whole behat feature text - PYSTRING, - EOF, - }, t) -} - -func Test_parse_empty_pystring_step(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["when_pystring_empty"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 1 { - t.Fatalf("expected one step to be parsed") - } - - steps[0].assertText("I do request with body:", t) - steps[0].assertPyString("", t) - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - WHEN, - PYSTRING, - PYSTRING, - EOF, - }, t) -} - -func Test_parse_unclosed_pystring_step(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["when_pystring_unclosed"])), - path: "some.feature", - } - _, err := p.parseSteps() - if err == nil { - t.Fatalf("expected an error, but got none") - } - p.assertMatchesTypes([]TokenType{ - WHEN, - PYSTRING, - TEXT, - TEXT, - EOF, - }, t) -} - -func Test_parse_step_group(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["step_group"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 4 { - t.Fatalf("expected four steps to be parsed, but got: %d", len(steps)) - } - - steps[0].assertText("there are conditions", t) - steps[1].assertText("there are more conditions", t) - steps[2].assertText("I do something", t) - steps[3].assertText("something should happen", t) - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - GIVEN, - AND, - WHEN, - THEN, - EOF, - }, t) -} - -func Test_parse_another_step_group(t *testing.T) { - p := &parser{ - lx: newLexer(strings.NewReader(testStepSamples["step_group_another"])), - path: "some.feature", - } - steps, err := p.parseSteps() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(steps) != 4 { - t.Fatalf("expected four steps to be parsed, but got: %d", len(steps)) - } - - steps[0].assertText(`an admin user "John Doe"`, t) - steps[1].assertText(`user "John Doe" belongs to user group "editors"`, t) - steps[2].assertText("I do something", t) - steps[3].assertText("I expect the result", t) - - p.next() // step over to eof - p.assertMatchesTypes([]TokenType{ - GIVEN, - AND, - WHEN, - THEN, - EOF, - }, t) -} diff --git a/gherkin/token.go b/gherkin/token.go deleted file mode 100644 index d1be1d8..0000000 --- a/gherkin/token.go +++ /dev/null @@ -1,65 +0,0 @@ -package gherkin - -import ( - "strings" - "unicode" -) - -// TokenType defines a gherkin token type -type TokenType int - -// TokenType constants -const ( - ILLEGAL TokenType = iota - COMMENT - NEWLINE - EOF - TEXT - TAGS - TABLEROW - PYSTRING - FEATURE - BACKGROUND - SCENARIO - OUTLINE - EXAMPLES - GIVEN - WHEN - THEN - AND - BUT -) - -// String gives a string representation of token type -func (t TokenType) String() string { - return keywords[t] -} - -// Token represents a line in gherkin feature file -type Token struct { - Type TokenType // type of token - Line, Indent int // line and indentation number - Value string // interpreted value - Text string // same text as read - Keyword string // @TODO: the translated keyword - Comment string // a comment -} - -// OfType checks whether token is one of types -func (t *Token) OfType(all ...TokenType) bool { - for _, typ := range all { - if typ == t.Type { - return true - } - } - return false -} - -// Length gives a token text length with indentation -// and keyword, but without comment -func (t *Token) Length() int { - if pos := strings.Index(t.Text, "#"); pos != -1 { - return len(strings.TrimRightFunc(t.Text[:pos], unicode.IsSpace)) - } - return len(t.Text) -} diff --git a/gherkin/util_test.go b/gherkin/util_test.go deleted file mode 100644 index 28f5e8c..0000000 --- a/gherkin/util_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package gherkin - -import "strings" - -func indent(n int, s string) string { - return strings.Repeat(" ", n) + s -} diff --git a/suite.go b/suite.go index 8e74202..f9f3c19 100644 --- a/suite.go +++ b/suite.go @@ -10,9 +10,14 @@ import ( "strconv" "strings" - "github.com/DATA-DOG/godog/gherkin" + "github.com/cucumber/gherkin-go" ) +type feature struct { + *gherkin.Feature + Path string `json:"path"` +} + // Regexp is an unified type for regular expression // it can be either a string or a *regexp.Regexp type Regexp interface{} @@ -63,16 +68,16 @@ type Suite interface { Step(expr Regexp, h StepHandler) // suite events BeforeSuite(f func()) - BeforeScenario(f func(*gherkin.Scenario)) + BeforeScenario(f func(interface{})) BeforeStep(f func(*gherkin.Step)) AfterStep(f func(*gherkin.Step, error)) - AfterScenario(f func(*gherkin.Scenario, error)) + AfterScenario(f func(interface{}, error)) AfterSuite(f func()) } type suite struct { stepHandlers []*StepDef - features []*gherkin.Feature + features []*feature fmt Formatter failed bool @@ -87,10 +92,10 @@ type suite struct { // suite event handlers beforeSuiteHandlers []func() - beforeScenarioHandlers []func(*gherkin.Scenario) + beforeScenarioHandlers []func(interface{}) beforeStepHandlers []func(*gherkin.Step) afterStepHandlers []func(*gherkin.Step, error) - afterScenarioHandlers []func(*gherkin.Scenario, error) + afterScenarioHandlers []func(interface{}, error) afterSuiteHandlers []func() } @@ -142,12 +147,15 @@ func (s *suite) BeforeSuite(f func()) { } // BeforeScenario registers a function or method -// to be run before every scenario. +// to be run before every scenario or scenario outline. +// +// The interface argument may be *gherkin.Scenario +// or *gherkin.ScenarioOutline // // It is a good practice to restore the default state // before every scenario so it would be isolated from // any kind of state. -func (s *suite) BeforeScenario(f func(*gherkin.Scenario)) { +func (s *suite) BeforeScenario(f func(interface{})) { s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f) } @@ -171,8 +179,11 @@ func (s *suite) AfterStep(f func(*gherkin.Step, error)) { } // AfterScenario registers an function or method -// to be run after every scenario -func (s *suite) AfterScenario(f func(*gherkin.Scenario, error)) { +// to be run after every scenario or scenario outline +// +// The interface argument may be *gherkin.Scenario +// or *gherkin.ScenarioOutline +func (s *suite) AfterScenario(f func(interface{}, error)) { s.afterScenarioHandlers = append(s.afterScenarioHandlers, f) } @@ -255,11 +266,8 @@ func (s *suite) matchStep(step *gherkin.Step) *StepDef { for _, a := range m[1:] { args = append(args, &Arg{value: a}) } - if step.Table != nil { - args = append(args, &Arg{value: step.Table}) - } - if step.PyString != nil { - args = append(args, &Arg{value: step.PyString}) + if step.Argument != nil { + args = append(args, &Arg{value: step.Argument}) } h.Args = args return h @@ -277,7 +285,10 @@ func (s *suite) runStep(step *gherkin.Step) (err error) { defer func() { if e := recover(); e != nil { - err = e.(error) + err, ok := e.(error) + if !ok { + err = fmt.Errorf(e.(string)) + } s.fmt.Failed(step, match, err) } }() @@ -318,34 +329,57 @@ func (s *suite) skipSteps(steps []*gherkin.Step) { } } -func (s *suite) runOutline(scenario *gherkin.Scenario) (err error) { - placeholders := scenario.Outline.Examples.Rows[0] - examples := scenario.Outline.Examples.Rows[1:] - for _, example := range examples { - var steps []*gherkin.Step - for _, step := range scenario.Outline.Steps { - text := step.Text - for i, placeholder := range placeholders { - text = strings.Replace(text, "<"+placeholder+">", example[i], -1) - } - // clone a step - cloned := &gherkin.Step{ - Token: step.Token, - Text: text, - Type: step.Type, - PyString: step.PyString, - Table: step.Table, - Background: step.Background, - Scenario: scenario, - } - steps = append(steps, cloned) - } +func (s *suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Background) (err error) { + // run before scenario handlers + defer func() { + // run after scenario handlers + }() - // set steps to scenario - scenario.Steps = steps - if err = s.runScenario(scenario); err != nil && err != ErrUndefined { - s.failed = true - if s.stopOnFailure { + s.fmt.Node(outline) + + for _, example := range outline.Examples { + s.fmt.Node(example) + + placeholders := example.TableHeader.Cells + groups := example.TableBody + + for _, group := range groups { + for _, f := range s.beforeScenarioHandlers { + f(outline) + } + var steps []*gherkin.Step + for _, outlineStep := range outline.Steps { + text := outlineStep.Text + for i, placeholder := range placeholders { + text = strings.Replace(text, "<"+placeholder.Value+">", group.Cells[i].Value, -1) + } + // clone a step + step := &gherkin.Step{ + Node: outlineStep.Node, + Text: text, + Keyword: outlineStep.Keyword, + Argument: outlineStep.Argument, + } + steps = append(steps, step) + } + // run background + if b != nil { + err = s.runSteps(b.Steps) + } + switch err { + case ErrUndefined: + s.skipSteps(steps) + case nil: + err = s.runSteps(steps) + default: + s.skipSteps(steps) + } + + for _, f := range s.afterScenarioHandlers { + f(outline, err) + } + + if s.stopOnFailure && err != ErrUndefined { return } } @@ -353,15 +387,18 @@ func (s *suite) runOutline(scenario *gherkin.Scenario) (err error) { return } -func (s *suite) runFeature(f *gherkin.Feature) { - s.fmt.Node(f) - for _, scenario := range f.Scenarios { +func (s *suite) runFeature(f *feature) { + s.fmt.Feature(f.Feature, f.Path) + for _, scenario := range f.ScenarioDefinitions { var err error - // handle scenario outline differently - if scenario.Outline != nil { - err = s.runOutline(scenario) - } else { - err = s.runScenario(scenario) + if f.Background != nil { + s.fmt.Node(f.Background) + } + switch t := scenario.(type) { + case *gherkin.ScenarioOutline: + err = s.runOutline(t, f.Background) + case *gherkin.Scenario: + err = s.runScenario(t, f.Background) } if err != nil && err != ErrUndefined { s.failed = true @@ -372,16 +409,15 @@ func (s *suite) runFeature(f *gherkin.Feature) { } } -func (s *suite) runScenario(scenario *gherkin.Scenario) (err error) { +func (s *suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (err error) { // run before scenario handlers for _, f := range s.beforeScenarioHandlers { f(scenario) } // background - if scenario.Feature.Background != nil { - s.fmt.Node(scenario.Feature.Background) - err = s.runSteps(scenario.Feature.Background.Steps) + if b != nil { + err = s.runSteps(b.Steps) } // scenario @@ -435,25 +471,33 @@ func (s *suite) parseFeatures() (err error) { // parse features err = filepath.Walk(path, func(p string, f os.FileInfo, err error) error { if err == nil && !f.IsDir() && strings.HasSuffix(p, ".feature") { - ft, err := gherkin.ParseFile(p) - switch { - case err == gherkin.ErrEmpty: - // its ok, just skip it - case err != nil: + reader, err := os.Open(p) + if err != nil { return err - default: - s.features = append(s.features, ft) } + ft, err := gherkin.ParseFeature(reader) + reader.Close() + if err != nil { + return err + } + s.features = append(s.features, &feature{Path: p, Feature: ft}) // filter scenario by line number if line != -1 { - var scenarios []*gherkin.Scenario - for _, s := range ft.Scenarios { - if s.Token.Line == line { - scenarios = append(scenarios, s) + var scenarios []interface{} + for _, def := range ft.ScenarioDefinitions { + var ln int + switch t := def.(type) { + case *gherkin.Scenario: + ln = t.Location.Line + case *gherkin.ScenarioOutline: + ln = t.Location.Line + } + if ln == line { + scenarios = append(scenarios, def) break } } - ft.Scenarios = scenarios + ft.ScenarioDefinitions = scenarios } s.applyTagFilter(ft) } @@ -477,17 +521,62 @@ func (s *suite) applyTagFilter(ft *gherkin.Feature) { return } - var scenarios []*gherkin.Scenario - for _, scenario := range ft.Scenarios { - if s.matchesTags(scenario.Tags) { + var scenarios []interface{} + for _, scenario := range ft.ScenarioDefinitions { + if s.matchesTags(allTags(ft, scenario)) { scenarios = append(scenarios, scenario) } } - ft.Scenarios = scenarios + ft.ScenarioDefinitions = scenarios +} + +func allTags(nodes ...interface{}) []string { + var tags, tmp []string + for _, node := range nodes { + var gr []*gherkin.Tag + switch t := node.(type) { + case *gherkin.Feature: + gr = t.Tags + case *gherkin.ScenarioOutline: + gr = t.Tags + case *gherkin.Scenario: + gr = t.Tags + case *gherkin.Examples: + gr = t.Tags + } + + for _, gtag := range gr { + tag := strings.TrimSpace(gtag.Name) + if tag[0] == '@' { + tag = tag[1:] + } + copy(tmp, tags) + var found bool + for _, tg := range tmp { + if tg == tag { + found = true + break + } + } + if !found { + tags = append(tags, tag) + } + } + } + return tags +} + +func hasTag(tags []string, tag string) bool { + for _, t := range tags { + if t == tag { + return true + } + } + return false } // based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters -func (s *suite) matchesTags(tags gherkin.Tags) (ok bool) { +func (s *suite) matchesTags(tags []string) (ok bool) { ok = true for _, andTags := range strings.Split(s.tags, "&&") { var okComma bool @@ -495,9 +584,9 @@ func (s *suite) matchesTags(tags gherkin.Tags) (ok bool) { tag = strings.Replace(strings.TrimSpace(tag), "@", "", -1) if tag[0] == '~' { tag = tag[1:] - okComma = !tags.Has(gherkin.Tag(tag)) || okComma + okComma = !hasTag(tags, tag) || okComma } else { - okComma = tags.Has(gherkin.Tag(tag)) || okComma + okComma = hasTag(tags, tag) || okComma } } ok = (false != okComma && ok && okComma) || false diff --git a/suite_test.go b/suite_test.go index 8c5efa1..57e5c10 100644 --- a/suite_test.go +++ b/suite_test.go @@ -4,13 +4,13 @@ import ( "fmt" "strings" - "github.com/DATA-DOG/godog/gherkin" + "github.com/cucumber/gherkin-go" ) func SuiteContext(s Suite) { c := &suiteContext{} - s.BeforeScenario(c.HandleBeforeScenario) + s.BeforeScenario(c.ResetBeforeEachScenario) s.Step(`^a feature path "([^"]*)"$`, c.featurePath) s.Step(`^I parse features$`, c.parseFeatures) @@ -28,6 +28,11 @@ func SuiteContext(s Suite) { s.Step(`^a failing step`, c.aFailingStep) s.Step(`^this step should fail`, c.aFailingStep) s.Step(`^the following steps? should be (passed|failed|skipped|undefined):`, c.followingStepsShouldHave) + + // lt + s.Step(`^savybių aplankas "([^"]*)"$`, c.featurePath) + s.Step(`^aš išskaitau savybes$`, c.parseFeatures) + s.Step(`^aš turėčiau turėti ([\d]+) savybių failus:$`, c.iShouldHaveNumFeatureFiles) } type firedEvent struct { @@ -41,7 +46,7 @@ type suiteContext struct { fmt *testFormatter } -func (s *suiteContext) HandleBeforeScenario(*gherkin.Scenario) { +func (s *suiteContext) ResetBeforeEachScenario(interface{}) { // reset whole suite with the state s.fmt = &testFormatter{} s.testedSuite = &suite{fmt: s.fmt} @@ -52,7 +57,7 @@ func (s *suiteContext) HandleBeforeScenario(*gherkin.Scenario) { } func (s *suiteContext) followingStepsShouldHave(args ...*Arg) error { - var expected = args[1].PyString().Lines + var expected = strings.Split(args[1].DocString().Content, "\n") var actual, unmatched []string var matched []int @@ -116,10 +121,10 @@ func (s *suiteContext) iAmListeningToSuiteEvents(args ...*Arg) error { s.testedSuite.AfterSuite(func() { s.events = append(s.events, &firedEvent{"AfterSuite", []interface{}{}}) }) - s.testedSuite.BeforeScenario(func(scenario *gherkin.Scenario) { + s.testedSuite.BeforeScenario(func(scenario interface{}) { s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}}) }) - s.testedSuite.AfterScenario(func(scenario *gherkin.Scenario, err error) { + s.testedSuite.AfterScenario(func(scenario interface{}, err error) { s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, err}}) }) s.testedSuite.BeforeStep(func(step *gherkin.Step) { @@ -138,9 +143,9 @@ func (s *suiteContext) aFailingStep(...*Arg) error { // parse a given feature file body as a feature func (s *suiteContext) aFeatureFile(args ...*Arg) error { name := args[0].String() - body := args[1].PyString().Raw - feature, err := gherkin.Parse(strings.NewReader(body), name) - s.testedSuite.features = append(s.testedSuite.features, feature) + body := args[1].DocString().Content + ft, err := gherkin.ParseFeature(strings.NewReader(body)) + s.testedSuite.features = append(s.testedSuite.features, &feature{Feature: ft, Path: name}) return err } @@ -167,7 +172,7 @@ func (s *suiteContext) iShouldHaveNumFeatureFiles(args ...*Arg) error { if len(s.testedSuite.features) != args[0].Int() { return fmt.Errorf("expected %d features to be parsed, but have %d", args[0].Int(), len(s.testedSuite.features)) } - expected := args[1].PyString().Lines + expected := strings.Split(args[1].DocString().Content, "\n") var actual []string for _, ft := range s.testedSuite.features { actual = append(actual, ft.Path) @@ -194,7 +199,7 @@ func (s *suiteContext) iRunFeatureSuite(args ...*Arg) error { func (s *suiteContext) numScenariosRegistered(args ...*Arg) (err error) { var num int for _, ft := range s.testedSuite.features { - num += len(ft.Scenarios) + num += len(ft.ScenarioDefinitions) } if num != args[0].Int() { err = fmt.Errorf("expected %d scenarios to be registered, but got %d", args[0].Int(), num) @@ -222,12 +227,18 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(args ...*Arg) error continue } - scenario := event.args[0].(*gherkin.Scenario) - if scenario.Title == args[0].String() { + var name string + switch t := event.args[0].(type) { + case *gherkin.Scenario: + name = t.Name + case *gherkin.ScenarioOutline: + name = t.Name + } + if name == args[0].String() { return nil } - found = append(found, scenario.Title) + found = append(found, name) } if len(found) == 0 { @@ -238,16 +249,16 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(args ...*Arg) error } func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(args ...*Arg) error { - tbl := args[0].Table() - if len(tbl.Rows[0]) != 2 { - return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0])) + tbl := args[0].DataTable() + if len(tbl.Rows[0].Cells) != 2 { + return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0].Cells)) } for _, row := range tbl.Rows { args := []*Arg{ StepArgument(""), // ignored - StepArgument(row[1]), - StepArgument(row[0]), + StepArgument(row.Cells[1].Value), + StepArgument(row.Cells[0].Value), } if err := s.thereWereNumEventsFired(args...); err != nil { return err diff --git a/tag_filter_test.go b/tag_filter_test.go index 0c83caa..d49ae4f 100644 --- a/tag_filter_test.go +++ b/tag_filter_test.go @@ -2,29 +2,19 @@ package godog import ( "testing" - - "github.com/DATA-DOG/godog/gherkin" ) func assertNotMatchesTagFilter(tags []string, filter string, t *testing.T) { - gtags := gherkin.Tags{} - for _, tag := range tags { - gtags = append(gtags, gherkin.Tag(tag)) - } s := &suite{tags: filter} - if s.matchesTags(gtags) { - t.Errorf(`expected tags: %v not to match tag filter "%s", but it did`, gtags, filter) + if s.matchesTags(tags) { + t.Errorf(`expected tags: %v not to match tag filter "%s", but it did`, tags, filter) } } func assertMatchesTagFilter(tags []string, filter string, t *testing.T) { - gtags := gherkin.Tags{} - for _, tag := range tags { - gtags = append(gtags, gherkin.Tag(tag)) - } s := &suite{tags: filter} - if !s.matchesTags(gtags) { - t.Errorf(`expected tags: %v to match tag filter "%s", but it did not`, gtags, filter) + if !s.matchesTags(tags) { + t.Errorf(`expected tags: %v to match tag filter "%s", but it did not`, tags, filter) } }