diff --git a/gherkin/ast.go b/gherkin/ast.go new file mode 100644 index 0000000..3007f58 --- /dev/null +++ b/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 +} diff --git a/gherkin/lexer/token_type.go b/gherkin/lexer/token_type.go index faef14a..e88a225 100644 --- a/gherkin/lexer/token_type.go +++ b/gherkin/lexer/token_type.go @@ -20,6 +20,8 @@ const ( FEATURE BACKGROUND SCENARIO + + steps GIVEN WHEN THEN diff --git a/gherkin/parse.go b/gherkin/parse.go index b189a49..47d180e 100644 --- a/gherkin/parse.go +++ b/gherkin/parse.go @@ -2,7 +2,8 @@ package gherkin import ( "errors" - "io" + "fmt" + "os" "strings" "github.com/l3pp4rd/behat/gherkin/lexer" @@ -11,6 +12,7 @@ import ( type Tag string type Scenario struct { + Title string Steps []*Step Tags []Tag } @@ -40,64 +42,148 @@ type Feature struct { 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 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 { - lx *lexer.Lexer + lx *lexer.Lexer + path string + ast *AST } -func Parse(r io.Reader) (*Feature, error) { - return (parser{lx: lexer.New(r)}).parseFeature() +func Parse(path string) (*Feature, error) { + 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) { - var tok *lexer.Token = p.lx.Next(lexer.COMMENT, lexer.NEW_LINE) +// reads tokens into AST and skips comments or new lines +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 { return nil, ErrEmpty } ft := &Feature{} if tok.Type == lexer.TAGS { - if p.lx.Peek().Type != lexer.FEATURE { - return ft, ErrTagsNextToFeature + if p.peek().Type != lexer.FEATURE { + return ft, p.err("tags must be a single line next to a feature definition", tok.Line) } ft.Tags = p.parseTags(tok.Value) - tok = p.lx.Next() + tok = p.next() } 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 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) } ft.Description = strings.Join(desc, "\n") - tok = p.lx.Next(lexer.COMMENT, lexer.NEW_LINE) - for ; tok.Type != lexer.EOF; p.lx.Next(lexer.COMMENT, lexer.NEW_LINE) { + tok = p.next() + for tok = p.next(); tok.Type != lexer.EOF; p.next() { + // there may be a background if tok.Type == lexer.BACKGROUND { 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() 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 } -func (p parser) parseBackground() *Background { +func (p *parser) parseBackground() *Background { 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, " ") { t := strings.Trim(tag, "@ ") if len(t) > 0 {