refactor to use cocumber gherkin3 parser library

* bdde4c4 fix test suite and migration changes
* a3b6e01 refactor pretty formatter
* 2c0c7ba fix outline scenario handling
* f6b411d add a different language test feature

add a different language test feature
Этот коммит содержится в:
gedi 2015-06-25 14:44:22 +03:00
родитель 7b805b1ee7
коммит c6d00dd6d5
26 изменённых файлов: 451 добавлений и 1926 удалений

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

@ -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

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

@ -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

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

@ -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"

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

@ -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
}

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

@ -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))

17
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
"""

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

@ -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
"""

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

@ -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"

22
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,27 +45,22 @@ type Formatter interface {
// failed represents a failed step data structure
// with all necessary references
type failed struct {
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 {
feature *feature
owner interface{}
step *gherkin.Step
def *StepDef
}
@ -72,11 +68,15 @@ type passed struct {
// skipped represents a skipped step data structure
// with all necessary references
type skipped struct {
feature *feature
owner interface{}
step *gherkin.Step
}
// undefined represents a pending step data structure
// with all necessary references
type undefined struct {
feature *feature
owner interface{}
step *gherkin.Step
}

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

@ -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
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))
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))
}
if step.Table != nil {
f.printTable(step.Table, 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
if _, ok := owner.(*gherkin.Background); ok {
switch {
case step.Background != nil && f.backgroundSteps == 0:
case f.backgroundSteps == 0:
return
case step.Background != nil && f.backgroundSteps > 0:
case f.backgroundSteps > 0:
f.backgroundSteps--
}
if f.outlineExamples != 0 {
f.outlineSteps = append(f.outlineSteps, stepAction)
if len(f.outlineSteps) == f.outlineNumSteps {
// an outline example steps has went through
f.printOutlineExample(step.Scenario)
f.outlineExamples--
}
return // wait till example steps
if outline, ok := owner.(*gherkin.ScenarioOutline); ok {
f.outlineSteps = append(f.outlineSteps, stepAction)
if len(f.outlineSteps) == len(outline.Steps) {
// an outline example steps has went through
f.printOutlineExample(outline)
f.outlineSteps = []interface{}{}
f.outlineNumExamples--
}
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
}

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

@ -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)
}

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

@ -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})
}

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

@ -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.

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

@ -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"

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

@ -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)
}

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

@ -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 youre 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 youve 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
}

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

@ -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],
}
}

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

@ -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 "<options>"
Then I should see "<result>"
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])
}
}
}

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

@ -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)
}

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

@ -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 "<options>"`, t)
s.assertOutlineStep(`I should see "<result>"`, t)
s.assertExampleRow(t, 0, "options", "result")
s.assertExampleRow(t, 1, "-t", "bar foo")
s.assertExampleRow(t, 2, "-tr", "foo bar")
}

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

@ -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)
}

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

@ -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)
}

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

@ -1,7 +0,0 @@
package gherkin
import "strings"
func indent(n int, s string) string {
return strings.Repeat(" ", n) + s
}

223
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 {
func (s *suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Background) (err error) {
// run before scenario handlers
defer func() {
// run after scenario handlers
}()
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 _, step := range scenario.Outline.Steps {
text := step.Text
for _, outlineStep := range outline.Steps {
text := outlineStep.Text
for i, placeholder := range placeholders {
text = strings.Replace(text, "<"+placeholder+">", example[i], -1)
text = strings.Replace(text, "<"+placeholder.Value+">", group.Cells[i].Value, -1)
}
// clone a step
cloned := &gherkin.Step{
Token: step.Token,
step := &gherkin.Step{
Node: outlineStep.Node,
Text: text,
Type: step.Type,
PyString: step.PyString,
Table: step.Table,
Background: step.Background,
Scenario: scenario,
Keyword: outlineStep.Keyword,
Argument: outlineStep.Argument,
}
steps = append(steps, cloned)
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)
}
// set steps to scenario
scenario.Steps = steps
if err = s.runScenario(scenario); err != nil && err != ErrUndefined {
s.failed = true
if s.stopOnFailure {
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

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

@ -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

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

@ -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)
}
}