lucid-tdl 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +21 -0
- data/lib/lucid-tdl/ast.rb +102 -0
- data/lib/lucid-tdl/parser/lexer.rb +130 -0
- data/lib/lucid-tdl/parser/lucid-tdl.rex +40 -0
- data/lib/lucid-tdl/parser/lucid-tdl.y +107 -0
- data/lib/lucid-tdl/parser/parser.rb +485 -0
- data/lib/lucid-tdl/version.rb +3 -0
- data/lib/lucid-tdl.rb +11 -0
- data/lucid-tdl.gemspec +19 -0
- data/test/lucid-tdl/parser/ast_test.rb +125 -0
- data/test/lucid-tdl/parser/full_parsing_test.rb +74 -0
- data/test/lucid-tdl/parser/lexer_test.rb +70 -0
- data/test/lucid-tdl/parser/parser_test.rb +179 -0
- data/test/test_helper.rb +4 -0
- metadata +68 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Jeff Nyman
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Lucid TDL
|
2
|
+
|
3
|
+
This is a test description language for the Lucid tool, modeled on Gherkin.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'lucid-tdl'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install lucid-tdl
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Instructions will follow once I have an implementation.
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = FileList['./test/**/*_test.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Generate Lexer"
|
11
|
+
task :lexer do
|
12
|
+
`rex lib/lucid-tdl/parser/lucid-tdl.rex -o lib/lucid-tdl/parser/lexer.rb`
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Generate Parser"
|
16
|
+
task :parser do
|
17
|
+
`racc lib/lucid-tdl/parser/lucid-tdl.y -o lib/lucid-tdl/parser/parser.rb`
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Generate Lexer and Parser"
|
21
|
+
task :generate => [:lexer, :parser]
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module LucidTDL
|
2
|
+
module AST
|
3
|
+
class Node
|
4
|
+
attr_reader :filename, :line
|
5
|
+
|
6
|
+
def accept(visitor)
|
7
|
+
name = self.class.name.split('::').last
|
8
|
+
visitor.send("visit_#{name}".to_sym, self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def pos(filename, line=nil)
|
12
|
+
@filename = filename
|
13
|
+
@line = line
|
14
|
+
end
|
15
|
+
|
16
|
+
end # class Node
|
17
|
+
|
18
|
+
class Feature < Node
|
19
|
+
include Enumerable
|
20
|
+
|
21
|
+
attr_reader :name
|
22
|
+
attr_accessor :description, :background, :scenarios, :tags
|
23
|
+
|
24
|
+
def initialize(name, scenarios=[], tags=[], background=nil)
|
25
|
+
@name = name
|
26
|
+
@background = background
|
27
|
+
@scenarios = scenarios
|
28
|
+
@tags = tags
|
29
|
+
end
|
30
|
+
|
31
|
+
def each
|
32
|
+
@scenarios.each
|
33
|
+
end
|
34
|
+
end # class Feature
|
35
|
+
|
36
|
+
class Ability < Feature
|
37
|
+
end
|
38
|
+
|
39
|
+
class Responsibility < Feature
|
40
|
+
end
|
41
|
+
|
42
|
+
class Background < Node
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
attr_accessor :steps
|
46
|
+
|
47
|
+
def initialize(steps=[])
|
48
|
+
@steps = steps
|
49
|
+
end
|
50
|
+
|
51
|
+
def line
|
52
|
+
@steps.first.line - 1 if @steps.any?
|
53
|
+
end
|
54
|
+
|
55
|
+
def each
|
56
|
+
@steps.each
|
57
|
+
end
|
58
|
+
end # class Background
|
59
|
+
|
60
|
+
class Scenario < Node
|
61
|
+
include Enumerable
|
62
|
+
|
63
|
+
attr_reader :name, :steps, :tags
|
64
|
+
|
65
|
+
def initialize(name, steps=[], tags=[])
|
66
|
+
@name = name.to_s
|
67
|
+
@steps = steps
|
68
|
+
@tags = tags
|
69
|
+
end
|
70
|
+
|
71
|
+
def line
|
72
|
+
@steps.first.line - 1 if @steps.any?
|
73
|
+
end
|
74
|
+
|
75
|
+
def each
|
76
|
+
@steps.each
|
77
|
+
end
|
78
|
+
end # class Scenario
|
79
|
+
|
80
|
+
class Test < Scenario
|
81
|
+
end
|
82
|
+
|
83
|
+
class Step < Node
|
84
|
+
attr_reader :name
|
85
|
+
attr_reader :keyword
|
86
|
+
|
87
|
+
def initialize(name, keyword)
|
88
|
+
@name = name.to_s
|
89
|
+
@keyword = keyword.to_s
|
90
|
+
end
|
91
|
+
end # class Step
|
92
|
+
|
93
|
+
class Tag < Node
|
94
|
+
attr_reader :name
|
95
|
+
|
96
|
+
def initialize(name)
|
97
|
+
@name = name
|
98
|
+
end
|
99
|
+
end # class Tag
|
100
|
+
|
101
|
+
end # module AST
|
102
|
+
end # module LucidTDL
|
@@ -0,0 +1,130 @@
|
|
1
|
+
#--
|
2
|
+
# DO NOT MODIFY!!!!
|
3
|
+
# This file is automatically generated by rex 1.0.5
|
4
|
+
# from lexical definition file "lib/lucid-tdl/parser/lucid-tdl.rex".
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'racc/parser'
|
8
|
+
class LucidTDL::Parser < Racc::Parser
|
9
|
+
require 'strscan'
|
10
|
+
|
11
|
+
class ScanError < StandardError ; end
|
12
|
+
|
13
|
+
attr_reader :lineno
|
14
|
+
attr_reader :filename
|
15
|
+
attr_accessor :state
|
16
|
+
|
17
|
+
def scan_setup(str)
|
18
|
+
@ss = StringScanner.new(str)
|
19
|
+
@lineno = 1
|
20
|
+
@state = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def action
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
|
27
|
+
def scan_str(str)
|
28
|
+
scan_setup(str)
|
29
|
+
do_parse
|
30
|
+
end
|
31
|
+
alias :scan :scan_str
|
32
|
+
|
33
|
+
def load_file( filename )
|
34
|
+
@filename = filename
|
35
|
+
open(filename, "r") do |f|
|
36
|
+
scan_setup(f.read)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def scan_file( filename )
|
41
|
+
load_file(filename)
|
42
|
+
do_parse
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def next_token
|
47
|
+
return if @ss.eos?
|
48
|
+
|
49
|
+
# skips empty actions
|
50
|
+
until token = _next_token or @ss.eos?; end
|
51
|
+
token
|
52
|
+
end
|
53
|
+
|
54
|
+
def _next_token
|
55
|
+
text = @ss.peek(1)
|
56
|
+
@lineno += 1 if text == "\n"
|
57
|
+
token = case @state
|
58
|
+
when nil
|
59
|
+
case
|
60
|
+
when (text = @ss.scan(/[ \t]+/))
|
61
|
+
;
|
62
|
+
|
63
|
+
when (text = @ss.scan(/\n/))
|
64
|
+
action { [:NEWLINE, text] }
|
65
|
+
|
66
|
+
when (text = @ss.scan(/Feature:/))
|
67
|
+
action { [:FEATURE, text[0..-2]] }
|
68
|
+
|
69
|
+
when (text = @ss.scan(/Ability:/))
|
70
|
+
action { [:ABILITY, text[0..-2]] }
|
71
|
+
|
72
|
+
when (text = @ss.scan(/Responsibility:/))
|
73
|
+
action { [:RESPONSIBILITY, text[0..-2]] }
|
74
|
+
|
75
|
+
when (text = @ss.scan(/Background:/))
|
76
|
+
action { [:BACKGROUND, text[0..-2]] }
|
77
|
+
|
78
|
+
when (text = @ss.scan(/Context:/))
|
79
|
+
action { [:CONTEXT, text[0..-2]] }
|
80
|
+
|
81
|
+
when (text = @ss.scan(/Scenario:/))
|
82
|
+
action { [:SCENARIO, text[0..-2]] }
|
83
|
+
|
84
|
+
when (text = @ss.scan(/Test:/))
|
85
|
+
action { [:TEST, text[0..-2]] }
|
86
|
+
|
87
|
+
when (text = @ss.scan(/@(\w|-)+/))
|
88
|
+
action { [:TAG, text[1..-1]] }
|
89
|
+
|
90
|
+
when (text = @ss.scan(/Given/))
|
91
|
+
action { [:GIVEN, text] }
|
92
|
+
|
93
|
+
when (text = @ss.scan(/When/))
|
94
|
+
action { [:WHEN, text] }
|
95
|
+
|
96
|
+
when (text = @ss.scan(/Then/))
|
97
|
+
action { [:THEN, text] }
|
98
|
+
|
99
|
+
when (text = @ss.scan(/And/))
|
100
|
+
action { [:AND, text] }
|
101
|
+
|
102
|
+
when (text = @ss.scan(/But/))
|
103
|
+
action { [:BUT, text] }
|
104
|
+
|
105
|
+
when (text = @ss.scan(/\*/))
|
106
|
+
action { [:GENERIC, text] }
|
107
|
+
|
108
|
+
when (text = @ss.scan(/[^\n]*/))
|
109
|
+
action { [:TEXT, text.strip] }
|
110
|
+
|
111
|
+
else
|
112
|
+
text = @ss.string[@ss.pos .. -1]
|
113
|
+
raise ScanError, "can not match: '" + text + "'"
|
114
|
+
end # if
|
115
|
+
|
116
|
+
else
|
117
|
+
raise ScanError, "undefined state: '" + state.to_s + "'"
|
118
|
+
end # case state
|
119
|
+
token
|
120
|
+
end # def _next_token
|
121
|
+
|
122
|
+
def tokenize(code)
|
123
|
+
scan_setup(code)
|
124
|
+
tokens = []
|
125
|
+
while token = next_token
|
126
|
+
tokens << token
|
127
|
+
end
|
128
|
+
tokens
|
129
|
+
end
|
130
|
+
end # class
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class LucidTDL::Parser
|
2
|
+
|
3
|
+
macro
|
4
|
+
BLANK [\ \t]+
|
5
|
+
|
6
|
+
rule
|
7
|
+
{BLANK} # no action
|
8
|
+
|
9
|
+
\n { [:NEWLINE, text] }
|
10
|
+
|
11
|
+
Feature: { [:FEATURE, text[0..-2]] }
|
12
|
+
Ability: { [:ABILITY, text[0..-2]] }
|
13
|
+
Responsibility: { [:RESPONSIBILITY, text[0..-2]] }
|
14
|
+
|
15
|
+
Background: { [:BACKGROUND, text[0..-2]] }
|
16
|
+
Context: { [:CONTEXT, text[0..-2]] }
|
17
|
+
Scenario: { [:SCENARIO, text[0..-2]] }
|
18
|
+
Test: { [:TEST, text[0..-2]] }
|
19
|
+
|
20
|
+
@(\w|-)+ { [:TAG, text[1..-1]] }
|
21
|
+
|
22
|
+
Given { [:GIVEN, text] }
|
23
|
+
When { [:WHEN, text] }
|
24
|
+
Then { [:THEN, text] }
|
25
|
+
And { [:AND, text] }
|
26
|
+
But { [:BUT, text] }
|
27
|
+
\* { [:GENERIC, text] }
|
28
|
+
|
29
|
+
[^\n]* { [:TEXT, text.strip] }
|
30
|
+
|
31
|
+
inner
|
32
|
+
def tokenize(code)
|
33
|
+
scan_setup(code)
|
34
|
+
tokens = []
|
35
|
+
while token = next_token
|
36
|
+
tokens << token
|
37
|
+
end
|
38
|
+
tokens
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class LucidTDL::Parser
|
2
|
+
|
3
|
+
rule
|
4
|
+
Root:
|
5
|
+
Feature { result = val[0] }
|
6
|
+
| Feature
|
7
|
+
Scenarios { result = val[0]; result.scenarios = val[1] }
|
8
|
+
| FeatureTags Feature { result = val[1]; result.tags = val[0] }
|
9
|
+
| FeatureTags Feature
|
10
|
+
Scenarios { result = val[1]; result.scenarios = val[2]; result.tags = val[0] }
|
11
|
+
;
|
12
|
+
|
13
|
+
Newline:
|
14
|
+
NEWLINE
|
15
|
+
| Newline NEWLINE
|
16
|
+
;
|
17
|
+
|
18
|
+
Description:
|
19
|
+
TEXT Newline { result = val[0] }
|
20
|
+
| Description TEXT Newline { result = val[0...-1].flatten }
|
21
|
+
;
|
22
|
+
|
23
|
+
FeatureTags:
|
24
|
+
Tags { result = val[0] }
|
25
|
+
| Newline Tags { result = val[1] }
|
26
|
+
;
|
27
|
+
|
28
|
+
Feature:
|
29
|
+
FeatureHeader { result = val[0] }
|
30
|
+
| FeatureHeader
|
31
|
+
Background { result = val[0]; result.background = val[1] }
|
32
|
+
;
|
33
|
+
|
34
|
+
FeatureHeader:
|
35
|
+
FeatureName { result = val[0] }
|
36
|
+
| FeatureName Newline { result = val[0] }
|
37
|
+
| FeatureName Newline
|
38
|
+
Description { result = val[0]; result.description = val[2] }
|
39
|
+
;
|
40
|
+
|
41
|
+
FeatureName:
|
42
|
+
FEATURE TEXT { result = AST::Feature.new(val[1]); result.pos(filename, lineno) }
|
43
|
+
| ABILITY TEXT { result = AST::Feature.new(val[1]); result.pos(filename, lineno) }
|
44
|
+
| RESPONSIBILITY TEXT { result = AST::Feature.new(val[1]); result.pos(filename, lineno) }
|
45
|
+
| Newline FEATURE TEXT { result = AST::Feature.new(val[2]); result.pos(filename, lineno) }
|
46
|
+
| Newline ABILITY TEXT { result = AST::Feature.new(val[2]); result.pos(filename, lineno) }
|
47
|
+
| Newline RESPONSIBILITY TEXT { result = AST::Feature.new(val[2]); result.pos(filename, lineno) }
|
48
|
+
;
|
49
|
+
|
50
|
+
Background:
|
51
|
+
BackgroundHeader
|
52
|
+
Steps { result = val[0]; result.steps = val[1] }
|
53
|
+
;
|
54
|
+
|
55
|
+
BackgroundHeader:
|
56
|
+
BACKGROUND Newline { result = AST::Background.new; result.pos(filename, lineno) }
|
57
|
+
| CONTEXT Newline { result = AST::Background.new; result.pos(filename, lineno) }
|
58
|
+
;
|
59
|
+
|
60
|
+
Steps:
|
61
|
+
Step { result = [val[0]] }
|
62
|
+
| Step Newline { result = [val[0]] }
|
63
|
+
| Step Newline Steps { val[2].unshift(val[0]); result = val[2] }
|
64
|
+
;
|
65
|
+
|
66
|
+
Step:
|
67
|
+
Keyword TEXT { result = AST::Step.new(val[1], val[0]); result.pos(filename, lineno) }
|
68
|
+
;
|
69
|
+
|
70
|
+
Keyword:
|
71
|
+
GIVEN | WHEN | THEN | AND | BUT | GENERIC
|
72
|
+
;
|
73
|
+
|
74
|
+
Scenarios:
|
75
|
+
Scenario { result = [val[0]] }
|
76
|
+
| Scenarios Scenario { result = val[0] << val[1] }
|
77
|
+
;
|
78
|
+
|
79
|
+
Scenario:
|
80
|
+
SCENARIO TEXT Newline
|
81
|
+
Steps { result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1) }
|
82
|
+
| TEST TEXT Newline
|
83
|
+
Steps { result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1) }
|
84
|
+
| Tags Newline
|
85
|
+
SCENARIO TEXT Newline
|
86
|
+
Steps { result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2) }
|
87
|
+
| Tags Newline
|
88
|
+
TEST TEXT Newline
|
89
|
+
Steps { result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2) }
|
90
|
+
;
|
91
|
+
|
92
|
+
Tags:
|
93
|
+
TAG { result = [AST::Tag.new(val[0])] }
|
94
|
+
| Tags TAG { result = val[0] << AST::Tag.new(val[1]) }
|
95
|
+
;
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
---- header
|
100
|
+
require_relative "./lexer"
|
101
|
+
require_relative "../ast"
|
102
|
+
|
103
|
+
---- inner
|
104
|
+
|
105
|
+
def parse(input)
|
106
|
+
scan_str(input)
|
107
|
+
end
|