parse scenario or background steps
Этот коммит содержится в:
родитель
e7ae301947
коммит
5a3b979e01
3 изменённых файлов: 166 добавлений и 19 удалений
59
gherkin/ast.go
Обычный файл
59
gherkin/ast.go
Обычный файл
|
@ -0,0 +1,59 @@
|
||||||
|
package gherkin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/l3pp4rd/behat/gherkin/lexer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
next, prev *item
|
||||||
|
value *lexer.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
type AST struct {
|
||||||
|
head, tail *item
|
||||||
|
mut *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAST() *AST {
|
||||||
|
return &AST{mut: &sync.Mutex{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AST) addTail(t *lexer.Token) *item {
|
||||||
|
l.mut.Lock()
|
||||||
|
defer l.mut.Unlock()
|
||||||
|
|
||||||
|
it := &item{next: nil, prev: l.tail, value: t}
|
||||||
|
if l.head == nil {
|
||||||
|
l.head = it
|
||||||
|
} else {
|
||||||
|
l.tail.next = it
|
||||||
|
}
|
||||||
|
l.tail = it
|
||||||
|
return l.tail
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AST) addBefore(t *lexer.Token, i *item) *item {
|
||||||
|
l.mut.Lock()
|
||||||
|
defer l.mut.Unlock()
|
||||||
|
|
||||||
|
it := &item{next: i, prev: i.prev, value: t}
|
||||||
|
i.prev = it
|
||||||
|
if it.prev == nil {
|
||||||
|
l.head = it
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AST) addAfter(t *lexer.Token, i *item) *item {
|
||||||
|
l.mut.Lock()
|
||||||
|
defer l.mut.Unlock()
|
||||||
|
|
||||||
|
it := &item{next: i.next, prev: i, value: t}
|
||||||
|
i.next = it
|
||||||
|
if it.next == nil {
|
||||||
|
l.tail = it
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ const (
|
||||||
FEATURE
|
FEATURE
|
||||||
BACKGROUND
|
BACKGROUND
|
||||||
SCENARIO
|
SCENARIO
|
||||||
|
|
||||||
|
steps
|
||||||
GIVEN
|
GIVEN
|
||||||
WHEN
|
WHEN
|
||||||
THEN
|
THEN
|
||||||
|
|
124
gherkin/parse.go
124
gherkin/parse.go
|
@ -2,7 +2,8 @@ package gherkin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/l3pp4rd/behat/gherkin/lexer"
|
"github.com/l3pp4rd/behat/gherkin/lexer"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
type Tag string
|
type Tag string
|
||||||
|
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
Title string
|
||||||
Steps []*Step
|
Steps []*Step
|
||||||
Tags []Tag
|
Tags []Tag
|
||||||
}
|
}
|
||||||
|
@ -40,64 +42,148 @@ type Feature struct {
|
||||||
Scenarios []*Scenario
|
Scenarios []*Scenario
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNotFeature = errors.New("expected a file to begin with a feature definition")
|
var steps = []lexer.TokenType{
|
||||||
|
lexer.GIVEN,
|
||||||
|
lexer.WHEN,
|
||||||
|
lexer.THEN,
|
||||||
|
lexer.AND,
|
||||||
|
lexer.BUT,
|
||||||
|
}
|
||||||
|
|
||||||
var ErrEmpty = errors.New("the feature file is empty")
|
var ErrEmpty = errors.New("the feature file is empty")
|
||||||
var ErrTagsNextToFeature = errors.New("tags must be a single line next to a feature definition")
|
|
||||||
var ErrSingleBackground = errors.New("there can only be a single background section")
|
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
lx *lexer.Lexer
|
lx *lexer.Lexer
|
||||||
|
path string
|
||||||
|
ast *AST
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(r io.Reader) (*Feature, error) {
|
func Parse(path string) (*Feature, error) {
|
||||||
return (parser{lx: lexer.New(r)}).parseFeature()
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return (&parser{
|
||||||
|
lx: lexer.New(file),
|
||||||
|
path: path,
|
||||||
|
ast: newAST(),
|
||||||
|
}).parseFeature()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p parser) parseFeature() (*Feature, error) {
|
// reads tokens into AST and skips comments or new lines
|
||||||
var tok *lexer.Token = p.lx.Next(lexer.COMMENT, lexer.NEW_LINE)
|
func (p *parser) next() *lexer.Token {
|
||||||
|
tok := p.lx.Next()
|
||||||
|
p.ast.addTail(tok)
|
||||||
|
if tok.OfType(lexer.COMMENT, lexer.NEW_LINE) {
|
||||||
|
return p.next()
|
||||||
|
}
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
// peaks into next token, skips comments or new lines
|
||||||
|
func (p *parser) peek() *lexer.Token {
|
||||||
|
if tok := p.lx.Peek(); tok.OfType(lexer.COMMENT, lexer.NEW_LINE) {
|
||||||
|
p.lx.Next()
|
||||||
|
}
|
||||||
|
return p.peek()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) err(s string, l int) error {
|
||||||
|
return fmt.Errorf("%s on %s:%d", s, p.path, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parseFeature() (*Feature, error) {
|
||||||
|
var tok *lexer.Token = p.next()
|
||||||
if tok.Type == lexer.EOF {
|
if tok.Type == lexer.EOF {
|
||||||
return nil, ErrEmpty
|
return nil, ErrEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
ft := &Feature{}
|
ft := &Feature{}
|
||||||
if tok.Type == lexer.TAGS {
|
if tok.Type == lexer.TAGS {
|
||||||
if p.lx.Peek().Type != lexer.FEATURE {
|
if p.peek().Type != lexer.FEATURE {
|
||||||
return ft, ErrTagsNextToFeature
|
return ft, p.err("tags must be a single line next to a feature definition", tok.Line)
|
||||||
}
|
}
|
||||||
ft.Tags = p.parseTags(tok.Value)
|
ft.Tags = p.parseTags(tok.Value)
|
||||||
tok = p.lx.Next()
|
tok = p.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if tok.Type != lexer.FEATURE {
|
if tok.Type != lexer.FEATURE {
|
||||||
return ft, ErrNotFeature
|
return ft, p.err("expected a file to begin with a feature definition, but got '"+tok.Type.String()+"' instead", tok.Line)
|
||||||
}
|
}
|
||||||
|
|
||||||
ft.Title = tok.Value
|
ft.Title = tok.Value
|
||||||
var desc []string
|
var desc []string
|
||||||
for ; p.lx.Peek().Type == lexer.TEXT; tok = p.lx.Next() {
|
for ; p.peek().Type == lexer.TEXT; tok = p.next() {
|
||||||
desc = append(desc, tok.Value)
|
desc = append(desc, tok.Value)
|
||||||
}
|
}
|
||||||
ft.Description = strings.Join(desc, "\n")
|
ft.Description = strings.Join(desc, "\n")
|
||||||
|
|
||||||
tok = p.lx.Next(lexer.COMMENT, lexer.NEW_LINE)
|
tok = p.next()
|
||||||
for ; tok.Type != lexer.EOF; p.lx.Next(lexer.COMMENT, lexer.NEW_LINE) {
|
for tok = p.next(); tok.Type != lexer.EOF; p.next() {
|
||||||
|
// there may be a background
|
||||||
if tok.Type == lexer.BACKGROUND {
|
if tok.Type == lexer.BACKGROUND {
|
||||||
if ft.Background != nil {
|
if ft.Background != nil {
|
||||||
return ft, ErrSingleBackground
|
return ft, p.err("there can only be a single background section, but found another", tok.Line)
|
||||||
}
|
}
|
||||||
ft.Background = p.parseBackground()
|
ft.Background = p.parseBackground()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// there may be tags before scenario
|
||||||
|
sc := &Scenario{}
|
||||||
|
if tok.Type == lexer.TAGS {
|
||||||
|
sc.Tags, tok = p.parseTags(tok.Value), p.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// there must be a scenario otherwise
|
||||||
|
if tok.Type != lexer.SCENARIO {
|
||||||
|
return ft, p.err("expected a scenario, but got '"+tok.Type.String()+"' instead", tok.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.Title = tok.Value
|
||||||
|
p.parseSteps(sc)
|
||||||
|
ft.Scenarios = append(ft.Scenarios, sc)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ft, nil
|
return ft, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p parser) parseBackground() *Background {
|
func (p *parser) parseBackground() *Background {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p parser) parseTags(s string) (tags []Tag) {
|
func (p *parser) parseSteps(s *Scenario) error {
|
||||||
|
var tok *lexer.Token
|
||||||
|
for ; p.peek().OfType(steps...); tok = p.next() {
|
||||||
|
step := &Step{Text: tok.Value}
|
||||||
|
switch tok.Type {
|
||||||
|
case lexer.GIVEN:
|
||||||
|
step.Type = Given
|
||||||
|
case lexer.WHEN:
|
||||||
|
step.Type = When
|
||||||
|
case lexer.THEN:
|
||||||
|
step.Type = Then
|
||||||
|
case lexer.AND:
|
||||||
|
case lexer.BUT:
|
||||||
|
if len(s.Steps) > 0 {
|
||||||
|
step.Type = s.Steps[len(s.Steps)-1].Type
|
||||||
|
} else {
|
||||||
|
step.Type = Given
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; p.peek().OfType(lexer.TEXT); tok = p.next() {
|
||||||
|
step.Text += " " + tok.Value
|
||||||
|
}
|
||||||
|
// now look for pystring or table
|
||||||
|
|
||||||
|
s.Steps = append(s.Steps, step)
|
||||||
|
// return fmt.Errorf("A step was expected, but got: '%s' instead on %s:%d", tok.Type, "file", tok.Line)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parseTags(s string) (tags []Tag) {
|
||||||
for _, tag := range strings.Split(s, " ") {
|
for _, tag := range strings.Split(s, " ") {
|
||||||
t := strings.Trim(tag, "@ ")
|
t := strings.Trim(tag, "@ ")
|
||||||
if len(t) > 0 {
|
if len(t) > 0 {
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче