qed 2.3.0 → 2.4.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 (50) hide show
  1. data/History.rdoc +15 -0
  2. data/eg/hello_world.rdoc +15 -0
  3. data/eg/view_error.rdoc +21 -0
  4. data/eg/website.rdoc +12 -0
  5. data/lib/qed/advice.rb +4 -3
  6. data/lib/qed/command.rb +2 -3
  7. data/lib/qed/core_ext/instance_exec.rb +36 -0
  8. data/lib/qed/demo.rb +0 -1
  9. data/lib/qed/evaluator.rb +75 -36
  10. data/lib/qed/extensions/filefixtures.rb +27 -0
  11. data/lib/qed/extensions/shell_session.rb +2 -0
  12. data/lib/qed/meta/data.rb +29 -0
  13. data/lib/qed/meta/gemfile +10 -0
  14. data/{PROFILE → lib/qed/meta/profile} +0 -0
  15. data/lib/qed/parser.rb +148 -74
  16. data/lib/qed/reporter/abstract.rb +175 -25
  17. data/lib/qed/reporter/bullet.rb +14 -10
  18. data/lib/qed/reporter/dotprogress.rb +26 -9
  19. data/lib/qed/reporter/verbatim.rb +36 -15
  20. data/lib/qed/scope.rb +18 -8
  21. data/lib/qed/session.rb +2 -2
  22. data/meta/data.rb +29 -0
  23. data/meta/gemfile +10 -0
  24. data/{lib/qed/profile.yml → meta/profile} +0 -0
  25. data/{demo → qed}/01_demos.rdoc +7 -13
  26. data/{demo → qed}/02_advice.rdoc +7 -7
  27. data/{demo → qed}/03_helpers.rdoc +2 -2
  28. data/{demo → qed}/04_samples.rdoc +0 -0
  29. data/{demo → qed}/05_quote.rdoc +0 -0
  30. data/{demo → qed}/07_toplevel.rdoc +0 -0
  31. data/{demo → qed}/08_cross_script.rdoc +0 -2
  32. data/{demo → qed}/09_cross_script.rdoc +5 -3
  33. data/{demo → qed}/10_constant_lookup.rdoc +0 -0
  34. data/{demo → qed}/applique/constant.rb +0 -0
  35. data/{demo → qed}/applique/env.rb +0 -0
  36. data/{demo → qed}/applique/fileutils.rb +0 -0
  37. data/{demo → qed}/applique/markup.rb +0 -0
  38. data/{demo → qed}/applique/quote.rb +0 -0
  39. data/{demo → qed}/applique/toplevel.rb +0 -0
  40. data/{demo → qed}/helpers/advice.rb +0 -0
  41. data/{demo → qed}/helpers/sample.rb +0 -0
  42. data/{demo → qed}/helpers/toplevel.rb +0 -0
  43. data/{demo → qed}/samples/data.txt +0 -0
  44. data/{demo → qed}/samples/table.yml +0 -0
  45. metadata +43 -38
  46. data/REQUIRE +0 -7
  47. data/VERSION +0 -5
  48. data/lib/qed/package.yml +0 -5
  49. data/script/qedoc +0 -2
  50. data/script/test +0 -4
@@ -1,5 +1,20 @@
1
1
  = RELEASE HISTORY
2
2
 
3
+ == 2.4.0 / 2010-09-02
4
+
5
+ All engines go! QED has not been tested against 1.8.6, 1.8.7 and 1.9.2.
6
+ Underthehood steps are not organized in doubely-linked lists, which makes
7
+ them much more robust and flexible. This release also improves scoping,
8
+ test counts, and inline documentation parsing.
9
+
10
+ Changes:
11
+
12
+ * Use new doubly-linked list step design.
13
+ * Fix -r option on command line.
14
+ * Provide #instance_exec core extension for Ruby 1.8.6.
15
+ * Scope is extended by and includes applique.
16
+
17
+
3
18
  == 2.3.0 / 2010-07-14
4
19
 
5
20
  Bug to the scurry! QED has broken through the code/document ceiling and
