tablatom-rubydoctest 0.2.1 → 1.0.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.
- data/History.txt +4 -0
- data/License.txt +16 -17
- data/Manifest.txt +13 -5
- data/PostInstall.txt +2 -3
- data/README.txt +48 -57
- data/bin/rubydoctest +41 -25
- data/config/hoe.rb +3 -5
- data/lib/code_block.rb +68 -0
- data/lib/doctest_require.rb +3 -0
- data/lib/lines.rb +143 -0
- data/lib/result.rb +63 -0
- data/lib/rubydoctest.rb +13 -255
- data/lib/rubydoctest/version.rb +3 -3
- data/lib/runner.rb +370 -0
- data/lib/special_directive.rb +44 -0
- data/lib/statement.rb +75 -0
- data/lib/test.rb +29 -0
- data/rubydoctest.gemspec +32 -0
- data/script/rstakeout +92 -0
- data/tasks/doctests.rake +29 -0
- data/textmate/DocTest (Markdown).textmate +7 -0
- data/textmate/DocTest (Ruby).textmate +55 -0
- data/textmate/DocTest (Text).textmate +66 -0
- data/website/index.html +141 -11
- data/website/template.html.erb +1 -1
- metadata +21 -8
- data/test/test_helper.rb +0 -2
- data/test/test_rubydoctest.rb +0 -11
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
|
data/lib/rubydoctest.rb
CHANGED
@@ -2,270 +2,28 @@ $:.unshift(File.dirname(__FILE__)) unless
|
|
2
2
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
3
|
|
4
4
|
require 'irb'
|
5
|
+
require "runner"
|
5
6
|
|
6
|
-
|
7
|
+
module RubyDocTest
|
7
8
|
|
8
9
|
class << self
|
9
|
-
attr_accessor :trace
|
10
|
-
|
11
|
-
|
12
|
-
PROMPT_RX = /^[>?]>( |\s*$)/
|
13
|
-
|
14
|
-
RESULT_RX = /^=> /
|
15
|
-
|
16
|
-
CODE_LINE_RX = /^( |\t)/
|
17
|
-
|
18
|
-
def initialize(src, file_name)
|
19
|
-
@passed = 0
|
20
|
-
@block_count = 0
|
21
|
-
@failures = []
|
22
|
-
@src_lines = src.split("\n")
|
23
|
-
@line_num = 0
|
24
|
-
@file_name = file_name
|
10
|
+
attr_accessor :trace, :ignore_interactive
|
11
|
+
attr_writer :output_format
|
25
12
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
print_report
|
32
|
-
end
|
33
|
-
|
34
|
-
attr_accessor :passed, :failures, :current_line, :src_lines, :line_num, :block_count
|
35
|
-
|
36
|
-
def environment
|
37
|
-
TOPLEVEL_BINDING
|
38
|
-
end
|
39
|
-
|
40
|
-
def next_line
|
41
|
-
@line_num += 1
|
42
|
-
@current_line = src_lines.shift
|
43
|
-
end
|
44
|
-
|
45
|
-
def next_line?
|
46
|
-
src_lines.any?
|
47
|
-
end
|
48
|
-
|
49
|
-
def strip_prompt(s)
|
50
|
-
s.sub(PROMPT_RX, "")
|
51
|
-
end
|
52
|
-
|
53
|
-
def result_start?(s=current_line)
|
54
|
-
s =~ RESULT_RX
|
55
|
-
end
|
56
|
-
|
57
|
-
def string_result_start?(s=current_line)
|
58
|
-
s =~ /^=>""/
|
59
|
-
end
|
60
|
-
|
61
|
-
def statement_start?(s=current_line)
|
62
|
-
s =~ PROMPT_RX
|
63
|
-
end
|
64
|
-
|
65
|
-
def strip_result_marker(s=current_line)
|
66
|
-
s.sub(RESULT_RX, "")
|
67
|
-
end
|
68
|
-
|
69
|
-
def normalize_result(s)
|
70
|
-
s.gsub(/:0x[a-f0-9]{8}>/, ':0xXXXXXXXX>').strip
|
71
|
-
end
|
72
|
-
|
73
|
-
def code_line?(s=current_line)
|
74
|
-
s =~ CODE_LINE_RX
|
75
|
-
end
|
76
|
-
|
77
|
-
def code_block_start?(s=current_line)
|
78
|
-
l = unindent_code_line
|
79
|
-
code_line? && (statement_start?(l) || irb_interrupt?(l))
|
80
|
-
end
|
81
|
-
|
82
|
-
def blank_line?(s=current_line)
|
83
|
-
s =~ /^\s*$/
|
84
|
-
end
|
85
|
-
|
86
|
-
def irb_interrupt?(line)
|
87
|
-
line =~ /!!!/
|
88
|
-
end
|
89
|
-
|
90
|
-
def run_file
|
91
|
-
# run_code_block if code_block_start?
|
92
|
-
while next_line
|
93
|
-
run_code_block if code_block_start?
|
94
|
-
end
|
95
|
-
failures.length == 0
|
96
|
-
end
|
97
|
-
|
98
|
-
def unindent_code_line(s=current_line)
|
99
|
-
s.sub(CODE_LINE_RX, "")
|
100
|
-
end
|
101
|
-
|
102
|
-
|
103
|
-
def get_code_lines
|
104
|
-
lines = []
|
105
|
-
while blank_line? || code_line?
|
106
|
-
lines << [unindent_code_line, line_num]
|
107
|
-
next_line
|
108
|
-
end
|
109
|
-
lines.pop while blank_line?(lines.last)
|
110
|
-
lines
|
111
|
-
end
|
112
|
-
|
113
|
-
|
114
|
-
def run_code_block
|
115
|
-
self.block_count += 1
|
116
|
-
|
117
|
-
lines = get_code_lines
|
118
|
-
|
119
|
-
reading = :statement
|
120
|
-
|
121
|
-
result = nil
|
122
|
-
statement = ""
|
123
|
-
statement_line = lines.first.last
|
124
|
-
lines.each do |line, line_num|
|
125
|
-
|
126
|
-
if irb_interrupt?(line)
|
127
|
-
evaluate(statement, statement_line) unless statement == ""
|
128
|
-
|
129
|
-
puts statement unless statement.blank?
|
130
|
-
puts "=> #{result}" unless result.blank?
|
131
|
-
puts
|
132
|
-
|
133
|
-
start_irb
|
134
|
-
line = nil
|
135
|
-
|
136
|
-
elsif string_result_start?(line)
|
137
|
-
reading = :string_result
|
138
|
-
line = nil
|
139
|
-
result = ""
|
140
|
-
|
141
|
-
elsif result_start?(line)
|
142
|
-
reading = :ruby_result
|
143
|
-
line = strip_result_marker(line)
|
144
|
-
result = ""
|
145
|
-
|
146
|
-
elsif statement_start?(line)
|
147
|
-
if result
|
148
|
-
# start of a new statement
|
149
|
-
if reading == :string_result
|
150
|
-
# end of a string result statement
|
151
|
-
result.chomp!
|
152
|
-
run_statement(statement, result, statement_line, true)
|
153
|
-
else
|
154
|
-
run_statement(statement, result, statement_line)
|
155
|
-
end
|
156
|
-
statement_line = line_num
|
157
|
-
statement = ""
|
158
|
-
result = nil
|
159
|
-
end
|
160
|
-
reading = :statement
|
161
|
-
end
|
162
|
-
|
163
|
-
if reading == :statement
|
164
|
-
# The statement has a prompt on every line
|
165
|
-
if line
|
166
|
-
line = strip_prompt(line)
|
167
|
-
statement << line + "\n"
|
168
|
-
end
|
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
|
169
18
|
else
|
170
|
-
|
171
|
-
result << line + "\n" if line
|
19
|
+
:plain
|
172
20
|
end
|
173
|
-
|
174
|
-
end
|
175
|
-
|
176
|
-
if result.nil?
|
177
|
-
# The block ends with a statement - just evaluate it
|
178
|
-
evaluate(statement, statement_line)
|
179
|
-
else
|
180
|
-
run_statement(statement, result, statement_line)
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
|
185
|
-
def start_irb
|
186
|
-
IRB.init_config(nil)
|
187
|
-
IRB.conf[:PROMPT_MODE] = :SIMPLE
|
188
|
-
irb = IRB::Irb.new(IRB::WorkSpace.new(environment))
|
189
|
-
IRB.conf[:MAIN_CONTEXT] = irb.context
|
190
|
-
irb.eval_input
|
191
|
-
end
|
192
|
-
|
193
|
-
|
194
|
-
def run_statement(statement, expected_result, statement_line, string_comparison=false)
|
195
|
-
actual_result = evaluate(statement, statement_line)
|
196
|
-
|
197
|
-
if result_matches?(expected_result, actual_result, string_comparison)
|
198
|
-
self.passed += 1
|
199
|
-
else
|
200
|
-
actual_result = actual_result.inspect unless string_comparison
|
201
|
-
failures << [statement, actual_result, expected_result, statement_line]
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
|
206
|
-
def result_matches?(expected_result, actual_result, string_comparison)
|
207
|
-
if string_comparison
|
208
|
-
actual_result == expected_result
|
209
|
-
else
|
210
|
-
actual_result = actual_result.inspect
|
211
|
-
normalize_result(expected_result) == normalize_result(actual_result) or
|
212
|
-
# If the expected result looks like a literal, see if they eval to equal objects - this will often fail
|
213
|
-
if expected_result =~ /^[:\[{A-Z'"%\/]/
|
214
|
-
begin
|
215
|
-
eval(expected_result) == eval(actual_result)
|
216
|
-
rescue Exception
|
217
|
-
false
|
218
|
-
end
|
219
|
-
end
|
220
21
|
end
|
221
|
-
end
|
222
|
-
|
223
|
-
|
224
|
-
def evaluate(statement, line_num)
|
225
|
-
statement.gsub!("__FILE__", @file_name.inspect)
|
226
|
-
eval(statement, environment, __FILE__, __LINE__)
|
227
|
-
rescue SyntaxError => e
|
228
|
-
puts "Syntax error in statement on line #{line_num}:"
|
229
|
-
puts indent(statement)
|
230
|
-
puts e.to_s
|
231
|
-
puts
|
232
|
-
exit 1
|
233
|
-
rescue Exception => e
|
234
|
-
puts "Exception in statement on line #{line_num}:"
|
235
|
-
puts indent(statement)
|
236
|
-
puts e.backtrace
|
237
22
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
puts e.to_s
|
242
|
-
puts
|
243
|
-
exit 1
|
23
|
+
def indent(s, level=4)
|
24
|
+
spaces = " " * level
|
25
|
+
spaces + s.split("\n").join("\n#{spaces}")
|
244
26
|
end
|
245
27
|
end
|
246
28
|
|
247
|
-
|
248
|
-
def number_run
|
249
|
-
passed + failures.length
|
250
|
-
end
|
251
|
-
|
252
|
-
def indent(s, level=4)
|
253
|
-
spaces = " " * level
|
254
|
-
spaces + s.split("\n").join("\n#{spaces}")
|
255
|
-
end
|
256
|
-
|
257
|
-
def print_report
|
258
|
-
statements_per_block = number_run.to_f / block_count
|
259
|
-
puts("%d blocks, %d tests (avg. %.1f/block), %d failures\n\n" %
|
260
|
-
[block_count, number_run, statements_per_block, failures.length])
|
261
|
-
|
262
|
-
failures.each do |statement, actual, expected, lnum|
|
263
|
-
puts "Failure line #{lnum}"
|
264
|
-
puts " Statement:", indent(statement)
|
265
|
-
puts " Expected:", indent(expected)
|
266
|
-
puts " Got:\n", indent(actual)
|
267
|
-
puts
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
29
|
end
|
data/lib/rubydoctest/version.rb
CHANGED
data/lib/runner.rb
ADDED
@@ -0,0 +1,370 @@
|
|
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 run
|
110
|
+
prepare_tests
|
111
|
+
newline = "\n "
|
112
|
+
everything_passed = true
|
113
|
+
puts "=== Testing '#{@file_name}'..."
|
114
|
+
ok, fail, err = 0, 0, 0
|
115
|
+
@tests.each do |t|
|
116
|
+
if SpecialDirective === t and t.name == "!!!"
|
117
|
+
start_irb unless RubyDocTest.ignore_interactive
|
118
|
+
else
|
119
|
+
begin
|
120
|
+
if t.pass?
|
121
|
+
ok += 1
|
122
|
+
status = ["OK".center(4), :green]
|
123
|
+
detail = nil
|
124
|
+
else
|
125
|
+
fail += 1
|
126
|
+
everything_passed = false
|
127
|
+
status = ["FAIL".center(4), :red]
|
128
|
+
detail = format_color(
|
129
|
+
"Got: #{t.actual_result}#{newline}Expected: #{t.expected_result}" + newline +
|
130
|
+
" from #{@file_name}:#{t.first_failed.result.line_number}",
|
131
|
+
:red)
|
132
|
+
|
133
|
+
end
|
134
|
+
rescue EvaluationError => e
|
135
|
+
err += 1
|
136
|
+
status = ["ERR".center(4), :yellow]
|
137
|
+
exception_text = e.original_exception.to_s.split("\n").join(newline)
|
138
|
+
if RubyDocTest.output_format == :html
|
139
|
+
exception_text = exception_text.gsub("<", "<").gsub(">", ">")
|
140
|
+
end
|
141
|
+
detail = format_color(
|
142
|
+
"#{e.original_exception.class.to_s}: #{exception_text}" + newline +
|
143
|
+
" from #{@file_name}:#{e.statement.line_number}" + newline +
|
144
|
+
e.statement.source_code,
|
145
|
+
:yellow)
|
146
|
+
end
|
147
|
+
puts \
|
148
|
+
"#{format_color(*status)} | " +
|
149
|
+
"#{t.description.split("\n").join(newline)}" +
|
150
|
+
(detail ? newline + detail : "")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
puts \
|
154
|
+
"#{@blocks.select{ |b| b.is_a? CodeBlock }.size} comparisons, " +
|
155
|
+
"#{@tests.size} doctests, " +
|
156
|
+
"#{fail} failures, " +
|
157
|
+
"#{err} errors"
|
158
|
+
everything_passed
|
159
|
+
end
|
160
|
+
|
161
|
+
# === Tests
|
162
|
+
#
|
163
|
+
# doctest: Non-statement lines get ignored while statement / result lines are included
|
164
|
+
# Default mode is :doctest, so non-irb prompts should be ignored.
|
165
|
+
# >> r = RubyDocTest::Runner.new("a\nb\n >> c = 1\n => 1")
|
166
|
+
# >> groups = r.read_groups
|
167
|
+
# >> groups.size
|
168
|
+
# => 2
|
169
|
+
#
|
170
|
+
# doctest: Group types are correctly created
|
171
|
+
# >> groups.map{ |g| g.class }
|
172
|
+
# => [RubyDocTest::Statement, RubyDocTest::Result]
|
173
|
+
#
|
174
|
+
# doctest: A ruby document can have =begin and =end blocks in it
|
175
|
+
# >> r = RubyDocTest::Runner.new(<<-RUBY, "test.rb")
|
176
|
+
# some_ruby_code = 1
|
177
|
+
# =begin
|
178
|
+
# this is a normal ruby comment
|
179
|
+
# >> z = 10
|
180
|
+
# => 10
|
181
|
+
# =end
|
182
|
+
# more_ruby_code = 2
|
183
|
+
# RUBY
|
184
|
+
# >> groups = r.read_groups
|
185
|
+
# >> groups.size
|
186
|
+
# => 2
|
187
|
+
# >> groups.map{ |g| g.lines.first }
|
188
|
+
# => [" >> z = 10", " => 10"]
|
189
|
+
def read_groups(src_lines = @src_lines, mode = @mode, start_index = 0)
|
190
|
+
groups = []
|
191
|
+
(start_index).upto(src_lines.size) do |index|
|
192
|
+
line = src_lines[index]
|
193
|
+
case mode
|
194
|
+
when :ruby
|
195
|
+
case line
|
196
|
+
|
197
|
+
# Beginning of a multi-line comment section
|
198
|
+
when /^=begin/
|
199
|
+
groups +=
|
200
|
+
# Get statements, results, and directives as if inside a doctest
|
201
|
+
read_groups(src_lines, :doctest_with_end, index)
|
202
|
+
|
203
|
+
else
|
204
|
+
if g = match_group("\\s*#\\s*", src_lines, index)
|
205
|
+
groups << g
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
when :doctest
|
210
|
+
if g = match_group("\\s*", src_lines, index)
|
211
|
+
groups << g
|
212
|
+
end
|
213
|
+
|
214
|
+
when :doctest_with_end
|
215
|
+
break if line =~ /^=end/
|
216
|
+
if g = match_group("\\s*", src_lines, index)
|
217
|
+
groups << g
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
222
|
+
groups
|
223
|
+
end
|
224
|
+
|
225
|
+
def match_group(prefix, src_lines, index)
|
226
|
+
case src_lines[index]
|
227
|
+
|
228
|
+
# An irb '>>' marker after a '#' indicates an embedded doctest
|
229
|
+
when /^(#{prefix})>>(\s|\s*$)/
|
230
|
+
Statement.new(src_lines, index, @file_name)
|
231
|
+
|
232
|
+
# An irb '=>' marker after a '#' indicates an embedded result
|
233
|
+
when /^(#{prefix})=>\s/
|
234
|
+
Result.new(src_lines, index)
|
235
|
+
|
236
|
+
# Whenever we match a directive (e.g. 'doctest'), add that in as well
|
237
|
+
when /^(#{prefix})(#{SpecialDirective::NAMES_FOR_RX})(.*)$/
|
238
|
+
SpecialDirective.new(src_lines, index)
|
239
|
+
|
240
|
+
else
|
241
|
+
nil
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# === Tests
|
246
|
+
#
|
247
|
+
# doctest: The organize_blocks method should separate Statement, Result and SpecialDirective
|
248
|
+
# objects into CodeBlocks.
|
249
|
+
# >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
|
250
|
+
# >> r.prepare_tests
|
251
|
+
#
|
252
|
+
# >> r.blocks.first.statements.map{|s| s.lines}
|
253
|
+
# => [[">> t = 1"], [">> t + 2"]]
|
254
|
+
#
|
255
|
+
# >> r.blocks.first.result.lines
|
256
|
+
# => ["=> 3"]
|
257
|
+
#
|
258
|
+
# >> r.blocks.last.statements.map{|s| s.lines}
|
259
|
+
# => [[">> u = 1"]]
|
260
|
+
#
|
261
|
+
# >> r.blocks.last.result
|
262
|
+
# => nil
|
263
|
+
#
|
264
|
+
# doctest: Two doctest directives--each having its own statement--should be separated properly
|
265
|
+
# by organize_blocks.
|
266
|
+
# >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
|
267
|
+
# >> r.prepare_tests
|
268
|
+
# >> r.blocks.map{|b| b.class}
|
269
|
+
# => [RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock,
|
270
|
+
# RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock]
|
271
|
+
#
|
272
|
+
# >> r.blocks[0].value
|
273
|
+
# => "one"
|
274
|
+
#
|
275
|
+
# >> r.blocks[1].statements.map{|s| s.lines}
|
276
|
+
# => [[">> t = 1"]]
|
277
|
+
#
|
278
|
+
# >> r.blocks[2].value
|
279
|
+
# => "two"
|
280
|
+
#
|
281
|
+
# >> r.blocks[3].statements.map{|s| s.lines}
|
282
|
+
# => [[">> t + 2"]]
|
283
|
+
def organize_blocks(groups = @groups)
|
284
|
+
blocks = []
|
285
|
+
current_statements = []
|
286
|
+
groups.each do |g|
|
287
|
+
case g
|
288
|
+
when Statement
|
289
|
+
current_statements << g
|
290
|
+
when Result
|
291
|
+
blocks << CodeBlock.new(current_statements, g)
|
292
|
+
current_statements = []
|
293
|
+
when SpecialDirective
|
294
|
+
case g.name
|
295
|
+
when "doctest:"
|
296
|
+
blocks << CodeBlock.new(current_statements) unless current_statements.empty?
|
297
|
+
current_statements = []
|
298
|
+
when "doctest_require:"
|
299
|
+
doctest_require = eval(g.value, TOPLEVEL_BINDING, @file_name, g.line_number)
|
300
|
+
if doctest_require.is_a? String
|
301
|
+
require_relative_to_file_name(doctest_require, @file_name)
|
302
|
+
end
|
303
|
+
when "!!!"
|
304
|
+
# ignore
|
305
|
+
end
|
306
|
+
blocks << g
|
307
|
+
end
|
308
|
+
end
|
309
|
+
blocks << CodeBlock.new(current_statements) unless current_statements.empty?
|
310
|
+
blocks
|
311
|
+
end
|
312
|
+
|
313
|
+
def require_relative_to_file_name(file_name, relative_to)
|
314
|
+
load_path = $:.dup
|
315
|
+
$:.unshift File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))
|
316
|
+
require File.basename(file_name)
|
317
|
+
ensure
|
318
|
+
$:.shift
|
319
|
+
end
|
320
|
+
|
321
|
+
# === Tests
|
322
|
+
#
|
323
|
+
# doctest: Tests should be organized into groups based on the 'doctest' SpecialDirective
|
324
|
+
# >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
|
325
|
+
# >> r.prepare_tests
|
326
|
+
# >> r.tests.size
|
327
|
+
# => 2
|
328
|
+
# >> r.tests[0].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
|
329
|
+
# => [[">> t = 1"]]
|
330
|
+
# >> r.tests[1].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
|
331
|
+
# => [[">> t + 2"]]
|
332
|
+
# >> r.tests[0].description
|
333
|
+
# => "one"
|
334
|
+
# >> r.tests[1].description
|
335
|
+
# => "two"
|
336
|
+
#
|
337
|
+
# doctest: Without a 'doctest' SpecialDirective, there is one Test called "Default Test".
|
338
|
+
# >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
|
339
|
+
# >> r.prepare_tests
|
340
|
+
# >> r.tests.size
|
341
|
+
# => 1
|
342
|
+
#
|
343
|
+
# >> r.tests.first.description
|
344
|
+
# => "Default Test"
|
345
|
+
#
|
346
|
+
# >> r.tests.first.code_blocks.size
|
347
|
+
# => 2
|
348
|
+
def organize_tests(blocks = @blocks)
|
349
|
+
tests = []
|
350
|
+
assigned_blocks = nil
|
351
|
+
unassigned_blocks = []
|
352
|
+
blocks.each do |g|
|
353
|
+
case g
|
354
|
+
when CodeBlock
|
355
|
+
(assigned_blocks || unassigned_blocks) << g
|
356
|
+
when SpecialDirective
|
357
|
+
case g.name
|
358
|
+
when "doctest:"
|
359
|
+
assigned_blocks = []
|
360
|
+
tests << Test.new(g.value, assigned_blocks)
|
361
|
+
when "!!!"
|
362
|
+
tests << g
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
tests << Test.new("Default Test", unassigned_blocks) unless unassigned_blocks.empty?
|
367
|
+
tests
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|