support scenario outline with example table

* 042235c expose StepDef which is needed to formatters
* e9474dc fix outline scenario formatting
* f02c6ce fix comment position calculation for outlines
* 5d7f128 remove step status type, use standard error
Этот коммит содержится в:
gedi 2015-06-19 13:55:27 +03:00
родитель 2e696381c9
коммит df26aa1c1c
12 изменённых файлов: 481 добавлений и 291 удалений

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

@ -56,34 +56,34 @@ func (f BeforeStepHandlerFunc) HandleBeforeStep(step *gherkin.Step) {
// in Suite to be executed after every step // in Suite to be executed after every step
// which will be run // which will be run
type AfterStepHandler interface { type AfterStepHandler interface {
HandleAfterStep(step *gherkin.Step, status Status) HandleAfterStep(step *gherkin.Step, err error)
} }
// AfterStepHandlerFunc is a function implementing // AfterStepHandlerFunc is a function implementing
// AfterStepHandler interface // AfterStepHandler interface
type AfterStepHandlerFunc func(step *gherkin.Step, status Status) type AfterStepHandlerFunc func(step *gherkin.Step, err error)
// HandleAfterStep is called with a *gherkin.Step argument // HandleAfterStep is called with a *gherkin.Step argument
// for after every step which is run by suite // for after every step which is run by suite
func (f AfterStepHandlerFunc) HandleAfterStep(step *gherkin.Step, status Status) { func (f AfterStepHandlerFunc) HandleAfterStep(step *gherkin.Step, err error) {
f(step, status) f(step, err)
} }
// AfterScenarioHandler can be registered // AfterScenarioHandler can be registered
// in Suite to be executed after every scenario // in Suite to be executed after every scenario
// which will be run // which will be run
type AfterScenarioHandler interface { type AfterScenarioHandler interface {
HandleAfterScenario(scenario *gherkin.Scenario, status Status) HandleAfterScenario(scenario *gherkin.Scenario, err error)
} }
// AfterScenarioHandlerFunc is a function implementing // AfterScenarioHandlerFunc is a function implementing
// AfterScenarioHandler interface // AfterScenarioHandler interface
type AfterScenarioHandlerFunc func(scenario *gherkin.Scenario, status Status) type AfterScenarioHandlerFunc func(scenario *gherkin.Scenario, err error)
// HandleAfterScenario is called with a *gherkin.Scenario argument // HandleAfterScenario is called with a *gherkin.Scenario argument
// for after every scenario which is run by suite // for after every scenario which is run by suite
func (f AfterScenarioHandlerFunc) HandleAfterScenario(scenario *gherkin.Scenario, status Status) { func (f AfterScenarioHandlerFunc) HandleAfterScenario(scenario *gherkin.Scenario, err error) {
f(scenario, status) f(scenario, err)
} }
// AfterSuiteHandler can be registered // AfterSuiteHandler can be registered

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

@ -27,8 +27,8 @@ Feature: suite events
When I run feature suite When I run feature suite
Then these events had to be fired for a number of times: Then these events had to be fired for a number of times:
| BeforeSuite | 1 | | BeforeSuite | 1 |
| BeforeScenario | 4 | | BeforeScenario | 6 |
| BeforeStep | 13 | | BeforeStep | 19 |
| AfterStep | 13 | | AfterStep | 19 |
| AfterScenario | 4 | | AfterScenario | 6 |
| AfterSuite | 1 | | AfterSuite | 1 |

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

@ -21,10 +21,16 @@ Feature: load features
features/load.feature features/load.feature
""" """
Scenario: load a feature file with a specified scenario Scenario Outline: loaded feature should have a number of scenarios
Given a feature path "features/load.feature:6" Given a feature path "<feature>"
When I parse features When I parse features
Then I should have 1 scenario registered Then I should have <number> scenario registered
Examples:
| feature | number |
| features/load.feature:3 | 0 |
| features/load.feature:6 | 1 |
| features/load.feature | 4 |
Scenario: load a number of feature files Scenario: load a number of feature files
Given a feature path "features/load.feature" Given a feature path "features/load.feature"

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

@ -10,8 +10,8 @@ import (
// output summary presentation // output summary presentation
type Formatter interface { type Formatter interface {
Node(interface{}) Node(interface{})
Failed(*gherkin.Step, *stepMatchHandler, error) Failed(*gherkin.Step, *StepDef, error)
Passed(*gherkin.Step, *stepMatchHandler) Passed(*gherkin.Step, *StepDef)
Skipped(*gherkin.Step) Skipped(*gherkin.Step)
Undefined(*gherkin.Step) Undefined(*gherkin.Step)
Summary() Summary()
@ -20,9 +20,9 @@ type Formatter interface {
// failed represents a failed step data structure // failed represents a failed step data structure
// with all necessary references // with all necessary references
type failed struct { type failed struct {
step *gherkin.Step step *gherkin.Step
handler *stepMatchHandler def *StepDef
err error err error
} }
func (f failed) line() string { func (f failed) line() string {
@ -41,8 +41,8 @@ func (f failed) line() string {
// passed represents a successful step data structure // passed represents a successful step data structure
// with all necessary references // with all necessary references
type passed struct { type passed struct {
step *gherkin.Step step *gherkin.Step
handler *stepMatchHandler def *StepDef
} }
// skipped represents a skipped step data structure // skipped represents a skipped step data structure

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

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"regexp"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -17,6 +18,8 @@ func init() {
}) })
} }
var outlinePlaceholderRegexp *regexp.Regexp = regexp.MustCompile("<[^>]+>")
// a built in default pretty formatter // a built in default pretty formatter
type pretty struct { type pretty struct {
feature *gherkin.Feature feature *gherkin.Feature
@ -25,6 +28,11 @@ type pretty struct {
background *gherkin.Background background *gherkin.Background
scenario *gherkin.Scenario scenario *gherkin.Scenario
// outline
outlineExamples int
outlineNumSteps int
outlineSteps []interface{}
// summary // summary
started time.Time started time.Time
features []*gherkin.Feature features []*gherkin.Feature
@ -51,33 +59,31 @@ func (f *pretty) Node(node interface{}) {
f.scenario = nil f.scenario = nil
f.background = nil f.background = nil
f.features = append(f.features, t) f.features = append(f.features, t)
fmt.Println(bcl("Feature: ", white) + t.Title) fmt.Println(bcl(t.Token.Keyword+": ", white) + t.Title)
fmt.Println(t.Description) fmt.Println(t.Description)
case *gherkin.Background: case *gherkin.Background:
// do not repeat background for the same feature // do not repeat background for the same feature
if f.background == nil && f.scenario == nil { if f.background == nil && f.scenario == nil {
f.background = t f.background = t
// determine comment position based on step length f.commentPos = longestStep(t.Steps, t.Token.Length())
f.commentPos = len(t.Token.Text)
for _, step := range t.Steps {
if len(step.Token.Text) > f.commentPos {
f.commentPos = len(step.Token.Text)
}
}
// print background node // print background node
fmt.Println("\n" + s(t.Token.Indent) + bcl("Background:", white)) fmt.Println("\n" + s(t.Token.Indent) + bcl(t.Token.Keyword+":", white))
} }
case *gherkin.Scenario: case *gherkin.Scenario:
f.scenario = t f.scenario = t
// determine comment position based on step length f.commentPos = longestStep(t.Steps, t.Token.Length())
f.commentPos = len(t.Token.Text) if t.Outline != nil {
for _, step := range t.Steps { f.outlineSteps = []interface{}{} // reset steps list
if len(step.Token.Text) > f.commentPos { f.commentPos = longestStep(t.Outline.Steps, t.Token.Length())
f.commentPos = len(step.Token.Text) 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("Scenario: ", white) + t.Title text := s(t.Token.Indent) + bcl(t.Token.Keyword+": ", white) + t.Title
text += s(f.commentPos-len(t.Token.Text)+1) + f.line(t.Token) text += s(f.commentPos-t.Token.Length()+1) + f.line(t.Token)
fmt.Println("\n" + text) fmt.Println("\n" + text)
} }
} }
@ -93,8 +99,22 @@ func (f *pretty) Summary() {
} }
if len(failedScenarios) > 0 { if len(failedScenarios) > 0 {
fmt.Println("\n--- " + cl("Failed scenarios:", red) + "\n") fmt.Println("\n--- " + cl("Failed scenarios:", red) + "\n")
var unique []string
for _, fail := range failedScenarios { for _, fail := range failedScenarios {
fmt.Println(" " + cl(fail.line(), red)) var found bool
for _, in := range unique {
if in == fail.line() {
found = true
break
}
}
if !found {
unique = append(unique, fail.line())
}
}
for _, fail := range unique {
fmt.Println(" " + cl(fail, red))
} }
} }
var total, passed int var total, passed int
@ -142,21 +162,129 @@ func (f *pretty) Summary() {
fmt.Println(elapsed) fmt.Println(elapsed)
} }
func (f *pretty) printStep(stepAction interface{}) { func (f *pretty) printOutlineExample() {
var c color var failed error
var step *gherkin.Step clr := green
var h *stepMatchHandler tbl := f.scenario.Outline.Examples
var err error firstExample := f.outlineExamples == len(tbl.Rows)-1
var suffix, prefix string
for i, act := range f.outlineSteps {
var c color
var def *StepDef
var err error
_, def, c, err = f.stepDetails(act)
// determine example row status
switch {
case err != nil:
failed = err
clr = red
case c == yellow:
clr = yellow
case c == cyan && clr == green:
clr = cyan
}
if firstExample {
// in first example, we need to print steps
var text string
ostep := f.scenario.Outline.Steps[i]
if def != nil {
if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
var pos int
for i := 0; i < len(m); i++ {
pair := m[i]
text += cl(ostep.Text[pos:pair[0]], cyan)
text += bcl(ostep.Text[pair[0]:pair[1]], cyan)
pos = pair[1]
}
text += cl(ostep.Text[pos:len(ostep.Text)], cyan)
} else {
text = cl(ostep.Text, cyan)
}
// 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)
} else {
text = cl(ostep.Text, cyan)
}
// print the step outline
fmt.Println(s(ostep.Token.Indent) + cl(ostep.Token.Keyword, cyan) + " " + text)
}
}
cols := make([]string, len(tbl.Rows[0]))
max := longest(tbl)
// an example table header
if firstExample {
out := f.scenario.Outline
fmt.Println("")
fmt.Println(s(out.Token.Indent) + bcl(out.Token.Keyword+":", white))
row := tbl.Rows[0]
for i, col := range row {
cols[i] = cl(col, cyan) + s(max[i]-len(col))
}
fmt.Println(s(tbl.Token.Indent) + "| " + strings.Join(cols, " | ") + " |")
}
// 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))
}
fmt.Println(s(tbl.Token.Indent) + "| " + strings.Join(cols, " | ") + " |")
// if there is an error
if failed != nil {
fmt.Println(s(tbl.Token.Indent) + bcl(failed, red))
}
}
func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) {
text := s(step.Token.Indent) + cl(step.Token.Keyword, c) + " "
switch {
case def != nil:
if m := (def.Expr.FindStringSubmatchIndex(step.Text))[2:]; len(m) > 0 {
var pos, i int
for pos, i = 0, 0; i < len(m); i++ {
if math.Mod(float64(i), 2) == 0 {
text += cl(step.Text[pos:m[i]], c)
} else {
text += bcl(step.Text[pos:m[i]], c)
}
pos = m[i]
}
text += cl(step.Text[pos:len(step.Text)], c)
} else {
text += cl(step.Text, c)
}
// 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)
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)
}
}
func (f *pretty) stepDetails(stepAction interface{}) (step *gherkin.Step, def *StepDef, c color, err error) {
switch typ := stepAction.(type) { switch typ := stepAction.(type) {
case *passed: case *passed:
step = typ.step step = typ.step
h = typ.handler def = typ.def
c = green c = green
case *failed: case *failed:
step = typ.step step = typ.step
h = typ.handler def = typ.def
err = typ.err err = typ.err
c = red c = red
case *skipped: case *skipped:
@ -168,56 +296,33 @@ func (f *pretty) printStep(stepAction interface{}) {
default: default:
fatal(fmt.Errorf("unexpected step type received: %T", typ)) fatal(fmt.Errorf("unexpected step type received: %T", typ))
} }
return
}
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)
// do not print background more than once // do not print background more than once
if f.scenario == nil && step.Background != f.background { if f.scenario == nil && step.Background != f.background {
return return
} }
if h != nil { if f.outlineExamples != 0 {
if m := (h.expr.FindStringSubmatchIndex(step.Text))[2:]; len(m) > 0 { f.outlineSteps = append(f.outlineSteps, stepAction)
var pos, i int if len(f.outlineSteps) == f.outlineNumSteps {
for pos, i = 0, 0; i < len(m); i++ { // an outline example steps has went through
if math.Mod(float64(i), 2) == 0 { f.printOutlineExample()
suffix += cl(step.Text[pos:m[i]], c) f.outlineExamples -= 1
} else {
suffix += bcl(step.Text[pos:m[i]], c)
}
pos = m[i]
}
suffix += cl(step.Text[pos:len(step.Text)], c)
} else {
suffix = cl(step.Text, c)
} }
// use reflect to get step handler function name return // wait till example steps
name := runtime.FuncForPC(reflect.ValueOf(h.handler).Pointer()).Name()
suffix += s(f.commentPos-len(step.Token.Text)+1) + cl(fmt.Sprintf("# %s", name), black)
} else {
suffix = cl(step.Text, c)
} }
prefix = s(step.Token.Indent) f.printStep(step, def, c)
switch step.Token.Type {
case gherkin.GIVEN:
prefix += cl("Given", c)
case gherkin.WHEN:
prefix += cl("When", c)
case gherkin.THEN:
prefix += cl("Then", c)
case gherkin.AND:
prefix += cl("And", c)
case gherkin.BUT:
prefix += cl("But", c)
}
fmt.Println(prefix, suffix)
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)
}
if err != nil { if err != nil {
fmt.Println(s(step.Token.Indent) + bcl(err, red)) fmt.Println(s(step.Token.Indent) + bcl(err, red))
} }
@ -225,8 +330,47 @@ func (f *pretty) printStep(stepAction interface{}) {
// print table with aligned table cells // print table with aligned table cells
func (f *pretty) printTable(t *gherkin.Table, c color) { func (f *pretty) printTable(t *gherkin.Table, c color) {
var longest = make([]int, len(t.Rows[0])) var l = longest(t)
var cols = make([]string, len(t.Rows[0])) var cols = make([]string, len(t.Rows[0]))
for _, row := range t.Rows {
for i, col := range row {
cols[i] = col + s(l[i]-len(col))
}
fmt.Println(s(t.Token.Indent) + 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}
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}
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}
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}
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 _, row := range t.Rows {
for i, col := range row { for i, col := range row {
if longest[i] < len(col) { if longest[i] < len(col) {
@ -234,38 +378,16 @@ func (f *pretty) printTable(t *gherkin.Table, c color) {
} }
} }
} }
for _, row := range t.Rows { return longest
for i, col := range row { }
cols[i] = col + s(longest[i]-len(col))
func longestStep(steps []*gherkin.Step, base int) int {
ret := base
for _, step := range steps {
length := step.Token.Length()
if length > base {
ret = length
} }
fmt.Println(s(t.Token.Indent) + cl("| "+strings.Join(cols, " | ")+" |", c))
} }
} return ret
// Passed is called to represent a passed step
func (f *pretty) Passed(step *gherkin.Step, match *stepMatchHandler) {
s := &passed{step: step, handler: match}
f.printStep(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}
f.printStep(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}
f.printStep(s)
f.undefined = append(f.undefined, s)
}
// Failed is called to represent a failed step
func (f *pretty) Failed(step *gherkin.Step, match *stepMatchHandler, err error) {
s := &failed{step: step, handler: match, err: err}
f.printStep(s)
f.failed = append(f.failed, s)
} }

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

