macros4cuke 0.1.07 → 0.2.00
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +12 -0
- data/README.md +6 -7
- data/features/demo02.feature +3 -3
- data/features/demo03.feature +5 -5
- data/features/demo04.feature +3 -3
- data/features/travelling-demo.feature +16 -17
- data/lib/macro_steps.rb +2 -2
- data/lib/macros4cuke/constants.rb +1 -1
- data/lib/macros4cuke/macro-step.rb +173 -242
- data/lib/macros4cuke/template-engine.rb +117 -0
- data/spec/macros4cuke/macro-step_spec.rb +60 -0
- data/spec/macros4cuke/template-engine_spec.rb +205 -0
- data/spec/spec_helper.rb +8 -0
- metadata +8 -20
data/HISTORY.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## [0.2.00] Version number was bumped
|
2
|
+
### Changes:
|
3
|
+
* Replaced the Mustache template engine by own lightweight TemplateEngine
|
4
|
+
* Macro-step arguments have a syntax that is closer to Gherkin's scenario outlines.
|
5
|
+
* Upgraded TemplateEngine class to become a simple templating system.
|
6
|
+
* Remove dependency to mustache gem.
|
7
|
+
|
8
|
+
### Documentation:
|
9
|
+
* All demos updated to new syntax.
|
10
|
+
* Updated the examples in README.md
|
11
|
+
|
12
|
+
|
1
13
|
## [0.1.07]
|
2
14
|
### Changes:
|
3
15
|
* Added new class TemplateEngine.
|
data/README.md
CHANGED
@@ -19,19 +19,18 @@ To each macro-step, it is possible to associate a sequence of sub-steps
|
|
19
19
|
## Synopsis ##
|
20
20
|
Here is an example taken from our demo files:
|
21
21
|
```cucumber
|
22
|
-
Given I define the step "When I [enter my userid
|
22
|
+
Given I define the step "When I [enter my userid <userid> and password <password>]" to mean:
|
23
23
|
"""
|
24
24
|
Given I landed in the homepage
|
25
25
|
When I click "Sign in"
|
26
|
-
And I fill in "Username" with "
|
27
|
-
And I fill in "Password" with "
|
26
|
+
And I fill in "Username" with "<userid>"
|
27
|
+
And I fill in "Password" with "<password>"
|
28
28
|
And I click "Submit"
|
29
29
|
"""
|
30
30
|
```
|
31
31
|
|
32
|
-
Notice how the arguments are enclosed between
|
33
|
-
|
34
|
-
for generating the sub-steps.
|
32
|
+
Notice how the arguments _userid_ and _password_ are enclosed between chevrons (angle brackets) <...>.
|
33
|
+
|
35
34
|
|
36
35
|
That macro-step can then be used in a scenario like this:
|
37
36
|
```cucumber
|
@@ -73,7 +72,7 @@ World(Macros4Cuke::MacroStepSupport)
|
|
73
72
|
|
74
73
|
* Step 3: Import the macro-management steps
|
75
74
|
In your /features/step_definitions/ folder:
|
76
|
-
- Create a
|
75
|
+
- Create a Ruby file (say, 'use\_macro\_steps.rb') with the following line:
|
77
76
|
|
78
77
|
```ruby
|
79
78
|
require 'macros4cuke/../macro_steps'
|
data/features/demo02.feature
CHANGED
@@ -9,12 +9,12 @@ Scenario: Creating a basic scenario with one argument
|
|
9
9
|
# The next step creates a macro(-step)
|
10
10
|
# The syntax of the new macro-step is specified between the double quotes.
|
11
11
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
12
|
-
# The macro argument is put between
|
13
|
-
Given I define the step "When I [log in as
|
12
|
+
# The macro argument is put between chevrons <...>.
|
13
|
+
Given I define the step "When I [log in as <userid>]" to mean:
|
14
14
|
"""
|
15
15
|
Given I landed in the homepage
|
16
16
|
When I click "Sign in"
|
17
|
-
And I fill in "Username" with "
|
17
|
+
And I fill in "Username" with "<userid>"
|
18
18
|
And I fill in "Password" with "unguessable"
|
19
19
|
And I click "Submit"
|
20
20
|
"""
|
data/features/demo03.feature
CHANGED
@@ -8,13 +8,13 @@ Feature: Show the use of a basic macro with multiple arguments
|
|
8
8
|
Scenario: defining basic macro with multiple arguments
|
9
9
|
# The next step creates a macro(-step)double quotes.
|
10
10
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
11
|
-
# The macro
|
12
|
-
Given I define the step "When I [enter my userid
|
11
|
+
# The macro-step arguments are put between chevrons <...>.
|
12
|
+
Given I define the step "When I [enter my userid <userid> and password <password>]" to mean:
|
13
13
|
"""
|
14
14
|
Given I landed in the homepage
|
15
15
|
When I click "Sign in"
|
16
|
-
And I fill in "Username" with "
|
17
|
-
And I fill in "Password" with "
|
16
|
+
And I fill in "Username" with "<userid>"
|
17
|
+
And I fill in "Password" with "<password>"
|
18
18
|
And I click "Submit"
|
19
19
|
"""
|
20
20
|
|
@@ -35,7 +35,7 @@ Invoked step: ... I click "Submit"
|
|
35
35
|
Scenario: A macro invoking another macro (YES, it's possible!)
|
36
36
|
Given I define the step "When I [enter my credentials]" to mean:
|
37
37
|
"""
|
38
|
-
|
38
|
+
# Notice that the next step is invoking the first macro above
|
39
39
|
When I [enter my userid "guest" and password "unguessable"]
|
40
40
|
"""
|
41
41
|
|
data/features/demo04.feature
CHANGED
@@ -9,13 +9,13 @@ Scenario: Defining a macro to be used with multiple arguments in a table
|
|
9
9
|
# The next step creates a macro(-step)
|
10
10
|
# The syntax of the new macro-step is specified between double quotes.
|
11
11
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
12
|
-
# The macro
|
12
|
+
# The macro arguments are put between chevrons <...>.
|
13
13
|
Given I define the step "When I [enter my credentials as]:" to mean:
|
14
14
|
"""
|
15
15
|
Given I landed in the homepage
|
16
16
|
When I click "Sign in"
|
17
|
-
And I fill in "Username" with "
|
18
|
-
And I fill in "Password" with "
|
17
|
+
And I fill in "Username" with "<userid>"
|
18
|
+
And I fill in "Password" with "<password>"
|
19
19
|
And I click "Submit"
|
20
20
|
"""
|
21
21
|
|
@@ -6,10 +6,10 @@ Feature: Show -visually- the several ways to use macros
|
|
6
6
|
|
7
7
|
|
8
8
|
Scenario: Definition of a simple macro-step with two arguments
|
9
|
-
Given I define the step "When I [travel from
|
9
|
+
Given I define the step "When I [travel from <origin> to <destination>]" to mean:
|
10
10
|
"""
|
11
|
-
When I leave
|
12
|
-
And I arrive in
|
11
|
+
When I leave <origin>
|
12
|
+
And I arrive in <destination>
|
13
13
|
"""
|
14
14
|
|
15
15
|
Scenario: Do a simple travel
|
@@ -23,16 +23,15 @@ Scenario: Do a simple travel
|
|
23
23
|
# Actual values can have embedded double quotes provided they are escaped.
|
24
24
|
When I [travel from "Tampa" to "\"Little Italy\""]
|
25
25
|
|
26
|
-
# Notice the HTML-escaping done by Mustache
|
27
26
|
|
28
27
|
|
29
28
|
|
30
29
|
Scenario: Defining a macro calling other macro(s)
|
31
|
-
Given I define the step "When I [travel from
|
30
|
+
Given I define the step "When I [travel from <origin> to <destination> and back]" to mean:
|
32
31
|
"""
|
33
32
|
# The next two steps are, in fact, macro-step invokations
|
34
|
-
When I [travel from "
|
35
|
-
When I [travel from "{{destination}}" to "
|
33
|
+
When I [travel from "<origin>" to "<destination>"]
|
34
|
+
When I [travel from "{{destination}}" to "<origin>"]
|
36
35
|
"""
|
37
36
|
|
38
37
|
Scenario: Do a travel back and forth
|
@@ -48,12 +47,12 @@ Scenario: Do a travel back and forth
|
|
48
47
|
Scenario: Defining a macro that requires a data table
|
49
48
|
Given I define the step "When I [fill in the form with]:" to mean:
|
50
49
|
"""
|
51
|
-
When I type "
|
52
|
-
And I type "
|
53
|
-
And I type "
|
54
|
-
And I type "
|
55
|
-
And I type "
|
56
|
-
And I type "
|
50
|
+
When I type "<firstname>"
|
51
|
+
And I type "<lastname>"
|
52
|
+
And I type "<street_address>"
|
53
|
+
And I type "<postcode>"
|
54
|
+
And I type "<city>"
|
55
|
+
And I type "<country>"
|
57
56
|
"""
|
58
57
|
|
59
58
|
Scenario: Using a macro-step with a data table
|
@@ -96,10 +95,10 @@ Scenario: Demonstrate that it is possible to use a sub-step with a data table
|
|
96
95
|
Given I define the step "When I [fill in, as a Londonian, the form with]:" to mean:
|
97
96
|
"""
|
98
97
|
When I [fill in the form with]:
|
99
|
-
|firstname|
|
100
|
-
|lastname |
|
101
|
-
|street_address|
|
102
|
-
|postcode
|
98
|
+
|firstname| <firstname>|
|
99
|
+
|lastname | <lastname> |
|
100
|
+
|street_address| <street_address>|
|
101
|
+
|postcode|<postcode> |
|
103
102
|
# The next two lines have hard-coded values
|
104
103
|
|city |London |
|
105
104
|
|country | U.K. |
|
data/lib/macro_steps.rb
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
# This step is used to define a macro-step
|
7
7
|
# Example:
|
8
|
-
# Given I define the step "When I [log in as
|
8
|
+
# Given I define the step "When I [log in as <userid>]" to mean:
|
9
9
|
# """
|
10
10
|
# Given I landed in the homepage
|
11
11
|
# When I click "Sign in"
|
12
|
-
# And I fill in "Username" with "
|
12
|
+
# And I fill in "Username" with "<userid>"
|
13
13
|
# And I fill in "Password" with "unguessable"
|
14
14
|
# And I click "Submit"
|
15
15
|
# """
|
@@ -1,243 +1,174 @@
|
|
1
|
-
# File: macro-step.rb
|
2
|
-
# Purpose: Implementation of the MacroStep class.
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
#
|
26
|
-
# [
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@renderer =
|
39
|
-
renderer.
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# -
|
53
|
-
# -
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
stripped_phrase
|
62
|
-
|
63
|
-
#
|
64
|
-
stripped_phrase.gsub!(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
#
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
when :
|
149
|
-
/
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
lines
|
164
|
-
|
165
|
-
|
166
|
-
processed
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
def add_tags_multi(tokens)
|
175
|
-
first_token = tokens.shift
|
176
|
-
unless first_token == :multi
|
177
|
-
raise InternalError, "Expecting a :multi token instead of a #{first_token}"
|
178
|
-
end
|
179
|
-
|
180
|
-
tokens.each do |an_opcode|
|
181
|
-
case an_opcode[0]
|
182
|
-
when :static
|
183
|
-
# Do nothing...
|
184
|
-
|
185
|
-
when :mustache
|
186
|
-
add_tags_mustache(an_opcode)
|
187
|
-
|
188
|
-
when String
|
189
|
-
#Do nothing...
|
190
|
-
else
|
191
|
-
raise InternalError, "Unknown Mustache token type #{an_opcode.first}"
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
# [mustache_opcode] An array with the first element being :mustache
|
197
|
-
def add_tags_mustache(mustache_opcode)
|
198
|
-
mustache_opcode.shift() # Drop the :mustache symbol
|
199
|
-
|
200
|
-
case mustache_opcode[0]
|
201
|
-
when :etag
|
202
|
-
triplet = mustache_opcode[1]
|
203
|
-
raise InternalError, "expected 'mustache' token instead of '#{triplet[0]}'" unless triplet[0] == :mustache
|
204
|
-
raise InternalError, "expected 'fetch' token instead of '#{triplet[1]}'" unless triplet[1] == :fetch
|
205
|
-
@args << triplet.last
|
206
|
-
|
207
|
-
when :fetch
|
208
|
-
@args << mustache_opcode.last
|
209
|
-
|
210
|
-
when :section
|
211
|
-
add_tags_section(mustache_opcode)
|
212
|
-
|
213
|
-
else
|
214
|
-
raise InternalError, "Unknown Mustache token type #{mustache_opcode.first}"
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
|
219
|
-
def add_tags_section(opcodes)
|
220
|
-
opcodes.shift() # Drop the :section symbol
|
221
|
-
|
222
|
-
opcodes.each do |op|
|
223
|
-
case op[0]
|
224
|
-
when :mustache
|
225
|
-
add_tags_mustache(op)
|
226
|
-
|
227
|
-
when :multi
|
228
|
-
add_tags_multi(op)
|
229
|
-
|
230
|
-
when String
|
231
|
-
return
|
232
|
-
else
|
233
|
-
raise InternalError, "Unknown Mustache token type #{op.first}"
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
end # class
|
239
|
-
|
240
|
-
end # module
|
241
|
-
|
242
|
-
|
1
|
+
# File: macro-step.rb
|
2
|
+
# Purpose: Implementation of the MacroStep class.
|
3
|
+
|
4
|
+
|
5
|
+
require_relative 'template-engine'
|
6
|
+
|
7
|
+
module Macros4Cuke # Module used as a namespace
|
8
|
+
|
9
|
+
# In essence, a macro step object represents a Cucumber step that is itself
|
10
|
+
# an aggregation of lower-level Cucumber steps.
|
11
|
+
class MacroStep
|
12
|
+
# A template engine that expands the substeps upon request.
|
13
|
+
attr_reader(:renderer)
|
14
|
+
|
15
|
+
# Name of the macro as derived from the macro phrase.
|
16
|
+
attr_reader(:name)
|
17
|
+
|
18
|
+
# The list of macro arguments that appears in the macro phrase.
|
19
|
+
attr_reader(:phrase_args)
|
20
|
+
|
21
|
+
# The list of macro argument names (as appearing in the substeps and in the macro phrase).
|
22
|
+
attr_reader(:args)
|
23
|
+
|
24
|
+
|
25
|
+
# Constructor.
|
26
|
+
# [aMacroPhrase] The text from the macro step definition that is between the square brackets.
|
27
|
+
# [theSubsteps] The source text of the steps to be expanded upon macro invokation.
|
28
|
+
def initialize(aMacroPhrase, theSubsteps)
|
29
|
+
@name = self.class.macro_key(aMacroPhrase, :definition)
|
30
|
+
|
31
|
+
# Retrieve the macro arguments embedded in the phrase.
|
32
|
+
@phrase_args = scan_arguments(aMacroPhrase, :definition)
|
33
|
+
@args = @phrase_args.dup()
|
34
|
+
|
35
|
+
# Manipulate the substeps source text
|
36
|
+
substeps_processed = preprocess(theSubsteps)
|
37
|
+
|
38
|
+
@renderer = TemplateEngine.new(substeps_processed)
|
39
|
+
@args.concat(renderer.variables)
|
40
|
+
@args.uniq!
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Compute the identifier of the macro from the given macro phrase.
|
45
|
+
# A macro phrase is a text that must start with a recognised verb and may contain zero or more placeholders.
|
46
|
+
# In definition mode, a placeholder is delimited by chevrons <..>
|
47
|
+
# In invokation mode, a placeholder is delimited by double quotes.
|
48
|
+
# The rule for building the identifier are:
|
49
|
+
# - Leading and trailing space(s) are removed.
|
50
|
+
# - Each underscore character is removed.
|
51
|
+
# - Every sequence of one or more space(s) is converted into an underscore
|
52
|
+
# - Each placeholder (i.e. = delimiters + enclosed text) is converted into a letter X.
|
53
|
+
# - The endings are transformed as follows: ] => '', ]: => _T
|
54
|
+
# Example:
|
55
|
+
# Consider the macro phrase: 'create the following "contactType" contact]:'
|
56
|
+
# The resulting macro_key is: 'create_the_following_X_contact_T'
|
57
|
+
def self.macro_key(aMacroPhrase, mode)
|
58
|
+
stripped_phrase = aMacroPhrase.strip # Remove leading ... trailing space(s)
|
59
|
+
|
60
|
+
# Remove every underscore
|
61
|
+
stripped_phrase.gsub!(/_/, '')
|
62
|
+
|
63
|
+
# Replace all consecutive whitespaces by an underscore
|
64
|
+
stripped_phrase.gsub!(/\s+/, '_')
|
65
|
+
|
66
|
+
|
67
|
+
# Determine the pattern to isolate each argument/parameter with its delimiters
|
68
|
+
pattern = case mode
|
69
|
+
when :definition
|
70
|
+
/<(?:[^\\<>]|\\.)*>/
|
71
|
+
when :invokation
|
72
|
+
/"([^\\"]|\\.)*"/
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# Each text between quotes or chevron is replaced by the letter X
|
77
|
+
normalized = stripped_phrase.gsub(pattern, 'X')
|
78
|
+
|
79
|
+
# Drop the "]" ending or replace the "]:# ending by "_T"
|
80
|
+
key = if normalized.end_with?("]")
|
81
|
+
normalized.chop()
|
82
|
+
else
|
83
|
+
normalized.sub(/\]:/, '_T')
|
84
|
+
end
|
85
|
+
|
86
|
+
return key
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# Render the steps from the template, given the values
|
91
|
+
# taken by the parameters
|
92
|
+
# [macro_parameters] a Hash with pairs of the kind: macro argument name => value
|
93
|
+
def expand(macro_parameters)
|
94
|
+
return renderer.render(nil, macro_parameters)
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
# Build a Hash from the given raw data.
|
99
|
+
# [aPhrase] an instance of the macro phrase.
|
100
|
+
# [rawData] An Array of couples.
|
101
|
+
# Each couple is of the form: argument name, a value.
|
102
|
+
# Multiple rows with same argument name are acceptable.
|
103
|
+
def validate_params(aPhrase, rawData)
|
104
|
+
macro_parameters = {}
|
105
|
+
|
106
|
+
# Retrieve the value(s) per variable in the phrase.
|
107
|
+
quoted_values = scan_arguments(aPhrase, :invokation)
|
108
|
+
quoted_values.each_with_index do |val, index|
|
109
|
+
macro_parameters[phrase_args[index]] = val
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
unless rawData.nil?
|
114
|
+
rawData.each do |(key, value)|
|
115
|
+
raise UnknownArgumentError.new(key) unless @args.include? key
|
116
|
+
if macro_parameters.include? key
|
117
|
+
if macro_parameters[key].kind_of?(Array)
|
118
|
+
macro_parameters[key] << value
|
119
|
+
else
|
120
|
+
macro_parameters[key] = [macro_parameters[key], value]
|
121
|
+
end
|
122
|
+
else
|
123
|
+
macro_parameters[key] = value
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
return macro_parameters
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
private
|
134
|
+
# Retrieve from the macro phrase, all the text between <..> or double quotes.
|
135
|
+
# Returns an array. Each of its elements corresponds to quoted text.
|
136
|
+
# Example:
|
137
|
+
# aMacroPhrase = 'a "qualifier" text with "quantity" placeholders.'
|
138
|
+
# Results in : ["qualifier", "quantity"]
|
139
|
+
# [aMacroPhrase] A phrase
|
140
|
+
# [mode] one of the following: :definition, :invokation
|
141
|
+
def scan_arguments(aMacroPhrase, mode)
|
142
|
+
# determine the syntax of the arguments/parameters
|
143
|
+
# as a regular expression with one capturing group
|
144
|
+
pattern = case mode
|
145
|
+
when :definition
|
146
|
+
/<((?:[^\\<>]|\\.)*)>/
|
147
|
+
# /{{{([^}]*)}}}|{{([^}]*)}}/ # Two capturing groups!...
|
148
|
+
when :invokation
|
149
|
+
/"((?:[^\\"]|\\.)*)"/
|
150
|
+
else
|
151
|
+
raise InternalError, "Internal error: Unknown mode argument #{mode}"
|
152
|
+
end
|
153
|
+
raw_result = aMacroPhrase.scan(pattern)
|
154
|
+
return raw_result.flatten.compact
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return the substeps text after some transformation
|
158
|
+
# [theSubstepsSource] The source text of the steps to be expanded upon macro invokation.
|
159
|
+
def preprocess(theSubstepsSource)
|
160
|
+
# Split text into lines
|
161
|
+
lines = theSubstepsSource.split(/\r\n?|\n/)
|
162
|
+
|
163
|
+
# Reject comment lines. This necessary because Cucumber::RbSupport::RbWorld#steps complains when it sees a comment.
|
164
|
+
processed = lines.reject { |a_line| a_line =~ /\s*#/ }
|
165
|
+
|
166
|
+
return processed.join("\n")
|
167
|
+
end
|
168
|
+
|
169
|
+
end # class
|
170
|
+
|
171
|
+
end # module
|
172
|
+
|
173
|
+
|
243
174
|
# End of file
|
@@ -6,8 +6,88 @@ require 'strscan' # Use the StringScanner for lexical analysis.
|
|
6
6
|
module Macros4Cuke # Module used as a namespace
|
7
7
|
|
8
8
|
|
9
|
+
|
10
|
+
class StaticRep
|
11
|
+
attr_reader(:source)
|
12
|
+
|
13
|
+
def initialize(aSourceText)
|
14
|
+
@source = aSourceText
|
15
|
+
end
|
16
|
+
|
17
|
+
public
|
18
|
+
def render(aContextObject = nil, theLocals)
|
19
|
+
return source
|
20
|
+
end
|
21
|
+
end # class
|
22
|
+
|
23
|
+
|
24
|
+
class VariableRep
|
25
|
+
attr_reader(:name)
|
26
|
+
|
27
|
+
def initialize(aVarName)
|
28
|
+
@name = aVarName
|
29
|
+
end
|
30
|
+
|
31
|
+
public
|
32
|
+
# The signature of this method should comply to the Tilt API.
|
33
|
+
# Actual values from the 'locals' Hash take precedence over the context object.
|
34
|
+
def render(aContextObject, theLocals)
|
35
|
+
actual_value = theLocals[name]
|
36
|
+
if actual_value.nil?
|
37
|
+
actual_value = aContextObject.send(name.to_sym) if aContextObject.respond_to?(name.to_sym)
|
38
|
+
actual_value = '' if actual_value.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
return actual_value.is_a?(String) ? actual_value : actual_value.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
end # class
|
45
|
+
|
46
|
+
|
47
|
+
class EOLRep
|
48
|
+
public
|
49
|
+
def render(aContextObject, theLocals)
|
50
|
+
return "\n"
|
51
|
+
end
|
52
|
+
end # class
|
53
|
+
|
54
|
+
|
9
55
|
class TemplateEngine
|
56
|
+
# The original text of the template is kept here.
|
57
|
+
attr_reader(:source)
|
58
|
+
|
59
|
+
# Constructor.
|
60
|
+
# [aSourceTemplate]
|
61
|
+
def initialize(aSourceTemplate)
|
62
|
+
@source = aSourceTemplate
|
63
|
+
@representation = compile(aSourceTemplate)
|
64
|
+
end
|
65
|
+
|
66
|
+
public
|
67
|
+
def render(aContextObject = nil, theLocals)
|
68
|
+
return '' if @representation.empty?
|
69
|
+
context = aContextObject.nil? ? Object.new : aContextObject
|
70
|
+
|
71
|
+
result = @representation.each_with_object('') do |element, subResult|
|
72
|
+
subResult << element.render(context, theLocals)
|
73
|
+
end
|
74
|
+
|
75
|
+
return result
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return all variable names that appear in the template.
|
79
|
+
def variables()
|
80
|
+
# The result will be cached/memoized...
|
81
|
+
@variables ||= begin
|
82
|
+
vars = @representation.select { |element| element.is_a?(VariableRep) }
|
83
|
+
vars.map(&:name)
|
84
|
+
end
|
85
|
+
|
86
|
+
return @variables
|
87
|
+
end
|
10
88
|
|
89
|
+
# Class method. Parse the given line text.
|
90
|
+
# Returns an array of couples.
|
11
91
|
def self.parse(aTextLine)
|
12
92
|
scanner = StringScanner.new(aTextLine)
|
13
93
|
result = []
|
@@ -47,6 +127,43 @@ class TemplateEngine
|
|
47
127
|
return result
|
48
128
|
end
|
49
129
|
|
130
|
+
private
|
131
|
+
# Create the internal representation of the given template.
|
132
|
+
def compile(aSourceTemplate)
|
133
|
+
# Split the input text into lines.
|
134
|
+
input_lines = aSourceTemplate.split(/\r\n?|\n/)
|
135
|
+
|
136
|
+
# Parse the input text into raw data.
|
137
|
+
raw_lines = input_lines.map { |line| self.class.parse(line) }
|
138
|
+
|
139
|
+
compiled_lines = raw_lines.map { |line| compile_line(line) }
|
140
|
+
return compiled_lines.flatten()
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
def compile_line(aRawLine)
|
145
|
+
line_rep = aRawLine.map { |couple| compile_couple(couple) }
|
146
|
+
line_rep << EOLRep.new
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# [aCouple] a two-element array of the form: [kind, text]
|
151
|
+
# Where kind must be one of :static, :dynamic
|
152
|
+
def compile_couple(aCouple)
|
153
|
+
(kind, text) = aCouple
|
154
|
+
|
155
|
+
result = case kind
|
156
|
+
when :static
|
157
|
+
StaticRep.new(text)
|
158
|
+
when :dynamic
|
159
|
+
VariableRep.new(text)
|
160
|
+
else
|
161
|
+
raise StandardError, "Internal error: Don't know template element of kind #{kind}"
|
162
|
+
end
|
163
|
+
|
164
|
+
return result
|
165
|
+
end
|
166
|
+
|
50
167
|
end # class
|
51
168
|
|
52
169
|
end # module
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# File: macro-step_spec.rb
|
2
|
+
|
3
|
+
|
4
|
+
require_relative '../spec_helper'
|
5
|
+
require_relative '../../lib/macros4cuke/macro-step' # The class under test
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
module Macros4Cuke # Open the module to avoid lengthy qualified names
|
10
|
+
|
11
|
+
describe MacroStep do
|
12
|
+
let(:sample_phrase) { "enter my credentials as <userid>]:" }
|
13
|
+
let(:sample_template) do
|
14
|
+
snippet = <<-SNIPPET
|
15
|
+
Given I landed in the homepage
|
16
|
+
When I click "Sign in"
|
17
|
+
And I fill in "Username" with "<userid>"
|
18
|
+
And I fill in "Password" with "<password>"
|
19
|
+
And I click "Submit"
|
20
|
+
SNIPPET
|
21
|
+
|
22
|
+
snippet
|
23
|
+
end
|
24
|
+
|
25
|
+
# Default instantiation rule
|
26
|
+
subject { MacroStep.new(sample_phrase, sample_template) }
|
27
|
+
|
28
|
+
|
29
|
+
context "Creation & initialization" do
|
30
|
+
it "should be created with a phrase and a template" do
|
31
|
+
lambda { MacroStep.new(sample_phrase, sample_template) }.should_not raise_error
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
it "should know its key/name" do
|
36
|
+
subject.name.should == "enter_my_credentials_as_X_T"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should know the tags(placeholders) from its phrase" do
|
40
|
+
subject.phrase_args.should == %w[userid]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should know the tags(placeholders) from its phrase and template" do
|
44
|
+
subject.args.should == %w[userid password]
|
45
|
+
end
|
46
|
+
|
47
|
+
end # context
|
48
|
+
|
49
|
+
|
50
|
+
context "Provided services" do
|
51
|
+
it "should render the substeps" do
|
52
|
+
end
|
53
|
+
end # context
|
54
|
+
|
55
|
+
|
56
|
+
end # describe
|
57
|
+
|
58
|
+
end # module
|
59
|
+
|
60
|
+
# End of file
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# encoding: utf-8 -- You should see a paragraph character: §
|
2
|
+
# File: template-engine_spec.rb
|
3
|
+
|
4
|
+
require_relative '../spec_helper'
|
5
|
+
require_relative '../../lib/macros4cuke/template-engine' # Load the class under test
|
6
|
+
|
7
|
+
module Macros4Cuke # Open this namespace to get rid of module qualifier prefixes
|
8
|
+
|
9
|
+
describe TemplateEngine do
|
10
|
+
let(:sample_template) do
|
11
|
+
source = <<-SNIPPET
|
12
|
+
Given I landed in the homepage
|
13
|
+
# The credentials are entered here
|
14
|
+
And I fill in "Username" with "<userid>"
|
15
|
+
And I fill in "Password" with "<password>"
|
16
|
+
And I click "Sign in"
|
17
|
+
SNIPPET
|
18
|
+
end
|
19
|
+
|
20
|
+
# Rule for default instantiation
|
21
|
+
subject { TemplateEngine.new sample_template }
|
22
|
+
|
23
|
+
|
24
|
+
context "Class services" do
|
25
|
+
# Convenience method used to shorten method call.
|
26
|
+
def parse_it(aText)
|
27
|
+
return TemplateEngine::parse(aText)
|
28
|
+
end
|
29
|
+
|
30
|
+
# remove enclosing chevrons <..> (if any)
|
31
|
+
def strip_chevrons(aText)
|
32
|
+
return aText.gsub(/^<|>$/, '')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should parse an empty text line" do
|
36
|
+
# Expectation: result should be an empty array.
|
37
|
+
parse_it('').should be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should parse a text line without tag" do
|
41
|
+
sample_text = 'Mary has a little lamb'
|
42
|
+
result = parse_it(sample_text)
|
43
|
+
|
44
|
+
# Expectation: an array with one couple: [:static, the source text]
|
45
|
+
result.should have(1).items
|
46
|
+
result[0].should == [:static, sample_text]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should parse a text line that consists of just a tag" do
|
50
|
+
sample_text = '<some_tag>'
|
51
|
+
result = parse_it(sample_text)
|
52
|
+
|
53
|
+
# Expectation: an array with one couple: [:static, the source text]
|
54
|
+
result.should have(1).items
|
55
|
+
result[0].should == [:dynamic, strip_chevrons(sample_text)]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should parse a text line with a tag at the start" do
|
59
|
+
sample_text = '<some_tag>some text'
|
60
|
+
result = parse_it(sample_text)
|
61
|
+
|
62
|
+
# Expectation: an array with two couples: [dynamic, 'some_tag'][:static, some text]
|
63
|
+
result.should have(2).items
|
64
|
+
result[0].should == [:dynamic, 'some_tag']
|
65
|
+
result[1].should == [:static, 'some text']
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should parse a text line with a tag at the end" do
|
69
|
+
sample_text = 'some text<some_tag>'
|
70
|
+
result = parse_it(sample_text)
|
71
|
+
|
72
|
+
# Expectation: an array with two couples: [:static, some text] [dynamic, 'some_tag']
|
73
|
+
result.should have(2).items
|
74
|
+
result[0].should == [:static, 'some text']
|
75
|
+
result[1].should == [:dynamic, 'some_tag']
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should parse a text line with a tag in the middle" do
|
79
|
+
sample_text = 'begin <some_tag> end'
|
80
|
+
result = parse_it(sample_text)
|
81
|
+
|
82
|
+
# Expectation: an array with three couples:
|
83
|
+
result.should have(3).items
|
84
|
+
result[0].should == [:static, 'begin ']
|
85
|
+
result[1].should == [:dynamic, 'some_tag']
|
86
|
+
result[2].should == [:static, ' end']
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should parse a text line with two tags in the middle" do
|
90
|
+
sample_text = 'begin <some_tag>middle<another_tag> end'
|
91
|
+
result = parse_it(sample_text)
|
92
|
+
|
93
|
+
# Expectation: an array with items couples:
|
94
|
+
result.should have(5).items
|
95
|
+
result[0].should == [:static , 'begin ']
|
96
|
+
result[1].should == [:dynamic, 'some_tag']
|
97
|
+
result[2].should == [:static , 'middle']
|
98
|
+
result[3].should == [:dynamic, 'another_tag']
|
99
|
+
result[4].should == [:static, ' end']
|
100
|
+
|
101
|
+
# Case: two consecutive tags
|
102
|
+
sample_text = 'begin <some_tag><another_tag> end'
|
103
|
+
result = parse_it(sample_text)
|
104
|
+
|
105
|
+
# Expectation: an array with four couples:
|
106
|
+
result.should have(4).items
|
107
|
+
result[0].should == [:static, 'begin ']
|
108
|
+
result[1].should == [:dynamic, 'some_tag']
|
109
|
+
result[2].should == [:dynamic, 'another_tag']
|
110
|
+
result[3].should == [:static, ' end']
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should parse a text line with escaped chevrons" do
|
114
|
+
sample_text = 'Mary has a \<little\> lamb'
|
115
|
+
result = parse_it(sample_text)
|
116
|
+
|
117
|
+
# Expectation: an array with one couple: [:static, the source text]
|
118
|
+
result.should have(1).items
|
119
|
+
result[0].should == [:static, sample_text]
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should parse a text line with escaped chevrons in a tag" do
|
123
|
+
sample_text = 'begin <some_\<\\>weird\>_tag> end'
|
124
|
+
result = parse_it(sample_text)
|
125
|
+
|
126
|
+
# Expectation: an array with three couples:
|
127
|
+
result.should have(3).items
|
128
|
+
result[0].should == [:static, 'begin ']
|
129
|
+
result[1].should == [:dynamic, 'some_\<\\>weird\>_tag']
|
130
|
+
result[2].should == [:static, ' end']
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should complain if a tag misses an closing chevron" do
|
134
|
+
sample_text = 'begin <some_tag\> end'
|
135
|
+
error_message = "Missing closing chevron '>'."
|
136
|
+
lambda { parse_it(sample_text) }.should raise_error(StandardError, error_message)
|
137
|
+
end
|
138
|
+
|
139
|
+
end # context
|
140
|
+
|
141
|
+
context "Creation and initialization" do
|
142
|
+
|
143
|
+
it "should accept an empty template text" do
|
144
|
+
lambda { TemplateEngine.new '' }.should_not raise_error
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should be created with a template text" do
|
148
|
+
lambda { TemplateEngine.new sample_template }.should_not raise_error
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should know the source text" do
|
152
|
+
subject.source.should == sample_template
|
153
|
+
|
154
|
+
# Case of an empty template
|
155
|
+
instance = TemplateEngine.new ''
|
156
|
+
instance.source.should be_empty
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "Provided services" do
|
161
|
+
|
162
|
+
it "should know the variable(s) it contains" do
|
163
|
+
subject.variables == [:userid, :password]
|
164
|
+
|
165
|
+
# Case of an empty source template text
|
166
|
+
instance = TemplateEngine.new ''
|
167
|
+
instance.variables.should be_empty
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should render the text given the actuals" do
|
171
|
+
locals = {'userid' => "johndoe"}
|
172
|
+
|
173
|
+
rendered_text = subject.render(nil, locals)
|
174
|
+
expected = <<-SNIPPET
|
175
|
+
Given I landed in the homepage
|
176
|
+
# The credentials are entered here
|
177
|
+
And I fill in "Username" with "johndoe"
|
178
|
+
And I fill in "Password" with ""
|
179
|
+
And I click "Sign in"
|
180
|
+
SNIPPET
|
181
|
+
|
182
|
+
# Place actual value in context object
|
183
|
+
Context = Struct.new(:userid, :password)
|
184
|
+
context = Context.new("sherlock", "holmes")
|
185
|
+
rendered_text = subject.render(context, {'userid' => 'susan'})
|
186
|
+
expected = <<-SNIPPET
|
187
|
+
Given I landed in the homepage
|
188
|
+
# The credentials are entered here
|
189
|
+
And I fill in "Username" with "susan"
|
190
|
+
And I fill in "Password" with "holmes"
|
191
|
+
And I click "Sign in"
|
192
|
+
SNIPPET
|
193
|
+
|
194
|
+
|
195
|
+
# Case of an empty source template text
|
196
|
+
instance = TemplateEngine.new ''
|
197
|
+
instance.render(nil, {}).should be_empty
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end # describe
|
202
|
+
|
203
|
+
end # module
|
204
|
+
|
205
|
+
# End of file
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: macros4cuke
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.00
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -43,25 +43,10 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
-
- !ruby/object:Gem::Dependency
|
47
|
-
name: mustache
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
|
-
requirements:
|
51
|
-
- - ! '>='
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
54
|
-
type: :runtime
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
|
-
requirements:
|
59
|
-
- - ! '>='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
46
|
description: ! "\tMacros4Cuke is a lightweight library that adds a macro facility
|
63
|
-
your Cucumber scenarios.\n In short, you can create new
|
64
|
-
of lower-level steps
|
47
|
+
your Cucumber scenarios.\n In short, you can create any new step that replaces
|
48
|
+
a sequence of lower-level steps.\n All this can be done directly in your feature
|
49
|
+
files without programming step definitions.\n"
|
65
50
|
email: famished.tiger@yahoo.com
|
66
51
|
executables: []
|
67
52
|
extensions: []
|
@@ -88,6 +73,9 @@ files:
|
|
88
73
|
- features/step_definitions/use_macro_steps.rb
|
89
74
|
- features/support/env.rb
|
90
75
|
- features/support/macro_support.rb
|
76
|
+
- spec/spec_helper.rb
|
77
|
+
- spec/macros4cuke/macro-step_spec.rb
|
78
|
+
- spec/macros4cuke/template-engine_spec.rb
|
91
79
|
homepage: https://github.com/famished-tiger/Macros4Cuke
|
92
80
|
licenses: []
|
93
81
|
post_install_message: ! '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|