@@ -0,0 +1,15 @@
1
+ = Hello World
2
+
3
+ Did you know that famous `Hello World` moniker is
4
+ eleven characters long?
5
+
6
+ "Hello World".size.assert == 11
7
+
8
+ To pass a piece of literal text on with a description
9
+ we simply need to end it with a ...
10
+
11
+ Now this text will appear verbatim.
12
+ In the applique arguments.
13
+
14
+ That's all.
15
+
@@ -0,0 +1,21 @@
1
+ = Examples of Failure
2
+
3
+ This document is here simply to demonstrate what
4
+ a failed and error raising code steps looks like.
5
+
6
+ When run with the -v (verbatim) option, for instance, +qed+
7
+ will highlight the following sections in red and give a brief
8
+ error message.
9
+
10
+ == Failure
11
+
12
+ This step demonstrates a failed assertion.
13
+
14
+ 1.assert == 2
15
+
16
+ == Error
17
+
18
+ This step demonstrates a raised error.
19
+
20
+ raise "Just because"
21
+
@@ -0,0 +1,12 @@
1
+ = Addition
2
+
3
+ require 'calculator'
4
+ calculator = Caclulater.new
5
+
6
+ A Calculator can add two numbers.
7
+
8
+ calculator.push 2
9
+ calculator.push 2
10
+ calculator.add
11
+ calculator.output.assert == 4
12
+
@@ -1,3 +1,5 @@
1
+ require 'qed/core_ext/instance_exec'
2
+
1
3
  module QED
2
4
 
3
5
  # = Advice
@@ -12,8 +14,7 @@ module QED
12
14
  #
13
15
  # == Pattern Matchers (When)
14
16
  #
15
- # Matchers are evaluated when they match a blocks
16
- # commentary.
17
+ # Matchers are evaluated when they match a description.
17
18
  #
18
19
  # == Event Signals (Before, After)
19
20
  #
@@ -67,7 +68,7 @@ module QED
67
68
 
68
69
  #
69
70
  def call_matchers(scope, section)
70
- match = section.commentary
71
+ match = section.text
71
72
  args = section.arguments
72
73
  matchers.each do |(patterns, proc)|
73
74
  compare = match
@@ -134,7 +134,7 @@ module QED
134
134
  @options[:loadpath] ||= []
135
135
  @options[:loadpath].concat(arg.split(/[:;]/).map{ |dir| File.expand_path(dir) })
136
136
  end
137
- opt.on('--require', "-r", "require library") do |arg|
137
+ opt.on('--require', "-r LIB", "require library") do |arg|
138
138
  @options[:requires] ||= []
139
139
  @options[:requires].concat(arg.split(/[:;]/)) #.map{ |dir| File.expand_path(dir) })
140
140
  end
@@ -207,7 +207,7 @@ module QED
207
207
  end
208
208
  end
209
209
  files = files.flatten.uniq
210
- files.map{|f| File.expand_path(f) }.sort
210
+ files.map{|f| File.expand_path(f) }.uniq.sort
211
211
  end
212
212
 
213
213
  # Parse command-line options along with profile options.
@@ -349,4 +349,3 @@ module QED
349
349
  end
350
350
 
351
351
  end
352
-
@@ -0,0 +1,36 @@
1
+ class Object
2
+
3
+ unless method_defined?(:instance_exec) # 1.9
4
+ require 'thread'
5
+
6
+ module InstanceExecMethods #:nodoc:
7
+ end
8
+
9
+ include InstanceExecMethods
10
+
11
+ # Evaluate the block with the given arguments within the context of
12
+ # this object, so self is set to the method receiver.
13
+ #
14
+ # From Mauricio's http://eigenclass.org/hiki/bounded+space+instance_exec
15
+ #
16
+ # This version has been borrowed from Rails for compatibility sake.
17
+ def instance_exec(*args, &block)
18
+ begin
19
+ old_critical, Thread.critical = Thread.critical, true
20
+ n = 0
21
+ n += 1 while respond_to?(method_name = "__instance_exec#{n}")
22
+ InstanceExecMethods.module_eval { define_method(method_name, &block) }
23
+ ensure
24
+ Thread.critical = old_critical
25
+ end
26
+
27
+ begin
28
+ send(method_name, *args)
29
+ ensure
30
+ InstanceExecMethods.module_eval { remove_method(method_name) } rescue nil
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -93,4 +93,3 @@ module QED
93
93
  end