@ -23,8 +23,8 @@ func (f *testFormatter) Node(node interface{}) {
func (f *testFormatter) Summary() {} func (f *testFormatter) Summary() {}
func (f *testFormatter) Passed(step *gherkin.Step, match *stepMatchHandler) { func (f *testFormatter) Passed(step *gherkin.Step, match *StepDef) {
f.passed = append(f.passed, &passed{step: step, handler: match}) f.passed = append(f.passed, &passed{step: step, def: match})
} }
func (f *testFormatter) Skipped(step *gherkin.Step) { func (f *testFormatter) Skipped(step *gherkin.Step) {
@ -35,6 +35,6 @@ func (f *testFormatter) Undefined(step *gherkin.Step) {
f.undefined = append(f.undefined, &undefined{step: step}) f.undefined = append(f.undefined, &undefined{step: step})
} }
func (f *testFormatter) Failed(step *gherkin.Step, match *stepMatchHandler, err error) { func (f *testFormatter) Failed(step *gherkin.Step, match *StepDef, err error) {
f.failed = append(f.failed, &failed{step: step, handler: match, err: err}) f.failed = append(f.failed, &failed{step: step, def: match, err: err})
} }

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

@ -88,6 +88,17 @@ func (t Tags) Has(tag Tag) bool {
return false 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 // Scenario describes the scenario details
// //
// if Examples table is not nil, then it // if Examples table is not nil, then it
@ -100,11 +111,11 @@ func (t Tags) Has(tag Tag) bool {
// initialization tasks // initialization tasks
type Scenario struct { type Scenario struct {
*Token *Token
Title string Title string
Steps []*Step Steps []*Step
Tags Tags Tags Tags
Examples *Table Outline *Outline
Feature *Feature Feature *Feature
} }
// Background steps are run before every scenario // Background steps are run before every scenario
@ -154,9 +165,8 @@ func (p *PyString) String() string {
// step definition or outline scenario // step definition or outline scenario
type Table struct { type Table struct {
*Token *Token
OutlineScenario *Scenario Step *Step
Step *Step Rows [][]string
Rows [][]string
} }
var allSteps = []TokenType{ var allSteps = []TokenType{
@ -320,10 +330,18 @@ func (p *parser) parseScenario() (s *Scenario, err error) {
"but got '" + peek.Type.String() + "' instead, for scenario outline examples", "but got '" + peek.Type.String() + "' instead, for scenario outline examples",
}, " "), examples.Line) }, " "), examples.Line)
} }
if s.Examples, err = p.parseTable(); err != nil { 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 return s, err
} }
s.Examples.OutlineScenario = s 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 return s, nil
} }

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

