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
Этот коммит содержится в:
родитель
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
Обычный файл
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:
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче