bryanlarsen-rubydoctest 1.0.2

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/lib/lines.rb ADDED
@@ -0,0 +1,143 @@
1
+ module RubyDocTest
2
+ # === Description
3
+ # Keeps track of which lines within a document belong to a group. Line groups are
4
+ # determined by their indentation level, as in the Python programming language.
5
+ #
6
+ # === Example
7
+ # This line and the next one
8
+ # are part of the same group
9
+ #
10
+ # But this line is separate from
11
+ # this line.
12
+ #
13
+ # === Note
14
+ # This class also considers one '#' character (comment) as an indentation character,
15
+ # i.e. similar to how whitespace is treated.
16
+ class Lines
17
+ def initialize(doc_lines, line_index = 0)
18
+ @doc_lines, @line_index = doc_lines, line_index
19
+ end
20
+
21
+
22
+ def line_number
23
+ @line_index + 1
24
+ end
25
+
26
+ # === Tests
27
+ # doctest: Return an array of 1 line if there is only one line
28
+ # >> l = RubyDocTest::Lines.new(["line 1"])
29
+ # >> l.lines
30
+ # => ["line 1"]
31
+ #
32
+ # doctest: Remove indentation from lines 2 to the end of this Lines group.
33
+ # >> l = RubyDocTest::Lines.new(["line 1", " line 2", " line 3", " line 4"])
34
+ # >> l.lines
35
+ # => ["line 1", "line 2", "line 3", " line 4"]
36
+ def lines
37
+ r = range
38
+ size = r.last - r.first + 1
39
+ if size > 1
40
+ # Remove indentation from all lines after the first,
41
+ # as measured from the 2nd line's indentation level
42
+ idt2 = indentation(@doc_lines, @line_index + 1)
43
+ [@doc_lines[range.first]] +
44
+ @doc_lines[(range.first + 1)..(range.last)].
45
+ map{ |l| $1 if l =~ /^#{Regexp.escape(idt2)}(.*)/ }
46
+ else
47
+ @doc_lines[range]
48
+ end
49
+ end
50
+
51
+
52
+ # === Description
53
+ # Calculate the range of python-like indentation within this Lines group
54
+ #
55
+ # === Tests
56
+ # >> l = RubyDocTest::Lines.new([])
57
+ #
58
+ # doctest: Return a range of one line when there is only one line to begin with
59
+ # >> l.range %w(a), 0
60
+ # => 0..0
61
+ #
62
+ # doctest: Return a range of one line when there are two lines, side by side
63
+ # >> l.range %w(a b), 0
64
+ # => 0..0
65
+ # >> l.range %w(a b), 1
66
+ # => 1..1
67
+ #
68
+ # doctest: Return a range of two lines when there are two lines, the second blank
69
+ # >> l.range ["a", ""], 0
70
+ # => 0..1
71
+ #
72
+ # doctest: Return a range of two lines when the second is indented
73
+ # >> l.range ["a", " b"], 0
74
+ # => 0..1
75
+ #
76
+ # doctest: Indentation can also include the ?> marker
77
+ # >> l.range [">> 1 +", "?> 2"], 0
78
+ # => 0..1
79
+ def range(doc_lines = @doc_lines, start_index = @line_index)
80
+ end_index = start_index
81
+ idt = indentation(doc_lines, start_index)
82
+ # Find next lines that are blank, or have indentation more than the first line
83
+ remaining_lines(doc_lines, start_index + 1).each do |current_line|
84
+ if current_line =~ /^(#{Regexp.escape(idt)}(\s+|\?>\s)|\s*$)/
85
+ end_index += 1
86
+ else
87
+ break
88
+ end
89
+ end
90
+ # Compute the range from what we found
91
+ start_index..end_index
92
+ end
93
+
94
+ def inspect
95
+ "#<#{self.class} lines=#{lines.inspect}>"
96
+ end
97
+
98
+ protected
99
+
100
+ # === Tests
101
+ # >> l = RubyDocTest::Lines.new([])
102
+ #
103
+ # doctest: Get a whitespace indent from a line with whitespace
104
+ # >> l.send :indentation, [" a"], 0
105
+ # => " "
106
+ #
107
+ # doctest: Get a whitespace and '#' indent from a comment line
108
+ # >> l.send :indentation, [" # a"], 0
109
+ # => " # "
110
+ def indentation(doc_lines = @doc_lines, line_index = @line_index)
111
+ if doc_lines[line_index]
112
+ doc_lines[line_index][/^(\s*#\s*|\s*)(\?>\s?)?/]
113
+ else
114
+ ""
115
+ end
116
+ end
117
+
118
+
119
+ # === Description
120
+ # Get lines from +start_index+ up to the end of the document.
121
+ #
122
+ # === Tests
123
+ # >> l = RubyDocTest::Lines.new([])
124
+ #
125
+ # doctest: Return an empty array if start_index is out of bounds
126
+ # >> l.send :remaining_lines, [], 1
127
+ # => []
128
+ # >> l.send :remaining_lines, [], -1
129
+ # => []
130
+ #
131
+ # doctest: Return the specified line at start_index, up to and including the
132
+ # last line of +doc_lines+.
133
+ # >> l.send :remaining_lines, %w(a b c), 1
134
+ # => %w(b c)
135
+ # >> l.send :remaining_lines, %w(a b c), 2
136
+ # => %w(c)
137
+ #
138
+ def remaining_lines(doc_lines = @doc_lines, start_index = @line_index)
139
+ return [] if start_index < 0 or start_index >= doc_lines.size
140
+ doc_lines[start_index..-1]
141
+ end
142
+ end
143
+ end
data/lib/result.rb ADDED
@@ -0,0 +1,63 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'lines'
5
+
6
+ module RubyDocTest
7
+ class Result < Lines
8
+
9
+ def normalize_result(s)
10
+ s.gsub(/:0x[a-f0-9]{8}>/, ':0xXXXXXXXX>').strip
11
+ end
12
+
13
+ def expected_result
14
+ @expected_result ||=
15
+ begin
16
+ lines.first =~ /^#{Regexp.escape(indentation)}=>\s(.*)$/
17
+ ([$1] + (lines[1..-1] || [])).join("\n")
18
+ end
19
+ end
20
+
21
+ # === Tests
22
+ # doctest: Strings should match
23
+ # >> r = RubyDocTest::Result.new(["=> 'hi'"])
24
+ # >> r.matches? 'hi'
25
+ # => true
26
+ #
27
+ # >> r = RubyDocTest::Result.new(["=> \"hi\""])
28
+ # >> r.matches? "hi"
29
+ # => true
30
+ #
31
+ # doctest: Regexps should match
32
+ # >> r = RubyDocTest::Result.new(["=> /^reg.../"])
33
+ # >> r.matches? /^reg.../
34
+ # => true
35
+ #
36
+ # >> r = RubyDocTest::Result.new(["=> /^reg.../"])
37
+ # >> r.matches? /^regexp/
38
+ # => false
39
+ #
40
+ # doctest: Arrays should match
41
+ # >> r = RubyDocTest::Result.new(["=> [1, 2, 3]"])
42
+ # >> r.matches? [1, 2, 3]
43
+ # => true
44
+ #
45
+ # doctest: Arrays of arrays should match
46
+ # >> r = RubyDocTest::Result.new(["=> [[1, 2], [3, 4]]"])
47
+ # >> r.matches? [[1, 2], [3, 4]]
48
+ # => true
49
+ #
50
+ # doctest: Hashes should match
51
+ # >> r = RubyDocTest::Result.new(["=> {:one => 1, :two => 2}"])
52
+ # >> r.matches?({:two => 2, :one => 1})
53
+ # => true
54
+ def matches?(actual_result)
55
+ normalize_result(actual_result.inspect) ==
56
+ normalize_result(expected_result) \
57
+ or
58
+ actual_result == eval(expected_result, TOPLEVEL_BINDING)
59
+ rescue Exception
60
+ false
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,29 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'irb'
5
+ require "runner"
6
+
7
+ module RubyDocTest
8
+
9
+ class << self
10
+ attr_accessor :trace, :ignore_interactive, :tests, :verbose
11
+ attr_writer :output_format
12
+
13
+ def output_format
14
+ if @output_format == :ansi or (@output_format.nil? and STDOUT.tty?)
15
+ :ansi
16
+ elsif @output_format == :html
17
+ :html
18
+ else
19
+ :plain
20
+ end
21
+ end
22
+
23
+ def indent(s, level=4)
24
+ spaces = " " * level
25
+ spaces + s.split("\n").join("\n#{spaces}")
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,9 @@
1
+ module Rubydoctest #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
data/lib/runner.rb ADDED
@@ -0,0 +1,414 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubydoctest'
5
+ require 'statement'
6
+ require 'result'
7
+ require 'special_directive'
8
+ require 'code_block'
9
+ require 'test'
10
+
11
+ module RubyDocTest
12
+ class Runner
13
+ attr_reader :groups, :blocks, :tests
14
+
15
+ @@color = {
16
+ :html => {
17
+ :red => %{<font color="red">%s</font>},
18
+ :yellow => %{<font color="#C0C000">%s</font>},
19
+ :green => %{<font color="green">%s</font>}
20
+ },
21
+ :ansi => {
22
+ :red => %{\e[31m%s\e[0m},
23
+ :yellow => %{\e[33m%s\e[0m},
24
+ :green => %{\e[32m%s\e[0m}
25
+ },
26
+ :plain => {
27
+ :red => "%s",
28
+ :yellow => "%s",
29
+ :green => "%s"
30
+ }
31
+ }
32
+
33
+ # The evaluation mode, either :doctest or :ruby.
34
+ #
35
+ # Modes:
36
+ # :doctest
37
+ # - The the Runner expects the file to contain text (e.g. a markdown file).
38
+ # In addition, it assumes that the text will occasionally be interspersed
39
+ # with irb lines which it should eval, e.g. '>>' and '=>'.
40
+ #
41
+ # :ruby
42
+ # - The Runner expects the file to be a Ruby source file. The source may contain
43
+ # comments that are interspersed with irb lines to eval, e.g. '>>' and '=>'.
44
+ attr_accessor :mode
45
+
46
+ # === Tests
47
+ #
48
+ # doctest: Runner mode should default to :doctest and :ruby from the filename
49
+ # >> r = RubyDocTest::Runner.new("", "test.doctest")
50
+ # >> r.mode
51
+ # => :doctest
52
+ #
53
+ # >> r = RubyDocTest::Runner.new("", "test.rb")
54
+ # >> r.mode
55
+ # => :ruby
56
+ #
57
+ # doctest: The src_lines should be separated into an array
58
+ # >> r = RubyDocTest::Runner.new("a\nb\n", "test.doctest")
59
+ # >> r.instance_variable_get("@src_lines")
60
+ # => ["a", "b"]
61
+ def initialize(src, file_name = "test.doctest", initial_mode = nil)
62
+ @src, @file_name = src, file_name
63
+ @mode = initial_mode || (File.extname(file_name) == ".rb" ? :ruby : :doctest)
64
+
65
+ @src_lines = src.split("\n")
66
+ @groups, @blocks = [], []
67
+ $rubydoctest = self
68
+ end
69
+
70
+ # doctest: Using the doctest_require: SpecialDirective should require a file relative to the current one.
71
+ # >> r = RubyDocTest::Runner.new("# doctest_require: 'doctest_require.rb'", __FILE__)
72
+ # >> r.prepare_tests
73
+ # >> is_doctest_require_successful?
74
+ # => true
75
+ def prepare_tests
76
+ @groups = read_groups
77
+ @blocks = organize_blocks
78
+ @tests = organize_tests
79
+ eval(@src, TOPLEVEL_BINDING, @file_name) if @mode == :ruby
80
+ end
81
+
82
+ # === Tests
83
+ # doctest: Run through a simple inline doctest (rb) file and see if it passes
84
+ # >> file = File.join(File.dirname(__FILE__), "..", "test", "inline.rb")
85
+ # >> r = RubyDocTest::Runner.new(IO.read(file), "inline.rb")
86
+ # >> r.pass?
87
+ # => true
88
+ def pass?
89
+ prepare_tests
90
+ @tests.all?{ |t| t.pass? }
91
+ end
92
+
93
+ # === Description
94
+ # Starts an IRB prompt when the "!!!" SpecialDirective is given.
95
+ def start_irb
96
+ IRB.init_config(nil)
97
+ IRB.conf[:PROMPT_MODE] = :SIMPLE
98
+ irb = IRB::Irb.new(IRB::WorkSpace.new(TOPLEVEL_BINDING))
99
+ IRB.conf[:MAIN_CONTEXT] = irb.context
100
+ catch(:IRB_EXIT) do
101
+ irb.eval_input
102
+ end
103
+ end
104
+
105
+ def format_color(text, color)
106
+ @@color[RubyDocTest.output_format][color] % text.to_s
107
+ end
108
+
109
+ def escape(text)
110
+ case RubyDocTest.output_format
111
+ when :html
112
+ text.gsub("<", "&lt;").gsub(">", "&gt;")
113
+ else
114
+ text
115
+ end
116
+ end
117
+
118
+ def run
119
+ prepare_tests
120
+ newline = "\n "
121
+ everything_passed = true
122
+ puts "=== Testing '#{@file_name}'..."
123
+ ok, fail, err = 0, 0, 0
124
+ @tests.each_with_index do |t, index|
125
+ if SpecialDirective === t and t.name == "!!!"
126
+ start_irb unless RubyDocTest.ignore_interactive
127
+ elsif RubyDocTest.tests.nil? or RubyDocTest.tests.include?(index + 1)
128
+ begin
129
+ if t.pass?
130
+ ok += 1
131
+ status = ["OK".center(4), :green]
132
+ detail = nil
133
+ else
134
+ fail += 1
135
+ everything_passed = false
136
+ status = ["FAIL".center(4), :red]
137
+
138
+ result_raw = t.first_failed.actual_result
139
+ got = if result_raw =~ /\n$/ && result_raw.count("\n") > 1
140
+ "Got: <<-__END__\n#{result_raw}__END__\n "
141
+ else
142
+ "Got: #{t.actual_result}#{newline}"
143
+ end
144
+ detail = format_color(
145
+ "#{got}Expected: #{t.expected_result}" + newline +
146
+ " from #{@file_name}:#{t.first_failed.result.line_number}",
147
+ :red)
148
+
149
+ end
150
+ rescue EvaluationError => e
151
+ err += 1
152
+ status = ["ERR".center(4), :yellow]
153
+ exception_text = e.original_exception.to_s.split("\n").join(newline)
154
+ detail = format_color(
155
+ "#{escape e.original_exception.class.to_s}: #{escape exception_text}" + newline +
156
+ " from #{@file_name}:#{e.statement.line_number}" + newline +
157
+ e.statement.source_code,
158
+ :yellow)
159
+ if RubyDocTest.verbose
160
+ detail += format_color(newline + e.original_exception.backtrace.join("\n"), :red)
161
+ end
162
+ end
163
+ puts \
164
+ "#{((index + 1).to_s + ".").ljust(3)} " +
165
+ "#{format_color(*status)} | " +
166
+ "#{t.description.split("\n").join(newline)}" +
167
+ (detail ? newline + detail : "")
168
+ end
169
+ end
170
+ puts \
171
+ "#{@blocks.select{ |b| b.is_a? CodeBlock }.size} comparisons, " +
172
+ "#{@tests.size} doctests, " +
173
+ "#{fail} failures, " +
174
+ "#{err} errors"
175
+ everything_passed
176
+ end
177
+
178
+ # === Tests
179
+ #
180
+ # doctest: Non-statement lines get ignored while statement / result lines are included
181
+ # Default mode is :doctest, so non-irb prompts should be ignored.
182
+ # >> r = RubyDocTest::Runner.new("a\nb\n >> c = 1\n => 1")
183
+ # >> groups = r.read_groups
184
+ # >> groups.size
185
+ # => 2
186
+ #
187
+ # doctest: Group types are correctly created
188
+ # >> groups.map{ |g| g.class }
189
+ # => [RubyDocTest::Statement, RubyDocTest::Result]
190
+ #
191
+ # doctest: A ruby document can have =begin and =end blocks in it
192
+ # >> r = RubyDocTest::Runner.new(<<-RUBY, "test.rb")
193
+ # some_ruby_code = 1
194
+ # =begin
195
+ # this is a normal ruby comment
196
+ # >> z = 10
197
+ # => 10
198
+ # =end
199
+ # more_ruby_code = 2
200
+ # RUBY
201
+ # >> groups = r.read_groups
202
+ # >> groups.size
203
+ # => 2
204
+ # >> groups.map{ |g| g.lines.first }
205
+ # => [" >> z = 10", " => 10"]
206
+ def read_groups(src_lines = @src_lines, mode = @mode, start_index = 0)
207
+ groups = []
208
+ (start_index).upto(src_lines.size) do |index|
209
+ line = src_lines[index]
210
+ case mode
211
+ when :ruby
212
+ case line
213
+
214
+ # Beginning of a multi-line comment section
215
+ when /^=begin/
216
+ groups +=
217
+ # Get statements, results, and directives as if inside a doctest
218
+ read_groups(src_lines, :doctest_with_end, index)
219
+
220
+ else
221
+ if g = match_group("\\s*#\\s*", src_lines, index)
222
+ groups << g
223
+ end
224
+
225
+ end
226
+ when :doctest
227
+ if g = match_group("\\s*", src_lines, index)
228
+ groups << g
229
+ end
230
+
231
+ when :doctest_with_end
232
+ break if line =~ /^=end/
233
+ if g = match_group("\\s*", src_lines, index)
234
+ groups << g
235
+ end
236
+
237
+ end
238
+ end
239
+ groups
240
+ end
241
+
242
+ def match_group(prefix, src_lines, index)
243
+ case src_lines[index]
244
+
245
+ # An irb '>>' marker after a '#' indicates an embedded doctest
246
+ when /^(#{prefix})>>(\s|\s*$)/
247
+ Statement.new(src_lines, index, @file_name)
248
+
249
+ # An irb '=>' marker after a '#' indicates an embedded result
250
+ when /^(#{prefix})=>\s/
251
+ Result.new(src_lines, index)
252
+
253
+ # Whenever we match a directive (e.g. 'doctest'), add that in as well
254
+ when /^(#{prefix})(#{SpecialDirective::NAMES_FOR_RX})(.*)$/
255
+ SpecialDirective.new(src_lines, index)
256
+
257
+ else
258
+ nil
259
+ end
260
+ end
261
+
262
+ # === Tests
263
+ #
264
+ # doctest: The organize_blocks method should separate Statement, Result and SpecialDirective
265
+ # objects into CodeBlocks.
266
+ # >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
267
+ # >> r.prepare_tests
268
+ #
269
+ # >> r.blocks.first.statements.map{|s| s.lines}
270
+ # => [[">> t = 1"], [">> t + 2"]]
271
+ #
272
+ # >> r.blocks.first.result.lines
273
+ # => ["=> 3"]
274
+ #
275
+ # >> r.blocks.last.statements.map{|s| s.lines}
276
+ # => [[">> u = 1"]]
277
+ #
278
+ # >> r.blocks.last.result
279
+ # => nil
280
+ #
281
+ # doctest: Two doctest directives--each having its own statement--should be separated properly
282
+ # by organize_blocks.
283
+ # >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
284
+ # >> r.prepare_tests
285
+ # >> r.blocks.map{|b| b.class}
286
+ # => [RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock,
287
+ # RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock]
288
+ #
289
+ # >> r.blocks[0].value
290
+ # => "one"
291
+ #
292
+ # >> r.blocks[1].statements.map{|s| s.lines}
293
+ # => [[">> t = 1"]]
294
+ #
295
+ # >> r.blocks[2].value
296
+ # => "two"
297
+ #
298
+ # >> r.blocks[3].statements.map{|s| s.lines}
299
+ # => [[">> t + 2"]]
300
+ def organize_blocks(groups = @groups)
301
+ blocks = []
302
+ current_statements = []
303
+ groups.each do |g|
304
+ case g
305
+ when Statement
306
+ current_statements << g
307
+ when Result
308
+ blocks << CodeBlock.new(current_statements, g)
309
+ current_statements = []
310
+ when SpecialDirective
311
+ case g.name
312
+ when "doctest:", "it:"
313
+ blocks << CodeBlock.new(current_statements) unless current_statements.empty?
314
+ current_statements = []
315
+ blocks << g
316
+ when "doctest_require:"
317
+ doctest_require = eval(g.value, TOPLEVEL_BINDING, @file_name, g.line_number)
318
+ if doctest_require.is_a? String
319
+ require_relative_to_file_name(doctest_require, @file_name)
320
+ end
321
+ blocks << g
322
+ when "!!!"
323
+ # ignore
324
+ unless RubyDocTest.ignore_interactive
325
+ fake_statement = Object.new
326
+ runner = self
327
+ (class << fake_statement; self; end).send(:define_method, :evaluate) do
328
+ runner.start_irb
329
+ end
330
+ current_statements << fake_statement
331
+ end
332
+ end
333
+ end
334
+ end
335
+ blocks << CodeBlock.new(current_statements) unless current_statements.empty?
336
+ blocks
337
+ end
338
+
339
+ def require_relative_to_file_name(file_name, relative_to)
340
+ load_path = $:.dup
341
+ $:.unshift File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))
342
+ if RubyDocTest.verbose
343
+ puts "doctest_require: [#{File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))}] #{File.basename(file_name)}"
344
+ end
345
+ require File.basename(file_name)
346
+ ensure
347
+ $:.shift
348
+ end
349
+
350
+ # === Tests
351
+ #
352
+ # doctest: Tests should be organized into groups based on the 'doctest' SpecialDirective
353
+ # >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
354
+ # >> r.prepare_tests
355
+ # >> r.tests.size
356
+ # => 2
357
+ # >> r.tests[0].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
358
+ # => [[">> t = 1"]]
359
+ # >> r.tests[1].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
360
+ # => [[">> t + 2"]]
361
+ # >> r.tests[0].description
362
+ # => "one"
363
+ # >> r.tests[1].description
364
+ # => "two"
365
+ #
366
+ # doctest: Without a 'doctest' SpecialDirective, there is one Test called "Default Test".
367
+ # >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
368
+ # >> r.prepare_tests
369
+ # >> r.tests.size
370
+ # => 1
371
+ #
372
+ # >> r.tests.first.description
373
+ # => "Default Test"
374
+ #
375
+ # >> r.tests.first.code_blocks.size
376
+ # => 2
377
+ #
378
+ # doctest: When using the "it:" directive, it should re-append "it" to the description;
379
+ # >> r = RubyDocTest::Runner.new("it: should behave\n>> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
380
+ # >> r.prepare_tests
381
+ # >> r.tests.size
382
+ # => 1
383
+ #
384
+ # >> r.tests.first.description
385
+ # => "it should behave"
386
+ #
387
+ # >> r.tests.first.code_blocks.size
388
+ # => 2
389
+ def organize_tests(blocks = @blocks)
390
+ tests = []
391
+ assigned_blocks = nil
392
+ unassigned_blocks = []
393
+ blocks.each do |g|
394
+ case g
395
+ when CodeBlock
396
+ (assigned_blocks || unassigned_blocks) << g
397
+ when SpecialDirective
398
+ case g.name
399
+ when "doctest:"
400
+ assigned_blocks = []
401
+ tests << Test.new(g.value, assigned_blocks)
402
+ when "it:"
403
+ assigned_blocks = []
404
+ tests << Test.new("it #{g.value}", assigned_blocks)
405
+ when "!!!"
406
+ tests << g
407
+ end
408
+ end
409
+ end
410
+ tests << Test.new("Default Test", unassigned_blocks) unless unassigned_blocks.empty?
411
+ tests
412
+ end
413
+ end
414
+ end