@ -21,6 +21,30 @@ var matchers = map[string]*regexp.Regexp{
"table_row": 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",
NEW_LINE: "New line",
TAGS: "Tags",
COMMENT: "Comment",
PYSTRING: "PyString",
TABLE_ROW: "Table row",
TEXT: "Text",
// general
GIVEN: "Given",
WHEN: "When",
THEN: "Then",
AND: "And",
BUT: "But",
FEATURE: "Feature",
BACKGROUND: "Background",
SCENARIO: "Scenario",
SCENARIO_OUTLINE: "Scenario Outline",
EXAMPLES: "Examples",
}
type lexer struct { type lexer struct {
reader *bufio.Reader reader *bufio.Reader
lines int lines int
@ -36,8 +60,9 @@ func (l *lexer) read() *Token {
line, err := l.reader.ReadString(byte('\n')) line, err := l.reader.ReadString(byte('\n'))
if err != nil && len(line) == 0 { if err != nil && len(line) == 0 {
return &Token{ return &Token{
Type: EOF, Type: EOF,
Line: l.lines, Line: l.lines + 1,
Keyword: keywords[EOF],
} }
} }
l.lines++ l.lines++
@ -45,8 +70,9 @@ func (l *lexer) read() *Token {
// newline // newline
if len(line) == 0 { if len(line) == 0 {
return &Token{ return &Token{
Type: NEW_LINE, Type: NEW_LINE,
Line: l.lines, Line: l.lines,
Keyword: keywords[NEW_LINE],
} }
} }
// comment // comment
@ -59,15 +85,17 @@ func (l *lexer) read() *Token {
Value: comment, Value: comment,
Text: line, Text: line,
Comment: comment, Comment: comment,
Keyword: keywords[COMMENT],
} }
} }
// pystring // pystring
if m := matchers["pystring"].FindStringSubmatch(line); len(m) > 0 { if m := matchers["pystring"].FindStringSubmatch(line); len(m) > 0 {
return &Token{ return &Token{
Type: PYSTRING, Type: PYSTRING,
Indent: len(m[1]), Indent: len(m[1]),
Line: l.lines, Line: l.lines,
Text: line, Text: line,
Keyword: keywords[PYSTRING],
} }
} }
// step // step
@ -91,6 +119,7 @@ func (l *lexer) read() *Token {
case "But": case "But":
tok.Type = BUT tok.Type = BUT
} }
tok.Keyword = keywords[tok.Type]
return tok return tok
} }
// scenario // scenario
@ -102,6 +131,7 @@ func (l *lexer) read() *Token {
Value: strings.TrimSpace(m[2]), Value: strings.TrimSpace(m[2]),
Text: line, Text: line,
Comment: strings.Trim(m[3], " #"), Comment: strings.Trim(m[3], " #"),
Keyword: keywords[SCENARIO],
} }
} }
// background // background
@ -112,6 +142,7 @@ func (l *lexer) read() *Token {
Line: l.lines, Line: l.lines,
Text: line, Text: line,
Comment: strings.Trim(m[2], " #"), Comment: strings.Trim(m[2], " #"),
Keyword: keywords[BACKGROUND],
} }
} }
// feature // feature
@ -123,6 +154,7 @@ func (l *lexer) read() *Token {
Value: strings.TrimSpace(m[2]), Value: strings.TrimSpace(m[2]),
Text: line, Text: line,
Comment: strings.Trim(m[3], " #"), Comment: strings.Trim(m[3], " #"),
Keyword: keywords[FEATURE],
} }
} }
// tags // tags
@ -134,6 +166,7 @@ func (l *lexer) read() *Token {
Value: strings.TrimSpace(m[2]), Value: strings.TrimSpace(m[2]),
Text: line, Text: line,
Comment: strings.Trim(m[3], " #"), Comment: strings.Trim(m[3], " #"),
Keyword: keywords[TAGS],
} }
} }
// table row // table row
@ -145,6 +178,7 @@ func (l *lexer) read() *Token {
Value: strings.TrimSpace(m[2]), Value: strings.TrimSpace(m[2]),
Text: line, Text: line,
Comment: strings.Trim(m[3], " #"), Comment: strings.Trim(m[3], " #"),
Keyword: keywords[TABLE_ROW],
} }
} }
// scenario outline // scenario outline
@ -156,6 +190,7 @@ func (l *lexer) read() *Token {
Value: strings.TrimSpace(m[2]), Value: strings.TrimSpace(m[2]),
Text: line, Text: line,
Comment: strings.Trim(m[3], " #"), Comment: strings.Trim(m[3], " #"),
Keyword: keywords[SCENARIO_OUTLINE],
} }
} }
// examples // examples
@ -166,15 +201,17 @@ func (l *lexer) read() *Token {
Line: l.lines, Line: l.lines,
Text: line, Text: line,
Comment: strings.Trim(m[2], " #"), Comment: strings.Trim(m[2], " #"),
Keyword: keywords[EXAMPLES],
} }
} }
// text // text
text := strings.TrimLeftFunc(line, unicode.IsSpace) text := strings.TrimLeftFunc(line, unicode.IsSpace)
return &Token{ return &Token{
Type: TEXT, Type: TEXT,
Line: l.lines, Line: l.lines,
Value: text, Value: text,
Indent: len(line) - len(text), Indent: len(line) - len(text),
Text: line, Text: line,
Keyword: keywords[TEXT],
} }
} }

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

