From df26aa1c1c2c14d2987b99c94c4758385fe6303d Mon Sep 17 00:00:00 2001 From: gedi Date: Fri, 19 Jun 2015 13:55:27 +0300 Subject: [PATCH] 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 --- events.go | 16 +- features/events.feature | 8 +- features/load.feature | 12 +- fmt.go | 14 +- fmt_pretty.go | 320 +++++++++++++++++++++++++++------------ fmt_test.go | 8 +- gherkin/gherkin.go | 38 +++-- gherkin/lexer.go | 63 ++++++-- gherkin/scenario_test.go | 28 ++-- gherkin/token.go | 64 +++----- suite.go | 193 ++++++++++++----------- suite_test.go | 8 +- 12 files changed, 481 insertions(+), 291 deletions(-) diff --git a/events.go b/events.go index 776407b..ece2274 100644 --- a/events.go +++ b/events.go @@ -56,34 +56,34 @@ func (f BeforeStepHandlerFunc) HandleBeforeStep(step *gherkin.Step) { // in Suite to be executed after every step // which will be run type AfterStepHandler interface { - HandleAfterStep(step *gherkin.Step, status Status) + HandleAfterStep(step *gherkin.Step, err error) } // AfterStepHandlerFunc is a function implementing // 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 // for after every step which is run by suite -func (f AfterStepHandlerFunc) HandleAfterStep(step *gherkin.Step, status Status) { - f(step, status) +func (f AfterStepHandlerFunc) HandleAfterStep(step *gherkin.Step, err error) { + f(step, err) } // AfterScenarioHandler can be registered // in Suite to be executed after every scenario // which will be run type AfterScenarioHandler interface { - HandleAfterScenario(scenario *gherkin.Scenario, status Status) + HandleAfterScenario(scenario *gherkin.Scenario, err error) } // AfterScenarioHandlerFunc is a function implementing // 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 // for after every scenario which is run by suite -func (f AfterScenarioHandlerFunc) HandleAfterScenario(scenario *gherkin.Scenario, status Status) { - f(scenario, status) +func (f AfterScenarioHandlerFunc) HandleAfterScenario(scenario *gherkin.Scenario, err error) { + f(scenario, err) } // AfterSuiteHandler can be registered diff --git a/features/events.feature b/features/events.feature index c978519..1c7bc66 100644 --- a/features/events.feature +++ b/features/events.feature @@ -27,8 +27,8 @@ Feature: suite events When I run feature suite Then these events had to be fired for a number of times: | BeforeSuite | 1 | - | BeforeScenario | 4 | - | BeforeStep | 13 | - | AfterStep | 13 | - | AfterScenario | 4 | + | BeforeScenario | 6 | + | BeforeStep | 19 | + | AfterStep | 19 | + | AfterScenario | 6 | | AfterSuite | 1 | diff --git a/features/load.feature b/features/load.feature index f65045e..a0feef2 100644 --- a/features/load.feature +++ b/features/load.feature @@ -21,10 +21,16 @@ Feature: load features features/load.feature """ - Scenario: load a feature file with a specified scenario - Given a feature path "features/load.feature:6" + Scenario Outline: loaded feature should have a number of scenarios + Given a feature path "" When I parse features - Then I should have 1 scenario registered + Then I should have 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 Given a feature path "features/load.feature" diff --git a/fmt.go b/fmt.go index 91ce520..10c045f 100644 --- a/fmt.go +++ b/fmt.go @@ -10,8 +10,8 @@ import ( // output summary presentation type Formatter interface { Node(interface{}) - Failed(*gherkin.Step, *stepMatchHandler, error) - Passed(*gherkin.Step, *stepMatchHandler) + Failed(*gherkin.Step, *StepDef, error) + Passed(*gherkin.Step, *StepDef) Skipped(*gherkin.Step) Undefined(*gherkin.Step) Summary() @@ -20,9 +20,9 @@ type Formatter interface { // failed represents a failed step data structure // with all necessary references type failed struct { - step *gherkin.Step - handler *stepMatchHandler - err error + step *gherkin.Step + def *StepDef + err error } func (f failed) line() string { @@ -41,8 +41,8 @@ func (f failed) line() string { // passed represents a successful step data structure // with all necessary references type passed struct { - step *gherkin.Step - handler *stepMatchHandler + step *gherkin.Step + def *StepDef } // skipped represents a skipped step data structure diff --git a/fmt_pretty.go b/fmt_pretty.go index c22c9ac..d7acd17 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "reflect" + "regexp" "runtime" "strings" "time" @@ -17,6 +18,8 @@ func init() { }) } +var outlinePlaceholderRegexp *regexp.Regexp = regexp.MustCompile("<[^>]+>") + // a built in default pretty formatter type pretty struct { feature *gherkin.Feature @@ -25,6 +28,11 @@ type pretty struct { background *gherkin.Background scenario *gherkin.Scenario + // outline + outlineExamples int + outlineNumSteps int + outlineSteps []interface{} + // summary started time.Time features []*gherkin.Feature @@ -51,33 +59,31 @@ func (f *pretty) Node(node interface{}) { f.scenario = nil f.background = nil 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) case *gherkin.Background: // do not repeat background for the same feature if f.background == nil && f.scenario == nil { f.background = t - // determine comment position based on step 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) - } - } + f.commentPos = longestStep(t.Steps, t.Token.Length()) // 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: f.scenario = t - // determine comment position based on step 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) + 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("Scenario: ", white) + t.Title - text += s(f.commentPos-len(t.Token.Text)+1) + f.line(t.Token) + text := s(t.Token.Indent) + bcl(t.Token.Keyword+": ", white) + t.Title + text += s(f.commentPos-t.Token.Length()+1) + f.line(t.Token) fmt.Println("\n" + text) } } @@ -93,8 +99,22 @@ func (f *pretty) Summary() { } if len(failedScenarios) > 0 { fmt.Println("\n--- " + cl("Failed scenarios:", red) + "\n") + var unique []string 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 @@ -142,21 +162,129 @@ func (f *pretty) Summary() { fmt.Println(elapsed) } -func (f *pretty) printStep(stepAction interface{}) { - var c color - var step *gherkin.Step - var h *stepMatchHandler - var err error - var suffix, prefix string +func (f *pretty) printOutlineExample() { + var failed error + clr := green + tbl := f.scenario.Outline.Examples + firstExample := f.outlineExamples == len(tbl.Rows)-1 + 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) { case *passed: step = typ.step - h = typ.handler + def = typ.def c = green case *failed: step = typ.step - h = typ.handler + def = typ.def err = typ.err c = red case *skipped: @@ -168,56 +296,33 @@ func (f *pretty) printStep(stepAction interface{}) { default: 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 if f.scenario == nil && step.Background != f.background { return } - if h != nil { - if m := (h.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 { - suffix += cl(step.Text[pos:m[i]], c) - } 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) + 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() + f.outlineExamples -= 1 } - // use reflect to get step handler function name - 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) + return // wait till example steps } - prefix = s(step.Token.Indent) - 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) - } + f.printStep(step, def, c) if err != nil { 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 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])) + 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 i, col := range row { if longest[i] < len(col) { @@ -234,38 +378,16 @@ func (f *pretty) printTable(t *gherkin.Table, c color) { } } } - for _, row := range t.Rows { - for i, col := range row { - cols[i] = col + s(longest[i]-len(col)) + return longest +} + +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)) } -} - -// 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) + return ret } diff --git a/fmt_test.go b/fmt_test.go index 60c8dce..489e433 100644 --- a/fmt_test.go +++ b/fmt_test.go @@ -23,8 +23,8 @@ func (f *testFormatter) Node(node interface{}) { func (f *testFormatter) Summary() {} -func (f *testFormatter) Passed(step *gherkin.Step, match *stepMatchHandler) { - f.passed = append(f.passed, &passed{step: step, handler: match}) +func (f *testFormatter) Passed(step *gherkin.Step, match *StepDef) { + f.passed = append(f.passed, &passed{step: step, def: match}) } 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}) } -func (f *testFormatter) Failed(step *gherkin.Step, match *stepMatchHandler, err error) { - f.failed = append(f.failed, &failed{step: step, handler: match, err: err}) +func (f *testFormatter) Failed(step *gherkin.Step, match *StepDef, err error) { + f.failed = append(f.failed, &failed{step: step, def: match, err: err}) } diff --git a/gherkin/gherkin.go b/gherkin/gherkin.go index d7ed7ec..3ce4ff8 100644 --- a/gherkin/gherkin.go +++ b/gherkin/gherkin.go @@ -88,6 +88,17 @@ func (t Tags) Has(tag Tag) bool { 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 @@ -100,11 +111,11 @@ func (t Tags) Has(tag Tag) bool { // initialization tasks type Scenario struct { *Token - Title string - Steps []*Step - Tags Tags - Examples *Table - Feature *Feature + Title string + Steps []*Step + Tags Tags + Outline *Outline + Feature *Feature } // Background steps are run before every scenario @@ -154,9 +165,8 @@ func (p *PyString) String() string { // step definition or outline scenario type Table struct { *Token - OutlineScenario *Scenario - Step *Step - Rows [][]string + Step *Step + Rows [][]string } 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", }, " "), 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 } - 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 } diff --git a/gherkin/lexer.go b/gherkin/lexer.go index 8ed1be6..26c0acf 100644 --- a/gherkin/lexer.go +++ b/gherkin/lexer.go @@ -21,6 +21,30 @@ var matchers = map[string]*regexp.Regexp{ "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 { reader *bufio.Reader lines int @@ -36,8 +60,9 @@ 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, + Type: EOF, + Line: l.lines + 1, + Keyword: keywords[EOF], } } l.lines++ @@ -45,8 +70,9 @@ func (l *lexer) read() *Token { // newline if len(line) == 0 { return &Token{ - Type: NEW_LINE, - Line: l.lines, + Type: NEW_LINE, + Line: l.lines, + Keyword: keywords[NEW_LINE], } } // comment @@ -59,15 +85,17 @@ func (l *lexer) read() *Token { 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, + Type: PYSTRING, + Indent: len(m[1]), + Line: l.lines, + Text: line, + Keyword: keywords[PYSTRING], } } // step @@ -91,6 +119,7 @@ func (l *lexer) read() *Token { case "But": tok.Type = BUT } + tok.Keyword = keywords[tok.Type] return tok } // scenario @@ -102,6 +131,7 @@ func (l *lexer) read() *Token { Value: strings.TrimSpace(m[2]), Text: line, Comment: strings.Trim(m[3], " #"), + Keyword: keywords[SCENARIO], } } // background @@ -112,6 +142,7 @@ func (l *lexer) read() *Token { Line: l.lines, Text: line, Comment: strings.Trim(m[2], " #"), + Keyword: keywords[BACKGROUND], } } // feature @@ -123,6 +154,7 @@ func (l *lexer) read() *Token { Value: strings.TrimSpace(m[2]), Text: line, Comment: strings.Trim(m[3], " #"), + Keyword: keywords[FEATURE], } } // tags @@ -134,6 +166,7 @@ func (l *lexer) read() *Token { Value: strings.TrimSpace(m[2]), Text: line, Comment: strings.Trim(m[3], " #"), + Keyword: keywords[TAGS], } } // table row @@ -145,6 +178,7 @@ func (l *lexer) read() *Token { Value: strings.TrimSpace(m[2]), Text: line, Comment: strings.Trim(m[3], " #"), + Keyword: keywords[TABLE_ROW], } } // scenario outline @@ -156,6 +190,7 @@ func (l *lexer) read() *Token { Value: strings.TrimSpace(m[2]), Text: line, Comment: strings.Trim(m[3], " #"), + Keyword: keywords[SCENARIO_OUTLINE], } } // examples @@ -166,15 +201,17 @@ func (l *lexer) read() *Token { 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, + Type: TEXT, + Line: l.lines, + Value: text, + Indent: len(line) - len(text), + Text: line, + Keyword: keywords[TEXT], } } diff --git a/gherkin/scenario_test.go b/gherkin/scenario_test.go index 40cd2e0..70b2b34 100644 --- a/gherkin/scenario_test.go +++ b/gherkin/scenario_test.go @@ -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 { for _, stp := range s.Steps { 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) { - if s.Examples == nil { + if s.Outline.Examples == nil { 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) } - 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)) } - for i, col := range s.Examples.Rows[num] { + 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]) } @@ -64,11 +74,11 @@ func Test_parse_scenario_outline(t *testing.T) { TABLE_ROW, }, t) - s.assertStep(`I am in a directory "test"`, t) - s.assertStep(`I have a file named "foo"`, t) - s.assertStep(`I have a file named "bar"`, t) - s.assertStep(`I run "ls" with options ""`, t) - s.assertStep(`I should see ""`, t) + s.assertOutlineStep(`I am in a directory "test"`, t) + s.assertOutlineStep(`I have a file named "foo"`, t) + s.assertOutlineStep(`I have a file named "bar"`, t) + s.assertOutlineStep(`I run "ls" with options ""`, t) + s.assertOutlineStep(`I should see ""`, t) s.assertExampleRow(t, 0, "options", "result") s.assertExampleRow(t, 1, "-t", "bar foo") diff --git a/gherkin/token.go b/gherkin/token.go index 42933d5..da21b22 100644 --- a/gherkin/token.go +++ b/gherkin/token.go @@ -1,29 +1,26 @@ package gherkin +import ( + "strings" + "unicode" +) + type TokenType int const ( ILLEGAL TokenType = iota - - specials COMMENT NEW_LINE EOF - - elements TEXT TAGS TABLE_ROW PYSTRING - - keywords FEATURE BACKGROUND SCENARIO SCENARIO_OUTLINE EXAMPLES - - steps GIVEN WHEN THEN @@ -31,54 +28,22 @@ const ( BUT ) +// String gives a string representation of token type func (t TokenType) String() string { - switch 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" + 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 { @@ -87,3 +52,12 @@ func (t *Token) OfType(all ...TokenType) bool { } return false } + +// Length gives a token text length with indentation +// and keyword, but without comment +func (t *Token) Length() int { + if pos := strings.Index(t.Text, "#"); pos != -1 { + return len(strings.TrimRightFunc(t.Text[:pos], unicode.IsSpace)) + } + return len(t.Text) +} diff --git a/suite.go b/suite.go index 304cdba..1cc5037 100644 --- a/suite.go +++ b/suite.go @@ -7,6 +7,7 @@ import ( "reflect" "regexp" "runtime" + "strings" "github.com/DATA-DOG/godog/gherkin" ) @@ -20,30 +21,6 @@ type Regexp interface{} // or a step handler 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 // registered as step definitions in godog // @@ -70,11 +47,17 @@ func (f StepHandlerFunc) HandleStep(args ...*Arg) error { 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 { - handler StepHandler - expr *regexp.Regexp +// StepDef is a registered step definition +// contains a StepHandler, a regexp which +// 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 @@ -91,7 +74,7 @@ type Suite interface { } type suite struct { - stepHandlers []*stepMatchHandler + stepHandlers []*StepDef features []*gherkin.Feature fmt Formatter @@ -151,9 +134,9 @@ func (s *suite) Step(expr Regexp, h Handler) { default: panic(fmt.Sprintf("expecting handler to satisfy StepHandler interface, got type: %T", h)) } - s.stepHandlers = append(s.stepHandlers, &stepMatchHandler{ - handler: handler, - expr: regex, + s.stepHandlers = append(s.stepHandlers, &StepDef{ + Handler: handler, + Expr: regex, }) } @@ -243,12 +226,10 @@ func (s *suite) run() { s.fmt.Summary() } -func (s *suite) runStep(step *gherkin.Step) (err error) { - var match *stepMatchHandler - var args []*Arg +func (s *suite) matchStep(step *gherkin.Step) *StepDef { for _, h := range s.stepHandlers { - if m := h.expr.FindStringSubmatch(step.Text); len(m) > 0 { - match = h + if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 { + var args []*Arg for _, a := range m[1:] { args = append(args, &Arg{value: a}) } @@ -258,12 +239,18 @@ func (s *suite) runStep(step *gherkin.Step) (err error) { if step.PyString != nil { 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 { s.fmt.Undefined(step) - return errPending + return ErrUndefined } 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) } else { s.fmt.Passed(step, match) @@ -281,9 +268,9 @@ func (s *suite) runStep(step *gherkin.Step) (err error) { return } -func (s *suite) runSteps(steps []*gherkin.Step) (st Status) { +func (s *suite) runSteps(steps []*gherkin.Step) (err error) { for _, step := range steps { - if st == Failed || st == Undefined { + if err != nil { s.fmt.Skipped(step) continue } @@ -293,19 +280,11 @@ func (s *suite) runSteps(steps []*gherkin.Step) (st Status) { h.HandleBeforeStep(step) } - err := s.runStep(step) - switch err { - case errPending: - st = Undefined - case nil: - st = Passed - default: - st = Failed - } + err = s.runStep(step) // run after step handlers for _, h := range s.afterStepHandlers { - h.HandleAfterStep(step, st) + h.HandleAfterStep(step, err) } } 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) { s.fmt.Node(f) for _, scenario := range f.Scenarios { - var status Status - - // run before scenario handlers - for _, h := range s.beforeScenarioHandlers { - h.HandleBeforeScenario(scenario) + var err error + // handle scenario outline differently + if scenario.Outline != nil { + err = s.runOutline(scenario) + } else { + err = s.runScenario(scenario) } - - // 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 { + if err != nil && err != ErrUndefined { s.failed = true if cfg.stopOnFailure { 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() { var longest int for _, def := range st.stepHandlers { - if longest < len(def.expr.String()) { - longest = len(def.expr.String()) + if longest < len(def.Expr.String()) { + longest = len(def.Expr.String()) } } for _, def := range st.stepHandlers { - 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)) + 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)) } if len(st.stepHandlers) == 0 { fmt.Println("there were no contexts registered, could not find any step definition..") diff --git a/suite_test.go b/suite_test.go index ef5ebdd..a022e3c 100644 --- a/suite_test.go +++ b/suite_test.go @@ -121,14 +121,14 @@ func (s *suiteContext) iAmListeningToSuiteEvents(args ...*Arg) error { s.testedSuite.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) { s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}}) })) - s.testedSuite.AfterScenario(AfterScenarioHandlerFunc(func(scenario *gherkin.Scenario, status Status) { - s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, status}}) + s.testedSuite.AfterScenario(AfterScenarioHandlerFunc(func(scenario *gherkin.Scenario, err error) { + s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, err}}) })) s.testedSuite.BeforeStep(BeforeStepHandlerFunc(func(step *gherkin.Step) { s.events = append(s.events, &firedEvent{"BeforeStep", []interface{}{step}}) })) - s.testedSuite.AfterStep(AfterStepHandlerFunc(func(step *gherkin.Step, status Status) { - s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, status}}) + s.testedSuite.AfterStep(AfterStepHandlerFunc(func(step *gherkin.Step, err error) { + s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, err}}) })) return nil }