94
94
 
95
95
  end
96
-
@@ -8,7 +8,7 @@ module QED
8
8
  #
9
9
  def initialize(script, *observers)
10
10
  @script = script
11
- @ast = script.parse
11
+ @steps = script.parse
12
12
 
13
13
  #@file = script.file
14
14
  #@scope = script.scope
@@ -21,46 +21,85 @@ module QED
21
21
  #
22
22
  def run
23
23
  advise!(:before_demo, @script)
24
- process
24
+ advise!(:demo, @script)
25
+ run_steps
25
26
  advise!(:after_demo, @script)
26
27
  end
27
28
 
28
29
  #
29
- def process
30
- @ast.each do |section|
31
- evaluate(section)
30
+ def run_steps #process
31
+ @steps.each do |step|
32
+ evaluate(step)
32
33
  end
33
34
  end
34
35
 
35
- # Evaluate a demo section.
36
- def evaluate(section)
37
- advise!(:text, section) # TODO: rename to :step?
38
- evaluate_links(section)
39
- advise!(:before_step, section, @script.file)
36
+ #
37
+ def evaluate(step)
38
+ type = step.type
39
+ advise!(:before_step, step) #, @script.file)
40
+ advise!("before_#{type}".to_sym, step) #, @script.file)
41
+ case type
42
+ when :head
43
+ evaluate_head(step)
44
+ when :desc
45
+ evaluate_desc(step)
46
+ when :data
47
+ evaluate_data(step)
48
+ when :code
49
+ evaluate_code(step)
50
+ else
51
+ raise "fatal: unknown #{type}"
52
+ end
53
+ advise!("after_#{type}".to_sym, step) #, @script.file)
54
+ advise!(:after_step, step) #, @script.file)
55
+ end
56
+
57
+ #
58
+ def evaluate_head(step)
59
+ advise!(:head, step)
60
+ end
61
+
62
+ #
63
+ def evaluate_desc(step)
64
+ evaluate_links(step)
40
65
  begin
41
- advise!(:when, section)
42
- # TODO: how to handle catching asserts in advice?
66
+ advise!(:desc, step)
67
+ advise!(:when, step) # triggers matchers
68
+ rescue SystemExit
69
+ pass!(step)
70
+ rescue Assertion => exception
71
+ fail!(step, exception)
72
+ rescue Exception => exception
73
+ error!(step, exception)
74
+ else
75
+ pass!(step)
43
76
  end
44
- if section.code?
45
- begin
46
- advise!(:code, section)
47
- @script.evaluate(section.eval_code, section.lineno)
48
- rescue SystemExit
49
- pass!(section)
50
- rescue Assertion => exception
51
- fail!(section, exception)
52
- rescue Exception => exception
53
- error!(section, exception)
54
- else
55
- pass!(section)
56
- end
77
+ end
78
+
79
+ #
80
+ def evaluate_data(step)
81
+ advise!(:data, step)
82
+ end
83
+
84
+ # Evaluate a demo step.
85
+ def evaluate_code(step)
86
+ begin
87
+ advise!(:code, step)
88
+ @script.evaluate(step.code, step.lineno)
89
+ rescue SystemExit
90
+ pass!(step)
91
+ rescue Assertion => exception
92
+ fail!(step, exception)
93
+ rescue Exception => exception
94
+ error!(step, exception)
95
+ else
96
+ pass!(step)
57
97
  end
58
- advise!(:after_step, section, @script.file)
59
98
  end
60
99
 
61
- # TODO: Not sure how to handle loading links in comment mode.
62
- def evaluate_links(section)
63
- section.commentary.scan(/\[qed:\/\/(.*?)\]/) do |match|
100
+ # TODO: Not sure how to handle loading links in --comment runner mode.
101
+ def evaluate_links(step)
102
+ step.text.scan(/\[qed:\/\/(.*?)\]/) do |match|
64
103
  file = $1
65
104
  # relative to demo script
66
105
  if File.exist?(File.join(@script.directory,file))
@@ -77,25 +116,25 @@ module QED
77
116
  end
78
117
 
79
118
  #
80
- def pass!(section)
81
- advise!(:pass, section)
119
+ def pass!(step)
120
+ advise!(:pass, step)
82
121
  end