@ -11,6 +11,16 @@ func (s *Scenario) assertTitle(title string, t *testing.T) {
} }
} }
func (s *Scenario) assertOutlineStep(text string, t *testing.T) *Step {
for _, stp := range s.Outline.Steps {
if stp.Text == text {
return stp
}
}
t.Fatal("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 { func (s *Scenario) assertStep(text string, t *testing.T) *Step {
for _, stp := range s.Steps { for _, stp := range s.Steps {
if stp.Text == text { if stp.Text == text {
@ -22,16 +32,16 @@ func (s *Scenario) assertStep(text string, t *testing.T) *Step {
} }
func (s *Scenario) assertExampleRow(t *testing.T, num int, cols ...string) { func (s *Scenario) assertExampleRow(t *testing.T, num int, cols ...string) {
if s.Examples == nil { if s.Outline.Examples == nil {
t.Fatalf("outline scenario '%s' has no examples", s.Title) t.Fatalf("outline scenario '%s' has no examples", s.Title)
} }
if len(s.Examples.Rows) <= num { if len(s.Outline.Examples.Rows) <= num {
t.Fatalf("outline scenario '%s' table has no row: %d", s.Title, num) t.Fatalf("outline scenario '%s' table has no row: %d", s.Title, num)
} }
if len(s.Examples.Rows[num]) != len(cols) { 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)) t.Fatalf("outline scenario '%s' table row length, does not match expected: %d", s.Title, len(cols))
} }
for i, col := range s.Examples.Rows[num] { for i, col := range s.Outline.Examples.Rows[num] {
if col != cols[i] { 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]) t.Fatalf("outline scenario '%s' table row %d, column %d - value '%s', does not match expected: %s", s.Title, num, i, col, cols[i])
} }
@ -64,11 +74,11 @@ func Test_parse_scenario_outline(t *testing.T) {
TABLE_ROW, TABLE_ROW,
}, t) }, t)
s.assertStep(`I am in a directory "test"`, t) s.assertOutlineStep(`I am in a directory "test"`, t)
s.assertStep(`I have a file named "foo"`, t) s.assertOutlineStep(`I have a file named "foo"`, t)
s.assertStep(`I have a file named "bar"`, t) s.assertOutlineStep(`I have a file named "bar"`, t)
s.assertStep(`I run "ls" with options "<options>"`, t) s.assertOutlineStep(`I run "ls" with options "<options>"`, t)
s.assertStep(`I should see "<result>"`, t) s.assertOutlineStep(`I should see "<result>"`, t)
s.assertExampleRow(t, 0, "options", "result") s.assertExampleRow(t, 0, "options", "result")
s.assertExampleRow(t, 1, "-t", "bar foo") s.assertExampleRow(t, 1, "-t", "bar foo")

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

