macros4cuke 0.1.07 → 0.2.00
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: ! '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|