83
122
 
84
123
  #
85
- def fail!(section, exception)
86
- advise!(:fail, section, exception)
124
+ def fail!(step, exception)
125
+ advise!(:fail, step, exception)
87
126
  #raise exception
88
127
  end
89
128
 
90
129
  #
91
- def error!(section, exception)
92
- advise!(:error, section, exception)
130
+ def error!(step, exception)
131
+ advise!(:error, step, exception)
93
132
  #raise exception
94
133
  end
95
134
 
96
135
  #
97
136
  def import!(file)
98
- advise!(:unload)
137
+ advise!(:unload) # should this also occur just befor after_demo ?
99
138
  eval(File.read(file), @script.binding, file)
100
139
  advise!(:load, file)
101
140
  end
@@ -0,0 +1,27 @@
1
+ # This extension provides a simple means for
2
+ # create file-system fixtures.
3
+
4
+ require 'erb'
5
+
6
+ # Set global temporary directory.
7
+ $tmpdir = 'tmp'
8
+
9
+ #
10
+ def copy_fixture(name, tmpdir=$tmpdir)
11
+ FileUtils.mkdir(tmpdir)
12
+ srcdir = File.join(demo_directory, 'fixtures', name)
13
+ paths = Dir.glob(File.join(srcdir, '**', '*'), File::FNM_DOTMATCH)
14
+ paths.each do |path|
15
+ basename = File.basename(path)
16
+ next if basename == '.'
17
+ next if basename == '..'
18
+ dest = File.join(tmpdir, path.sub(srcdir+'/', ''))
19
+ if File.directory?(path)
20
+ FileUtils.mkdir(dest)
21
+ else
22
+ text = ERB.new(File.read(path)).result
23
+ File.open(dest, 'w'){ |f| f << text }
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,2 @@
1
+ # This extension provides a basic means for testing
2
+ # shell commands.
@@ -0,0 +1,29 @@
1
+ Object.__send__(:remove_const, :VERSION) if Object.const_defined?(:VERSION) # becuase Ruby 1.8~ gets in the way
2
+
3
+ module QED
4
+
5
+ def self.__DIR__
6
+ File.dirname(__FILE__)
7
+ end
8
+
9
+ def self.gemfile
10
+ @gemfile ||= (
11
+ require 'yaml'
12
+ YAML.load(File.new(__DIR__ + '/gemfile'))
13
+ )
14
+ end
15
+
16
+ def self.profile
17
+ @profile ||= (
18
+ require 'yaml'
19
+ YAML.load(File.new(__DIR__ + '/profile'))
20
+ )
21
+ end
22
+
23
+ def self.const_missing(name)
24
+ key = name.to_s.downcase
25
+ gemfile[key] || profile[key] || super(name)
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,10 @@
1
+ name: qed
2
+ version: 2.4.0
3
+ date: 2010-09-02
4
+
5
+ requires:
6
+ - ansi
7
+ - facets
8
+ - ae (test)
9
+ - syckle (build)
10
+
File without changes
@@ -36,7 +36,7 @@ module QED
36
36
  when :comment
37
37
  parse_comment_lines
38
38
  else
39
- index = -1
39
+ index = 0 #-1
40
40
  File.readlines(file).to_a.map do |line|
41
41
  [index += 1, line]
42
42
  end
@@ -46,25 +46,32 @@ module QED
46
46
  # TODO: It would be nice if we could get ther require statement for the
47
47
  # comment mode to be relative to an actual loadpath.
48
48
  def parse_comment_lines
49
- omit = false
49
+ ruby_omit = false
50
+ rdoc_omit = false
50
51
  lines = [
51
52
  [0, "Load #{File.basename(file)} script.\n"],
52
53
  [0, "\n"],
53
54
  [0, " require '#{file}'\n"]
54
55
  ]
55
- index = 0
56
+ index = 1
56
57
  File.readlines(file).each do |l|
57
58
  case l
59
+ when /^=begin(?!\s+qed)/
60
+ ruby_omit = true
61
+ when /^=end/
62
+ ruby_omit = false
58
63
  when /^\s*\#\-\-\s*$/
59
- omit = true
64
+ rdoc_omit = true
60
65
  when /^\s*\#\+\+\s*$/
