lucid 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/HISTORY.md +8 -0
  4. data/LICENSE +3 -0
  5. data/README.md +11 -2
  6. data/Rakefile +10 -1
  7. data/lib/autotest/discover.rb +5 -5
  8. data/lib/autotest/lucid_mixin.rb +34 -35
  9. data/lib/lucid/ast/table.rb +5 -4
  10. data/lib/lucid/cli/configuration.rb +0 -6
  11. data/lib/lucid/cli/options.rb +3 -12
  12. data/lib/lucid/formatter/condensed.rb +46 -0
  13. data/lib/lucid/formatter/html.rb +9 -3
  14. data/lib/lucid/formatter/junit.rb +6 -8
  15. data/lib/lucid/formatter/standard.rb +1 -7
  16. data/lib/lucid/generators/project/events-symbiont.rb +1 -1
  17. data/lib/lucid/platform.rb +1 -1
  18. data/lib/lucid/runtime.rb +1 -1
  19. data/lib/lucid/sequence.rb +5 -0
  20. data/lib/lucid/sequence/sequence_errors.rb +64 -0
  21. data/lib/lucid/sequence/sequence_group.rb +35 -0
  22. data/lib/lucid/sequence/sequence_phrase.rb +166 -0
  23. data/lib/lucid/sequence/sequence_steps.rb +20 -0
  24. data/lib/lucid/sequence/sequence_support.rb +26 -0
  25. data/lib/lucid/sequence/sequence_template.rb +354 -0
  26. data/lib/lucid/spec_file.rb +3 -1
  27. data/lib/lucid/step_match.rb +1 -1
  28. data/lib/lucid/wire_support/wire_packet.rb +1 -1
  29. data/lucid.gemspec +11 -9
  30. data/spec/lucid/app_spec.rb +42 -0
  31. data/spec/lucid/configuration_spec.rb +112 -0
  32. data/spec/lucid/sequences/sequence_conditional_spec.rb +74 -0
  33. data/spec/lucid/sequences/sequence_group_spec.rb +55 -0
  34. data/spec/lucid/sequences/sequence_phrase_spec.rb +122 -0
  35. data/spec/lucid/sequences/sequence_placeholder_spec.rb +56 -0
  36. data/spec/lucid/sequences/sequence_section_spec.rb +61 -0
  37. data/spec/lucid/sequences/sequence_support_spec.rb +65 -0
  38. data/spec/lucid/sequences/sequence_template_spec.rb +298 -0
  39. data/spec/spec_helper.rb +13 -0
  40. metadata +86 -54
  41. data/.rspec +0 -1
@@ -2,7 +2,7 @@ AfterConfiguration do |config|
2
2
  puts("Specs are being executed from: #{config.spec_location}")
3
3
  end
4
4
 
5
- Before('~@practice') do
5
+ Before('~@practice','~@sequence') do
6
6
  @browser = Symbiont::Browser.start
7
7
  end
8
8
 
@@ -2,7 +2,7 @@ require 'rbconfig'
2
2
 
3
3
  module Lucid
4
4
  unless defined?(Lucid::VERSION)
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.0'
6
6
  BINARY = File.expand_path(File.dirname(__FILE__) + '/../../bin/lucid')
7
7
  LIBDIR = File.expand_path(File.dirname(__FILE__) + '/../../lib')
8
8
  JRUBY = defined?(JRUBY_VERSION)
data/lib/lucid/runtime.rb CHANGED
@@ -13,7 +13,7 @@ require 'lucid/runtime/orchestrator'
13
13
 
14
14
  module Lucid
15
15
  class Runtime
16
- attr_reader :results
16
+ attr_reader :results, :orchestrator
17
17
 
18
18
  include Formatter::Duration
19
19
  include Runtime::InterfaceIO