@ -1,29 +1,26 @@
package gherkin package gherkin
import (
"strings"
"unicode"
)
type TokenType int type TokenType int
const ( const (
ILLEGAL TokenType = iota ILLEGAL TokenType = iota
specials
COMMENT COMMENT
NEW_LINE NEW_LINE
EOF EOF
elements
TEXT TEXT
TAGS TAGS
TABLE_ROW TABLE_ROW
PYSTRING PYSTRING
keywords
FEATURE FEATURE
BACKGROUND BACKGROUND
SCENARIO SCENARIO
SCENARIO_OUTLINE SCENARIO_OUTLINE
EXAMPLES EXAMPLES
steps
GIVEN GIVEN
WHEN WHEN
THEN THEN
@ -31,54 +28,22 @@ const (
BUT BUT
) )
// String gives a string representation of token type
func (t TokenType) String() string { func (t TokenType) String() string {
switch t { return keywords[t]
case COMMENT:
return "comment"
case NEW_LINE:
return "new line"
case EOF:
return "end of file"
case TEXT:
return "text"
case TAGS:
return "tags"
case TABLE_ROW:
return "table row"
case PYSTRING:
return "pystring"
case FEATURE:
return "feature"
case BACKGROUND:
return "background"
case SCENARIO:
return "scenario"
case SCENARIO_OUTLINE:
return "scenario outline"
case EXAMPLES:
return "examples"
case GIVEN:
return "given step"
case WHEN:
return "when step"
case THEN:
return "then step"
case AND:
return "and step"
case BUT:
return "but step"
}
return "illegal"
} }
// Token represents a line in gherkin feature file
type Token struct { type Token struct {
Type TokenType // type of token Type TokenType // type of token
Line, Indent int // line and indentation number Line, Indent int // line and indentation number
Value string // interpreted value Value string // interpreted value
Text string // same text as read Text string // same text as read
Keyword string // @TODO: the translated keyword
Comment string // a comment Comment string // a comment
} }
// OfType checks whether token is one of types
func (t *Token) OfType(all ...TokenType) bool { func (t *Token) OfType(all ...TokenType) bool {
for _, typ := range all { for _, typ := range all {
if typ == t.Type { if typ == t.Type {
@ -87,3 +52,12 @@ func (t *Token) OfType(all ...TokenType) bool {
} }
return false 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)
}

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

@ -7,6 +7,7 @@ import (
"reflect" "reflect"
"regexp" "regexp"
"runtime" "runtime"
"strings"
"github.com/DATA-DOG/godog/gherkin" "github.com/DATA-DOG/godog/gherkin"
) )
@ -20,30 +21,6 @@ type Regexp interface{}
// or a step handler // or a step handler
type Handler interface{} type Handler interface{}
// Status represents a step or scenario status
type Status int
// step or scenario status constants
const (
Invalid Status = iota
Passed
Failed
Undefined
)
// String represents status as string
func (s Status) String() string {
switch s {
case Passed:
return "passed"
case Failed:
return "failed"
case Undefined:
return "undefined"
}
return "invalid"
}
// Objects implementing the StepHandler interface can be // Objects implementing the StepHandler interface can be
// registered as step definitions in godog // registered as step definitions in godog
// //
@ -70,11 +47,17 @@ func (f StepHandlerFunc) HandleStep(args ...*Arg) error {
return f(args...) return f(args...)
} }
var errPending = fmt.Errorf("pending step") // ErrUndefined is returned in case if step definition was not found
var ErrUndefined = fmt.Errorf("step is undefined")
type stepMatchHandler struct { // StepDef is a registered step definition
handler StepHandler // contains a StepHandler, a regexp which
expr *regexp.Regexp // is used to match a step and Args which
// were matched by last step
type StepDef struct {
Args []*Arg
Handler StepHandler
Expr *regexp.Regexp
} }
// Suite is an interface which allows various contexts // Suite is an interface which allows various contexts
@ -91,7 +74,7 @@ type Suite interface {
} }
type suite struct { type suite struct {
stepHandlers []*stepMatchHandler stepHandlers []*StepDef
features []*gherkin.Feature features []*gherkin.Feature
fmt Formatter fmt Formatter
@ -151,9 +134,9 @@ func (s *suite) Step(expr Regexp, h Handler) {
default: default:
panic(fmt.Sprintf("expecting handler to satisfy StepHandler interface, got type: %T", h)) panic(fmt.Sprintf("expecting handler to satisfy StepHandler interface, got type: %T", h))
} }
s.stepHandlers = append(s.stepHandlers, &stepMatchHandler{ s.stepHandlers = append(s.stepHandlers, &StepDef{
handler: handler, Handler: handler,
expr: regex, Expr: regex,
}) })
} }
@ -243,12 +226,10 @@ func (s *suite) run() {
s.fmt.Summary() s.fmt.Summary()
} }
func (s *suite) runStep(step *gherkin.Step) (err error) { func (s *suite) matchStep(step *gherkin.Step) *StepDef {
var match *stepMatchHandler
var args []*Arg
for _, h := range s.stepHandlers { for _, h := range s.stepHandlers {
if m := h.expr.FindStringSubmatch(step.Text); len(m) > 0 { if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 {
match = h var args []*Arg
for _, a := range m[1:] { for _, a := range m[1:] {
args = append(args, &Arg{value: a}) args = append(args, &Arg{value: a})
} }
@ -258,12 +239,18 @@ func (s *suite) runStep(step *gherkin.Step) (err error) {
if step.PyString != nil { if step.PyString != nil {
args = append(args, &Arg{value: step.PyString}) args = append(args, &Arg{value: step.PyString})
} }
break h.Args = args
return h
} }
} }
return nil
}
func (s *suite) runStep(step *gherkin.Step) (err error) {
match := s.matchStep(step)
if match == nil { if match == nil {
s.fmt.Undefined(step) s.fmt.Undefined(step)
return errPending return ErrUndefined
} }
defer func() { defer func() {
@ -273,7 +260,7 @@ func (s *suite) runStep(step *gherkin.Step) (err error) {
} }
}() }()
if err = match.handler.HandleStep(args...); err != nil { if err = match.Handler.HandleStep(match.Args...); err != nil {
s.fmt.Failed(step, match, err) s.fmt.Failed(step, match, err)
} else { } else {
s.fmt.Passed(step, match) s.fmt.Passed(step, match)
@ -281,9 +268,9 @@ func (s *suite) runStep(step *gherkin.Step) (err error) {
return return
} }
func (s *suite) runSteps(steps []*gherkin.Step) (st Status) { func (s *suite) runSteps(steps []*gherkin.Step) (err error) {
for _, step := range steps { for _, step := range steps {
if st == Failed || st == Undefined { if err != nil {
s.fmt.Skipped(step) s.fmt.Skipped(step)
continue continue
} }
@ -293,19 +280,11 @@ func (s *suite) runSteps(steps []*gherkin.Step) (st Status) {
h.HandleBeforeStep(step) h.HandleBeforeStep(step)
} }
err := s.runStep(step) err = s.runStep(step)
switch err {
case errPending:
st = Undefined
case nil:
st = Passed
default:
st = Failed
}
// run after step handlers // run after step handlers
for _, h := range s.afterStepHandlers { for _, h := range s.afterStepHandlers {
h.HandleAfterStep(step, st) h.HandleAfterStep(step, err)
} }
} }
return return
@ -317,39 +296,52 @@ 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)
}
// set steps to scenario
scenario.Steps = steps
if err = s.runScenario(scenario); err != nil && err != ErrUndefined {
s.failed = true
if cfg.stopOnFailure {
return
}
}
}
return
}
func (s *suite) runFeature(f *gherkin.Feature) { func (s *suite) runFeature(f *gherkin.Feature) {
s.fmt.Node(f) s.fmt.Node(f)
for _, scenario := range f.Scenarios { for _, scenario := range f.Scenarios {
var status Status var err error
// handle scenario outline differently
// run before scenario handlers if scenario.Outline != nil {
for _, h := range s.beforeScenarioHandlers { err = s.runOutline(scenario)
h.HandleBeforeScenario(scenario) } else {
err = s.runScenario(scenario)
} }
if err != nil && err != ErrUndefined {
// background
if f.Background != nil {
s.fmt.Node(f.Background)
status = s.runSteps(f.Background.Steps)
}
// scenario
s.fmt.Node(scenario)
switch {
case status == Failed:
s.skipSteps(scenario.Steps)
case status == Undefined:
s.skipSteps(scenario.Steps)
case status == Passed || status == Invalid:
status = s.runSteps(scenario.Steps)
}
// run after scenario handlers
for _, h := range s.afterScenarioHandlers {
h.HandleAfterScenario(scenario, status)
}
if status == Failed {
s.failed = true s.failed = true
if cfg.stopOnFailure { if cfg.stopOnFailure {
return return
@ -358,16 +350,47 @@ func (s *suite) runFeature(f *gherkin.Feature) {
} }
} }
func (s *suite) runScenario(scenario *gherkin.Scenario) (err error) {
// run before scenario handlers
for _, h := range s.beforeScenarioHandlers {
h.HandleBeforeScenario(scenario)
}
// background
if scenario.Feature.Background != nil {
s.fmt.Node(scenario.Feature.Background)
err = s.runSteps(scenario.Feature.Background.Steps)
}
// scenario
s.fmt.Node(scenario)
switch err {
case ErrUndefined:
s.skipSteps(scenario.Steps)
case nil:
err = s.runSteps(scenario.Steps)
default:
s.skipSteps(scenario.Steps)
}
// run after scenario handlers
for _, h := range s.afterScenarioHandlers {
h.HandleAfterScenario(scenario, err)
}
return
}
func (st *suite) printStepDefinitions() { func (st *suite) printStepDefinitions() {
var longest int var longest int
for _, def := range st.stepHandlers { for _, def := range st.stepHandlers {
if longest < len(def.expr.String()) { if longest < len(def.Expr.String()) {
longest = len(def.expr.String()) longest = len(def.Expr.String())
} }
} }
for _, def := range st.stepHandlers { for _, def := range st.stepHandlers {
location := runtime.FuncForPC(reflect.ValueOf(def.handler).Pointer()).Name() location := runtime.FuncForPC(reflect.ValueOf(def.Handler).Pointer()).Name()
fmt.Println(cl(def.expr.String(), yellow)+s(longest-len(def.expr.String())), cl("# "+location, black)) fmt.Println(cl(def.Expr.String(), yellow)+s(longest-len(def.Expr.String())), cl("# "+location, black))
} }
if len(st.stepHandlers) == 0 { if len(st.stepHandlers) == 0 {
fmt.Println("there were no contexts registered, could not find any step definition..") fmt.Println("there were no contexts registered, could not find any step definition..")

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

@ -121,14 +121,14 @@ func (s *suiteContext) iAmListeningToSuiteEvents(args ...*Arg) error {
s.testedSuite.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) { s.testedSuite.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) {
s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}}) s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}})
})) }))
s.testedSuite.AfterScenario(AfterScenarioHandlerFunc(func(scenario *gherkin.Scenario, status Status) { s.testedSuite.AfterScenario(AfterScenarioHandlerFunc(func(scenario *gherkin.Scenario, err error) {
s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, status}}) s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, err}})
})) }))
s.testedSuite.BeforeStep(BeforeStepHandlerFunc(func(step *gherkin.Step) { s.testedSuite.BeforeStep(BeforeStepHandlerFunc(func(step *gherkin.Step) {
s.events = append(s.events, &firedEvent{"BeforeStep", []interface{}{step}}) s.events = append(s.events, &firedEvent{"BeforeStep", []interface{}{step}})
})) }))
s.testedSuite.AfterStep(AfterStepHandlerFunc(func(step *gherkin.Step, status Status) { s.testedSuite.AfterStep(AfterStepHandlerFunc(func(step *gherkin.Step, err error) {
s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, status}}) s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, err}})
})) }))
return nil return nil
} }