lucid 0.1.1 → 0.2.0

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.
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