@@ -0,0 +1,5 @@
1
+ # This file is the starting point for the Sequence functionality.
2
+ # It will only reference files in the sequence directory and does
3
+ # not impact the core runtime at all.
4
+
5
+ require 'lucid/sequence/sequence_support'
@@ -0,0 +1,64 @@
1
+ module Sequence
2
+
3
+ class SequenceError < StandardError
4
+ end
5
+
6
+ class DuplicateSequenceError < SequenceError
7
+ def initialize(phrase)
8
+ super("A sequence with phrase '#{phrase}' already exists.")
9
+ end
10
+ end
11
+
12
+ class UnknownSequenceError < SequenceError
13
+ def initialize(phrase)
14
+ super("Unknown sequence step with phrase: '#{phrase}'.")
15
+ end
16
+ end
17
+
18
+ class EmptyParameterError < SequenceError
19
+ def initialize(text)
20
+ super("An empty or blank parameter occurred in '#{text}'.")
21
+ end
22
+ end
23
+
24
+ class InvalidElementError < SequenceError
25
+ def initialize(tag, invalid_element)
26
+ msg = "The invalid element '#{invalid_element}' occurs in the parameter '#{tag}'."
27
+ super(msg)
28
+ end
29
+ end
30
+
31
+ class UselessPhraseParameter < SequenceError
32
+ def initialize(param)
33
+ super("The phrase parameter '#{param}' does not appear in any step.")
34
+ end
35
+ end
36
+
37
+ class DataTableNotFound < SequenceError
38
+ def initialize(phrase)
39
+ msg = "The step with phrase [#{phrase}]: requires a data table."
40
+ super(msg)
41
+ end
42
+ end
43
+
44
+ class UnknownParameterError < SequenceError
45
+ def initialize(name)
46
+ super("Unknown sequence step parameter '#{name}'.")
47
+ end
48
+ end
49
+
50
+ class AmbiguousParameterValue < SequenceError
51
+ def initialize(name, phrase, table)
52
+ msg = "The sequence parameter '#{name}' has value '#{phrase}' and '#{table}'."
53
+ super(msg)
54
+ end
55
+ end
56
+
57
+ class UnreachableStepParameter < SequenceError
58
+ def initialize(param)
59
+ msg = "The step parameter '#{param}' does not appear in the phrase."
60
+ super(msg)
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,35 @@
1
+ require 'singleton'
2
+ require 'lucid/sequence/sequence_phrase'
3
+
4
+ module Sequence
5
+ class SequenceGroup
6
+ include Singleton
7
+
8
+ def add_sequence(phrase, sequence, data)
9
+ new_sequence = SequencePhrase.new(phrase, sequence, data)
10
+ raise DuplicateSequenceError.new(phrase) if find_sequence(phrase, data)
11
+ sequence_steps[new_sequence.key] = new_sequence
12
+ end
13
+
14
+ def sequence_steps
15
+ @sequence_steps ||= {}
16
+ return @sequence_steps
17
+ end
18
+
19
+ def generate_steps(phrase, data = nil)
20
+ data_table =! data.nil?
21
+ sequence = find_sequence(phrase, data_table)
22
+ raise UnknownSequenceError.new(phrase) if sequence.nil?
23
+ return sequence.expand(phrase, data)
24
+ end
25
+
26
+ def clear
27
+ sequence_steps.clear
28
+ end
29
+
30
+ def find_sequence(phrase, data)
31
+ key = SequencePhrase.sequence_key(phrase, data, :invoke)
32
+ return sequence_steps[key]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,166 @@
1
+ require 'lucid/sequence/sequence_template'
2
+ require 'lucid/sequence/sequence_errors'
3
+
4
+ module Sequence
5
+ class SequencePhrase
6
+
7
+ attr_reader :key
8
+ attr_reader :values
9
+ attr_reader :template
10
+ attr_reader :phrase_params
11
+
12
+ ParameterConstant = { 'quotes' => '"""'}
13
+
14
+ def initialize(phrase, sequence, data)
15
+ @key = self.class.sequence_key(phrase, data, :define)
16
+ #puts "*** [Sequence Step] - Key: #{@key}"
17
+
18
+ @phrase_params = scan_parameters(phrase, :define)
19
+ #puts "*** [Sequence Step] - Phrase Params: #{@phrase_params}"
20
+
21
+ transformed_steps = preprocess(sequence)
22
+ #puts "*** [Sequence Step] - Steps: \n#{transformed_steps}"
23
+
24
+ @template = SequenceTemplate::Engine.new(transformed_steps)
25
+ #puts "\n*** [Sequence Step] - Template: #{@template.inspect}\n"
26
+
27
+ phrase_variables = template.variables
28
+ #puts "\n*** [Sequence Step] - Phrase Variables: #{phrase_variables}"
29
+
30
+ @values = validate_phrase_values(@phrase_params, phrase_variables)
31
+ @values.concat(phrase_variables)
32
+ #puts "*** [Sequence Step] - Values: #{@values}"
33
+
34
+ @values.uniq!
35
+ end
36
+
37
+ def self.sequence_key(phrase, data, mode)
38
+ new_phrase = phrase.strip
39
+
40
+ # These lines replace every series of whitespace with an underscore.
41
+ # For that to work, I have to make sure there are no existing
42
+ # underscore characters in the first place so any pre-existing
43
+ # underscores get removed first.
44
+ new_phrase.gsub!(/_/, '')
45
+ new_phrase.gsub!(/\s+/, '_')
46
+
47
+ pattern = case mode
48
+ when :define
49
+ /<(?:[^\\<>]|\\.)*>/
50
+ when :invoke
51
+ /"([^\\"]|\\.)*"/
52
+ end
53
+
54
+ # Here 'patterned' means that for a given phrase, any bit of text
55
+ # between quotes or chevrons is replaced by the letter X.
56
+ patterned = new_phrase.gsub(pattern, 'X')
57
+
58
+ key = patterned + (data ? '_T' : '')
59
+
60
+ return key
61
+ end
62
+
63
+ # "Scanning parameters" means that a phrase will be scanned to find any
64
+ # text between chevrons or double quotes. These will be placed into an
65
+ # array.
66
+ def scan_parameters(phrase, mode)
67
+ pattern = case mode
68
+ when :define
69
+ /<((?:[^\\<>]|\\.)*)>/
70
+ when :invoke
71
+ /"((?:[^\\"]|\\.)*)"/
72
+ end
73
+
74
+ result = phrase.scan(pattern)
75
+ params = result.flatten.compact
76
+
77
+ # Any escaped double quotes need to be replaced by a double quote.
78
+ params.map! { |item| item.sub(/\\"/, '"') } if mode == :invoke
79
+
80
+ return params
81
+ end
82
+
83
+ def preprocess(sequence)
84
+ # Split text into individual lines and make sure to remove any lines
85
+ # with hash style comments.
86
+ lines = sequence.split(/\r\n?|\n/)
87
+ processed = lines.reject { |line| line =~ /\s*#/ }
88
+
89
+ return processed.join("\n")
90
+ end
91
+
92
+ # The goal of this method is to check for various inconsistencies that
93
+ # can occur between parameter names in the sequence phrase and those
94
+ # in the actual steps.
95
+ def validate_phrase_values(params, phrase_variables)
96
+ # This covers the case when phrase names a parameter that never
97
+ # occurs in any of the steps.
98
+ params.each do |param|
99
+ unless phrase_variables.include? param
100
+ raise UselessPhraseParameter.new(param)
101
+ end
102
+ end
103
+
104
+ # This covers the case when a step has a parameter that never appears
105
+ # in the sequence phrase and when data table is not being used.
106
+ unless data_table_required?
107
+ phrase_variables.each do |variable|
108
+ unless params.include?(variable) || ParameterConstant.include?(variable)
109
+ raise UnreachableStepParameter.new(variable)
110
+ end
111
+ end
112
+ end
113
+
114
+ return phrase_params.dup
115
+ end
116
+
117
+ def data_table_required?
118
+ return key =~ /_T$/
119
+ end
120
+
121
+ def expand(phrase, data)
122
+ params = validate_params(phrase, data)
123
+ params = ParameterConstant.merge(params)
124
+ return template.output(nil, params)
125
+ end
126
+
127
+ def validate_params(phrase, data)
128
+ sequence_parameters = {}
129
+
130
+ quoted_values = scan_parameters(phrase, :invoke)
131
+ quoted_values.each_with_index do |val, index|
132
+ sequence_parameters[phrase_params[index]] = val
133
+ end
134
+
135
+ unless data.nil?
136
+ data.each do |row|
137
+ (key, value) = validate_row(row, sequence_parameters)
138
+ if sequence_parameters.include? key
139
+ if sequence_parameters[key].kind_of?(Array)
140
+ sequence_parameters[key] << value
141
+ else
142
+ sequence_parameters[key] = [sequence_parameters[key], value]
143
+ end
144
+ else
145
+ sequence_parameters[key] = value
146
+ end
147
+ end
148
+ end
149
+
150
+ return sequence_parameters
151
+ end
152
+
153
+ def validate_row(row, params)
154
+ (key, value) = row
155
+
156
+ raise UnknownParameterError.new(key) unless values.include? key
157
+
158
+ if (phrase_params.include? key) && (params[key] != value)
159
+ raise AmbiguousParameterValue.new(key, params[key], value)
160
+ end
161
+
162
+ return row
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,20 @@
1
+ # The only test definitions that should go in here are those that
2
+ # will build sequence steps. Meaning, each test step here is meant
3
+ # to be one that is equivalent to a sequence of test steps.
4
+
5
+ Given(/^the step "(?:Given|When|Then|\*) \[((?:[^\\\]]|\\.)+)\](:?)" is defined to mean:$/) do |phrase, table, sequence|
6
+ data_provided = (table == ':')
7
+ add_sequence(phrase, sequence, data_provided)
8
+ end
9
+
10
+ When(/^\[((?:[^\\\]]|\\.)+)\]$/) do |phrase|
11
+ invoke_sequence(phrase)
12
+ end
13
+
14
+ When(/^\[([^\]]+)\]:$/) do |phrase, data_table|
15
+ unless data_table.kind_of?(Lucid::AST::Table)
16
+ raise Sequence::DataTableNotFound.new(phrase)
17
+ end
18
+
19
+ invoke_sequence(phrase, data_table.raw)
20
+ end
@@ -0,0 +1,26 @@
1
+ require 'lucid/sequence/sequence_group'
2
+ require 'lucid/sequence/sequence_errors'
3
+
4
+ module Sequence
5
+ module SequenceSupport
6
+
7
+ def add_sequence(phrase, sequence, data)
8
+ SequenceGroup.instance.add_sequence(phrase, sequence, data)
9
+ end
10
+
11
+ def invoke_sequence(phrase, data = nil)
12
+ # It's necessary to generate textual versions of all the steps that
13
+ # are to be executed.
14
+ group = SequenceGroup.instance
15
+ generated_steps = group.generate_steps(phrase, data)
16
+
17
+ # This statement causes Lucid to execute the generated test steps.
18
+ steps(generated_steps)
19
+ end
20
+
21
+ def clear_sequences
22
+ SequenceGroup.instance.clear
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,354 @@
1
+ require 'strscan'
2
+
3
+ module Sequence
4
+ module SequenceTemplate
5
+
6
+ class StaticText
7
+ attr_reader :source
8
+
9
+ def initialize(source)
10
+ @source = source
11
+ end
12
+
13
+ def output(context, params)
14
+ return source
15
+ end
16
+ end
17
+
18
+ class UnaryElement
19
+ attr_reader :name
20
+
21
+ def initialize(name)
22
+ @name = name
23
+ end
24
+
25
+ def retrieve_value_from(context, params)
26
+ actual_value = params[name]
27
+
28
+ if actual_value.nil? && context.respond_to?(name.to_sym)
29
+ actual_value = context.send(name.to_sym)
30
+ end
31
+
32
+ return actual_value
33
+ end
34
+ end
35
+
36
+ class Placeholder < UnaryElement
37
+ def output(context, params)
38
+ actual_value = retrieve_value_from(context, params)
39
+
40
+ result = case actual_value
41
+ when NilClass
42
+ ''
43
+ when Array
44
+ actual_value.join('<br/>')
45
+ when String
46
+ actual_value
47
+ else
48
+ actual_value.to_s()
49
+ end
50
+
51
+ return result
52
+ end
53
+ end
54
+
55
+ class EOLine
56
+ def output(context, params)
57
+ return "\n"
58
+ end
59
+ end
60
+
61
+ class Section < UnaryElement
62
+ attr_reader :children
63
+
64
+ def initialize(name)
65
+ super(name)
66
+ @children = []
67
+ end
68
+
69
+ def add_child(child)
70
+ children << child
71
+ end
72
+
73
+ def variables
74
+ section_variables = children.each_with_object([]) do |child, result|
75
+ case child
76
+ when Placeholder
77
+ result << child.name
78
+ when Section
79
+ result.concat(child.variables)
80
+ else
81
+ # noop
82
+ end
83
+ end
84
+ return section_variables.flatten.uniq
85
+ end
86
+
87
+ def output(context, params)
88
+ msg = "Method Section.#{__method__} must be implemented in subclass."
89
+ raise NotImplementedError, msg
90
+ end
91
+ end
92
+
93
+ class ConditionalSection < Section
94
+ attr_reader :existence
95
+
96
+ def initialize(name, generate)
97
+ super(name)
98
+ @existence = generate
99
+ end
100
+
101
+ def output(context, params)
102
+ value = retrieve_value_from(context, params)
103
+ if (!value.nil? && existence) || (value.nil? && !existence)
104
+ result = children.each_with_object('') do |child, item|
105
+ item << child.output(context, params)
106
+ end
107
+ else
108
+ result = ''
109
+ end
110
+
111
+ return result
112
+ end
113
+
114
+ def to_s
115
+ return "<?#{name}>"
116
+ end
117
+ end
118
+
119
+ SectionEndMarker = Struct.new(:name)
120
+
121
+ class Engine
122
+ attr_reader :source
123
+
124
+ # Invalid internal elements refers to spaces, any punctuation sign or
125
+ # delimiter that is forbidden between chevrons <...> template tags.
126
+ InvalidInternalElements = begin
127
+ forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~"
128
+ all_escaped = []
129
+ forbidden.each_char() { |ch| all_escaped << Regexp.escape(ch) }
130
+ pattern = all_escaped.join('|')
131
+ Regexp.new(pattern)
132
+ end
133
+
134
+ def initialize(source)
135
+ @source = source
136
+
137
+ # The generated source contains an internal representation of the
138
+ # given template text.
139
+ @generated_source = generate(source)
140
+ end
141
+
142
+ # The parse mechanism is designed to break up TDL lines into static
143
+ # and dynamic components. Dynamic components will correspond to
144
+ # tagged elements of the string, which indicate parameters in the
145
+ # original TDL phrase.
146
+ def self.parse(line)
147
+ scanner = StringScanner.new(line)
148
+ result = []
149
+
150
+ until scanner.eos?
151
+ tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/)
152
+
153
+ unless tag_literal.nil?
154
+ result << [:dynamic, tag_literal.gsub(/^<|>$/, '')]
155
+ end
156
+
157
+ text_literal = scanner.scan(/(?:[^\\<>]|\\.)+/)
158
+ result << [:static, text_literal] unless text_literal.nil?
159
+
160
+ indicate_parsing_error(line) if tag_literal.nil? && text_literal.nil?
161
+ end
162
+
163
+ return result
164
+ end
165
+
166
+ def self.indicate_parsing_error(line)
167
+ # The regular expression will be looking to match \< or \>. Those are
168
+ # escaped chevrons and will be replaced.
169
+ no_escaped = line.gsub(/\\[<>]/, '--')
170
+ unbalance_count = 0
171
+
172
+ no_escaped.each_char do |ch|
173
+ case ch
174
+ when '<'
175
+ unbalance_count += 1
176
+ when '>'
177
+ unbalance_count -= 1
178
+ end
179
+
180
+ raise StandardError, "Nested opening chevron '<'." if unbalance_count > 1
181
+ raise StandardError, "Missing opening chevron '<'." if unbalance_count < 0
182
+ end
183
+
184
+ raise StandardError, "Missing closing chevron '>'." if unbalance_count == 1
185
+ end
186
+
187
+ def parse_element(text)
188
+ # Check if the text matched is a ? or a / character. If the next bit
189
+ # of text after the ? or / is a invalid element of if there is an
190
+ # invalid element at all, an error will be raised.
191
+ if text =~ /^[\?\/]/
192
+ matching = InvalidInternalElements.match(text[1..-1])
193
+ else
194
+ matching = InvalidInternalElements.match(text)
195
+ end
196
+
197
+ raise InvalidElementError.new(text, matching[0]) if matching
198
+
199
+ result = case text[0, 1]
200
+ when '/'
201
+ SectionEndMarker.new(text[1..-1])
202
+ when '?'
203
+ ConditionalSection.new(text[1..-1], true)
204
+ else
205
+ Placeholder.new(text)
206
+ end
207
+
208
+ return result
209
+ end
210
+
211
+ # This method is used to retrieve all of the variable elements, which
212
+ # will be placeholder names, that appear in the template.
213
+ def variables
214
+ @variables ||= begin
215
+ vars = @generated_source.each_with_object([]) do |element, result|
216
+ case element
217
+ when Placeholder
218
+ result << element.name
219
+ when Section
220
+ result.concat(element.variables)
221
+ else
222
+ # noop
223
+ end
224
+ end
225
+ vars.flatten.uniq
226
+ end
227
+ return @variables
228
+ end
229
+
230
+ # This general output method will provide a final template within
231
+ # the given scope object (Placeholder, StaticText, etc) and with
232
+ # any of the parameters specified.
233
+ def output(context, params)
234
+ return '' if @generated_source.empty?
235
+ result = @generated_source.each_with_object('') do |element, item|
236
+ item << element.output(context, params)
237
+ end
238
+ return result
239
+ end
240
+
241
+ # To "generate" means to create an internal representation of
242
+ # the template.
243
+ def generate(source)
244
+ input_lines = source.split(/\r\n?|\n/)
245
+
246
+ raw_lines = input_lines.map do |line|
247
+ line_items = self.class.parse(line)
248
+ line_items.each do |(kind, text)|
249
+ if (kind == :dynamic) && text.strip.empty?
250
+ raise EmptyParameterError.new(line.strip)
251
+ end
252
+ end
253
+ line_items
254
+ end
255
+
256
+ template_lines = raw_lines.map { |line| generate_line(line) }
257
+ return generate_sections(template_lines.flatten)
258
+ end
259
+
260
+ def generate_line(line)
261
+ line_rep = line.map { |item| generate_couple(item) }
262
+ section_item = nil
263
+
264
+ line_to_despace = line_rep.all? do |item|
265
+ case item
266
+ when StaticText
267
+ item.source =~ /\s+/
268
+ when Section, SectionEndMarker
269
+ if section_item.nil?
270
+ section_item = item
271
+ true
272
+ else
273
+ false
274
+ end
275
+ else
276
+ false
277
+ end
278
+ end
279
+
280
+ if line_to_despace && ! section_item.nil?
281
+ line_rep = [section_item]
282
+ else
283
+ line_rep_ending(line_rep)
284
+ end
285
+
286
+ return line_rep
287
+ end
288
+
289
+ def generate_couple(item)
290
+ (kind, text) = item
291
+
292
+ result = case kind
293
+ when :static
294
+ StaticText.new(text)
295
+ when :dynamic
296
+ parse_element(text)
297
+ end
298
+
299
+ return result
300
+ end
301
+
302
+ def generate_sections(sequence)
303
+ open_sections = []
304
+
305
+ generated = sequence.each_with_object([]) do |element, result|
306
+ case element
307
+ when Section
308
+ open_sections << element
309
+ when SectionEndMarker
310
+ validate_section_end(element, open_sections)
311
+ result << open_sections.pop()
312
+ else
313
+ if open_sections.empty?
314
+ result << element
315
+ else
316
+ open_sections.last.add_child(element)
317
+ end
318
+ end
319
+ end
320
+
321
+ unless open_sections.empty?
322
+ error_message = "Unterminated section #{open_sections.last}."
323
+ raise StandardError, error_message
324
+ end
325
+
326
+ return generated
327
+ end
328
+
329
+ def validate_section_end(marker, sections)
330
+ if sections.empty?
331
+ msg = "End of section </#{marker.name}> found while no corresponding section is open."
332
+ raise StandardError, msg
333
+ end
334
+
335
+ if marker.name != sections.last.name
336
+ msg = "End of section </#{marker.name}> does not match current section '#{sections.last.name}'."
337
+ raise StandardError, msg
338
+ end
339
+ end
340
+
341
+ def line_rep_ending(line)
342
+ if line.last.is_a?(SectionEndMarker)
343
+ section_end = line.pop()
344
+ line << EOLine.new
345
+ line << section_end
346
+ else
347
+ line << EOLine.new
348
+ end
349
+ end
350
+
351
+ end # class: Engine
352
+
353
+ end
354
+ end