61
- omit = false
62
- when /^\s*\#\ \-\-/ # ?
63
- # -- skip internal comments
64
- when /^\s*\#/
65
- lines << [index, l.lstrip.sub(/^\#\ ?/, '')] unless omit
66
+ rdoc_omit = false
67
+ ##when /^\s*\#\ \-\-/ # not needed just double comment
68
+ ## # -- skip internal comments
69
+ when /^\s*##/
70
+ ## skip internal comments
71
+ when /^\s*\#/
72
+ lines << [index, l.lstrip.sub(/^\#\ ?/, '')] unless (ruby_omit or rdoc_omit)
66
73
  else
67
- lines << [index, "\n"] unless lines.last[1] == "\n"
74
+ lines << [index, "\n"] unless lines.last[1] == "\n" unless (ruby_omit or rdoc_omit)
68
75
  end
69
76
  index += 1
70
77
  end
@@ -135,33 +142,37 @@ module QED
135
142
 
136
143
  def parse
137
144
  tree = []
138
- mode = :rem
145
+ flush = true
139
146
  pend = false
140
- block = Block.new
147
+ block = Block.new(file)
141
148
  lines.each do |lineno, line|
142
149
  case line
143
150
  when /^\s*$/
144
- case mode
145
- when :rem
146
- pend = true unless line == 0
147
- block.rem << [lineno, line]
148
- when :raw
151
+ if flush
152
+ pend = true unless lineno == 0
153
+ block.raw << [lineno, line]
154
+ else
149
155
  block.raw << [lineno, line]
150
156
  end
151
157
  when /\A\s+/
152
- mode = :raw
158
+ if flush
159
+ tree << block.ready!(flush, tree.last)
160
+ block = Block.new(file)
161
+ end
162
+ pend = false
163
+ flush = false
153
164
  block.raw << [lineno, line]
154
165
  else
155
- if pend || mode == :raw
156
- pend = false
157
- mode = :rem
158
- tree << block.ready!
159
- block = Block.new
166
+ if pend || !flush
167
+ tree << block.ready!(flush, tree.last)
168
+ pend = false
169
+ flush = true
170
+ block = Block.new(file)
160
171
  end
161
- block.rem << [lineno, line]
172
+ block.raw << [lineno, line]
162
173
  end
163
174
  end
164
- tree << block.ready!
175
+ tree << block.ready!(flush, tree.last)
165
176
  @ast = tree
166
177
  end
167
178
 
@@ -176,97 +187,160 @@ module QED
176
187
 
177
188
  # Section Block
178
189
  class Block
179
- # Block commentary.
180
- attr :rem
181
-
182
190
  # Block raw code/text.
183
191
  attr :raw
184
192
 
193
+ # previous block
194
+ attr :back_step
195
+
196
+ # next block
197
+ attr :next_step
198
+
185
199
  #
186
- def initialize
187
- @rem = []
188
- @raw = []
189
- @has_code = true
200
+ def initialize(file)
201
+ @file = file
202
+ @raw = []
203
+ @type = :description
204
+ @back_step = nil
205
+ @next_step = nil
190
206
  end
191
207
 
192
208
  #
193
- def ready!
194
- @commentary = rem.map{ |lineno, line| line }.join
195
- @example = raw.map{ |lineno, line| line }.join
196
- @has_code = false if @raw.empty?
197
- @has_code = false if continuation?
209
+ def ready!(flush, back_step)
210
+ @flush = flush
211
+ @back_step = back_step
212
+
213
+ @text = raw.map{ |lineno, line| line }.join
214
+ @type = parse_type
215
+
216
+ @back_step.next_step = self if @back_step
217
+
198
218
  self
199
219
  end
200
220
 
201
221
  #
202
- def commentary
203
- @commentary
222
+ def to_s
223
+ case type
224
+ when :description
225
+ text
226
+ else
227
+ text
228
+ end
229
+ end
230
+
231
+ #
232
+ def text
233
+ @text
204
234
  end
205
235
 
206
236
  #
207
- def example
208
- @example
237
+ def flush?
238
+ @flush
209
239
  end
210
240
 
211
241
  # Returns an Array of prepared example text
212
242
  # for use in advice.
213
243
  def arguments
214
- continuation? ? [example_argument] : []
244
+ if next_step && next_step.data?
245
+ [next_step.sample_text]
246
+ else
247
+ []
248
+ end
215
249
  end
216
250
 
217
- #
218
- def code?
219
- @has_code
251
+ # What type of block is this?
252
+ def type
253
+ @type
220
254
  end
221
255
 
222
- # First line of example text.
223
- def lineno
224
- @line ||= @raw.first.first
225
- end
256
+ #
257
+ def head? ; @type == :head ; end
226
258
 
227
259
  #
228
- def code
229
- @example
230
- end
260
+ def desc? ; @type == :desc ; end
231
261
 
232
262
  #
233
- def eval_code
234
- @eval_code ||= tweak_code
263
+ def code? ; @type == :code ; end
264
+
265
+ # Any commentary ending in `...` or `:` will mark the following
266
+ # block as a plain text *sample* and not example code to be evaluated.
267
+ def data? ; @type == :data ; end
268
+
269
+ #
270
+ alias_method :header?, :head?
271
+
272
+ #
273
+ alias_method :description?, :desc?
274
+
275
+
276
+ # First line of example text.
277
+ def lineno
278
+ @line ||= @raw.first.first
235
279
  end
236
280
 
237
281
  #
238
- def tweak_code
239
- code = example.dup
240
- code.gsub!(/\n\s*\#\ ?\=\>/, '.assert == ')
241
- code.gsub!(/\s*\#\ ?\=\>/, '.assert == ')
242
- code
282
+ def code
283
+ @code ||= tweak_code
243
284
  end
244
285
 
245
286
  # Clean up the example text, removing unccesseary white lines
246
287
  # and triple quote brackets, but keep indention intact.
247
- def clean_example
248
- text = example.chomp.sub(/\A\n/,'')
249
- if md = /\A["]{3,}(.*?)["]{3,}\Z/.match(text)
250
- text = md[1]
288
+ def clean_text
289
+ str = text.chomp.sub(/\A\n/,'')
290
+ if md = /\A["]{3,}(.*?)["]{3,}\Z/.match(str)
291
+ str = md[1]
251
292
  end
252
- text.rstrip
293
+ str.rstrip
253
294
  end
254
295
 
255
- # When the example is raw text and passed to an adivce block, this
296
+ # When the text is sample text and passed to an adivce block, this
256
297
  # provides the prepared form of the example text, removing white lines,
257
298
  # triple quote brackets and indention.
258
- def example_argument
259
- text = example.tabto(0).chomp.sub(/\A\n/,'')
260
- if md = /\A["]{3,}(.*?)["]{3,}\Z/.match(text)
261
- text = md[1]
299
+ def sample_text
300
+ str = text.tabto(0).chomp.sub(/\A\n/,'')
301
+ if md = /\A["]{3,}(.*?)["]{3,}\Z/.match(str)
302
+ str = md[1]
303
+ end
304
+ str.rstrip
305
+ end
306
+
307
+ # TODO: object_hexid
308
+ def inspect
309
+ %[#<Block:#{object_id} "#{text[0..25]} ...">]
310
+ end
311
+
312
+ protected
313
+
314
+ #
315
+ def next_step=(n)
316
+ @next_step = n
317
+ end
318
+
319
+ private
320
+
321
+ #
322
+ def parse_type
323
+ if flush?
324
+ if /\A[=#]/ =~ text
325
+ :head
326
+ else
327
+ :desc
328
+ end
329
+ else
330
+ if back_step && /(\.\.\.|\:)\s*\Z/m =~ back_step.text.strip
331
+ :data
332
+ else
333
+ :code
334
+ end
262
335
  end
263
- text.rstrip
264
336
  end
265
337
 
266
- # And commentary ending in `...` or `:` will mark the following
267
- # example as plain text and not code to be evaluated.
268
- def continuation?
269
- /(\.\.\.|\:)\s*\Z/m =~ commentary
338
+ #
339
+ def tweak_code
340
+ code = text.dup
341
+ code.gsub!(/\n\s*\#\ ?\=\>/, '.assert = ')
342
+ code.gsub!(/\s*\#\ ?\=\>/, '.assert = ')
343
+ code
270
344
  end
271
345
 
272
346
  end