support gherkin scenario outline and examples

read scenario outline related tokens in gherkin lexer

* e8b71e6 test lexing of scenario outline with examples
* 13f73f2 update gherkin parser to parse scenario outline with examples
* cfad4fe test scenario outline persing
Этот коммит содержится в:
gedi 2015-06-11 10:48:26 +03:00
родитель 1cc5fde508
коммит 4afb53d310
6 изменённых файлов: 247 добавлений и 51 удалений

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

@ -87,11 +87,21 @@ func (t Tags) Has(tag Tag) bool {
}
// 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 {
Title string
Steps []*Step
Tags Tags
Comment string
Title string
Steps []*Step
Tags Tags
Examples *Table
Comment string
}
// Background steps are run before every scenario
@ -243,34 +253,56 @@ func (p *parser) parseFeature() (ft *Feature, err error) {
}
// there may be tags before scenario
sc := &Scenario{}
sc.Tags = append(sc.Tags, ft.Tags...)
var tags Tags
tags = append(tags, ft.Tags...)
if tok.Type == TAGS {
for _, t := range p.parseTags() {
if !sc.Tags.Has(t) {
sc.Tags = append(sc.Tags, t)
if !tags.Has(t) {
tags = append(tags, t)
}
}
tok = p.peek()
}
// there must be a scenario otherwise
if tok.Type != SCENARIO {
return ft, p.err("expected a scenario, but got '"+tok.Type.String()+"' instead", tok.Line)
// there must be a scenario or scenario outline otherwise
if !tok.OfType(SCENARIO, SCENARIO_OUTLINE) {
return ft, p.err("expected a scenario or scenario outline, but got '"+tok.Type.String()+"' instead", tok.Line)
}
sc.Title = tok.Value
sc.Comment = tok.Comment
p.next() // jump to scenario steps
if sc.Steps, err = p.parseSteps(); err != nil {
scenario, err := p.parseScenario()
if err != nil {
return ft, err
}
ft.Scenarios = append(ft.Scenarios, sc)
scenario.Tags = tags
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, Comment: tok.Comment}
if s.Steps, err = p.parseSteps(); err != nil {
return s, err
}
if examples := p.peek(); examples.Type == EXAMPLES {
p.next() // jump over the peeked token
peek := p.peek()
if peek.Type != TABLE_ROW {
return s, p.err(strings.Join([]string{
"expected a table row,",
"but got '" + peek.Type.String() + "' instead, for scenario outline examples",
}, " "), examples.Line)
}
if s.Examples, err = p.parseTable(); err != nil {
return s, err
}
}
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, Comment: tok.Comment}
@ -294,11 +326,11 @@ func (p *parser) parseSteps() (steps []*Step, err error) {
tok = p.peek()
switch tok.Type {
case PYSTRING:
if err := p.parsePystring(step); err != nil {
if step.PyString, err = p.parsePystring(); err != nil {
return steps, err
}
case TABLE_ROW:
if err := p.parseTable(step); err != nil {
if step.Table, err = p.parseTable(); err != nil {
return steps, err
}
default:
@ -312,7 +344,7 @@ func (p *parser) parseSteps() (steps []*Step, err error) {
return steps, nil
}
func (p *parser) parsePystring(s *Step) error {
func (p *parser) parsePystring() (*PyString, error) {
var tok *Token
started := p.next() // skip the start of pystring
var lines []string
@ -320,27 +352,28 @@ func (p *parser) parsePystring(s *Step) error {
lines = append(lines, tok.Text)
}
if tok.Type == EOF {
return fmt.Errorf("pystring which was opened on %s:%d was not closed", p.path, started.Line)
return nil, fmt.Errorf("pystring which was opened on %s:%d was not closed", p.path, started.Line)
}
s.PyString = &PyString{Body: strings.Join(lines, "\n")}
return nil
return &PyString{
Body: strings.Join(lines, "\n"),
}, nil
}
func (p *parser) parseTable(s *Step) error {
s.Table = &Table{}
func (p *parser) parseTable() (*Table, error) {
tbl := &Table{}
for row := p.peek(); row.Type == TABLE_ROW; 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(s.Table.rows) > 0 && len(s.Table.rows[0]) != len(cols) {
return p.err("table row has not the same number of columns compared to previous row", row.Line)
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)
}
s.Table.rows = append(s.Table.rows, cols)
tbl.rows = append(tbl.rows, cols)
p.next() // jump over the peeked token
}
return nil
return tbl, nil
}
func (p *parser) parseTags() (tags Tags) {

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

@ -9,14 +9,16 @@ import (
)
var matchers = map[string]*regexp.Regexp{
"feature": regexp.MustCompile("^(\\s*)Feature:\\s*([^#]*)(#.*)?"),
"scenario": regexp.MustCompile("^(\\s*)Scenario:\\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*)\\|([^#]*)(#.*)?"),
"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*)\\|([^#]*)(#.*)?"),
}
type lexer struct {
@ -145,6 +147,27 @@ func (l *lexer) read() *Token {
Comment: strings.Trim(m[3], " #"),
}
}
// scenario outline
if m := matchers["scenario_outline"].FindStringSubmatch(line); len(m) > 0 {
return &Token{
Type: SCENARIO_OUTLINE,
Indent: len(m[1]),
Line: l.lines - 1,
Value: strings.TrimSpace(m[2]),
Text: line,
Comment: strings.Trim(m[3], " #"),
}
}
// examples
if m := matchers["examples"].FindStringSubmatch(line); len(m) > 0 {
return &Token{
Type: EXAMPLES,
Indent: len(m[1]),
Line: l.lines - 1,
Text: line,
Comment: strings.Trim(m[2], " #"),
}
}
// text
text := strings.TrimLeftFunc(line, unicode.IsSpace)
return &Token{

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

@ -5,7 +5,7 @@ import (
"testing"
)
var lexerSamples = map[string]string{
var testLexerSamples = map[string]string{
"feature": `Feature: gherkin lexer
in order to run features
as gherkin lexer
@ -27,10 +27,22 @@ var lexerSamples = map[string]string{
| 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(lexerSamples["feature"]))
l := newLexer(strings.NewReader(testLexerSamples["feature"]))
tok := l.read()
if tok.Type != FEATURE {
t.Fatalf("Expected a 'feature' type, but got: '%s'", tok.Type)
@ -99,16 +111,16 @@ func Test_feature_read(t *testing.T) {
func Test_minimal_feature(t *testing.T) {
file := strings.Join([]string{
lexerSamples["feature"] + "\n",
testLexerSamples["feature"] + "\n",
indent(2, lexerSamples["background"]),
indent(4, lexerSamples["step_given"]) + "\n",
indent(2, testLexerSamples["background"]),
indent(4, testLexerSamples["step_given"]) + "\n",
indent(2, lexerSamples["comment"]),
indent(2, lexerSamples["scenario"]),
indent(4, lexerSamples["step_given"]),
indent(4, lexerSamples["step_when"]),
indent(4, lexerSamples["step_then"]),
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))
@ -142,9 +154,9 @@ func Test_minimal_feature(t *testing.T) {
func Test_table_row_reading(t *testing.T) {
file := strings.Join([]string{
indent(2, lexerSamples["background"]),
indent(4, lexerSamples["step_given_table"]),
indent(4, lexerSamples["step_given"]),
indent(2, testLexerSamples["background"]),
indent(4, testLexerSamples["step_given_table"]),
indent(4, testLexerSamples["step_given"]),
}, "\n")
l := newLexer(strings.NewReader(file))
@ -179,3 +191,31 @@ func Test_table_row_reading(t *testing.T) {
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{
SCENARIO_OUTLINE,
GIVEN,
AND,
AND,
WHEN,
THEN,
NEW_LINE,
EXAMPLES,
TABLE_ROW,
TABLE_ROW,
TABLE_ROW,
}
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])
}
}
}

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

@ -35,7 +35,9 @@ func Test_parse_feature_file(t *testing.T) {
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"),
indent(2, "Scenario: user is able to reset his password") + "\n",
// scenario outline
testLexerSamples["scenario_outline_with_examples"],
}, "\n")
p := &parser{
@ -79,9 +81,22 @@ func Test_parse_feature_file(t *testing.T) {
TAGS,
SCENARIO,
NEW_LINE,
SCENARIO_OUTLINE,
GIVEN,
AND,
AND,
WHEN,
THEN,
NEW_LINE,
EXAMPLES,
TABLE_ROW,
TABLE_ROW,
TABLE_ROW,
}, t)
ft.assertHasNumScenarios(3, t)
ft.assertHasNumScenarios(4, t)
ft.Scenarios[0].assertHasNumTags(2, t)
ft.Scenarios[0].assertHasTag("global-one", t)
@ -96,4 +111,6 @@ func Test_parse_feature_file(t *testing.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)
}

77
gherkin/scenario_test.go Обычный файл
Просмотреть файл

@ -0,0 +1,77 @@
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) assertStep(text string, t *testing.T) *Step {
for _, stp := range s.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) assertExampleRow(t *testing.T, num int, cols ...string) {
if s.Examples == nil {
t.Fatalf("outline scenario '%s' has no examples", s.Title)
}
if len(s.Examples.rows) <= num {
t.Fatalf("outline scenario '%s' table has no row: %d", s.Title, num)
}
if len(s.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] {
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",
ast: newAST(),
}
s, err := p.parseScenario()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
s.assertTitle("ls supports kinds of options", t)
p.ast.assertMatchesTypes([]TokenType{
SCENARIO_OUTLINE,
GIVEN,
AND,
AND,
WHEN,
THEN,
NEW_LINE,
EXAMPLES,
TABLE_ROW,
TABLE_ROW,
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 "<options>"`, t)
s.assertStep(`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")
}

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

@ -20,6 +20,8 @@ const (
FEATURE
BACKGROUND
SCENARIO
SCENARIO_OUTLINE
EXAMPLES
steps
GIVEN
@ -51,6 +53,10 @@ func (t TokenType) String() string {
return "background"
case SCENARIO:
return "scenario"
case SCENARIO_OUTLINE:
return "scenario outline"
case EXAMPLES:
return "examples"
case GIVEN:
return "given step"
case WHEN: