matthewrudy-rubydoctest 1.0.1
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 +24 -0
- data/License.txt +19 -0
- data/Manifest.txt +38 -0
- data/PostInstall.txt +7 -0
- data/README.txt +100 -0
- data/Rakefile +4 -0
- data/bin/rubydoctest +66 -0
- data/config/hoe.rb +72 -0
- data/config/requirements.rb +15 -0
- 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 +29 -0
- data/lib/rubydoctest/version.rb +9 -0
- data/lib/runner.rb +377 -0
- data/lib/special_directive.rb +44 -0
- data/lib/statement.rb +75 -0
- data/lib/test.rb +29 -0
- data/rubydoctest.gemspec +72 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/rstakeout +92 -0
- data/script/txt2html +82 -0
- data/setup.rb +1585 -0
- data/tasks/deployment.rake +34 -0
- data/tasks/doctests.rake +29 -0
- data/tasks/environment.rake +7 -0
- data/tasks/website.rake +17 -0
- data/textmate/DocTest (Markdown).textmate +7 -0
- data/textmate/DocTest (Ruby).textmate +65 -0
- data/textmate/DocTest (Text).textmate +66 -0
- data/website/index.html +94 -0
- data/website/index.txt +37 -0
- data/website/javascripts/rounded_corners_lite.inc.js +285 -0
- data/website/stylesheets/screen.css +138 -0
- data/website/template.html.erb +48 -0
- metadata +106 -0
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
ADDED
@@ -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
|
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
|
data/lib/runner.rb
ADDED
@@ -0,0 +1,377 @@
|
|
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("<", "<").gsub(">", ">")
|
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
|
+
detail = format_color(
|
138
|
+
"Got: #{escape t.actual_result}#{newline}Expected: #{escape t.expected_result}" + newline +
|
139
|
+
" from #{@file_name}:#{t.first_failed.result.line_number}",
|
140
|
+
:red)
|
141
|
+
|
142
|
+
end
|
143
|
+
rescue EvaluationError => e
|
144
|
+
err += 1
|
145
|
+
status = ["ERR".center(4), :yellow]
|
146
|
+
exception_text = e.original_exception.to_s.split("\n").join(newline)
|
147
|
+
detail = format_color(
|
148
|
+
"#{escape e.original_exception.class.to_s}: #{escape exception_text}" + newline +
|
149
|
+
" from #{@file_name}:#{e.statement.line_number}" + newline +
|
150
|
+
e.statement.source_code,
|
151
|
+
:yellow)
|
152
|
+
end
|
153
|
+
puts \
|
154
|
+
"#{((index + 1).to_s + ".").ljust(3)} " +
|
155
|
+
"#{format_color(*status)} | " +
|
156
|
+
"#{t.description.split("\n").join(newline)}" +
|
157
|
+
(detail ? newline + detail : "")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
puts \
|
161
|
+
"#{@blocks.select{ |b| b.is_a? CodeBlock }.size} comparisons, " +
|
162
|
+
"#{@tests.size} doctests, " +
|
163
|
+
"#{fail} failures, " +
|
164
|
+
"#{err} errors"
|
165
|
+
everything_passed
|
166
|
+
end
|
167
|
+
|
168
|
+
# === Tests
|
169
|
+
#
|
170
|
+
# doctest: Non-statement lines get ignored while statement / result lines are included
|
171
|
+
# Default mode is :doctest, so non-irb prompts should be ignored.
|
172
|
+
# >> r = RubyDocTest::Runner.new("a\nb\n >> c = 1\n => 1")
|
173
|
+
# >> groups = r.read_groups
|
174
|
+
# >> groups.size
|
175
|
+
# => 2
|
176
|
+
#
|
177
|
+
# doctest: Group types are correctly created
|
178
|
+
# >> groups.map{ |g| g.class }
|
179
|
+
# => [RubyDocTest::Statement, RubyDocTest::Result]
|
180
|
+
#
|
181
|
+
# doctest: A ruby document can have =begin and =end blocks in it
|
182
|
+
# >> r = RubyDocTest::Runner.new(<<-RUBY, "test.rb")
|
183
|
+
# some_ruby_code = 1
|
184
|
+
# =begin
|
185
|
+
# this is a normal ruby comment
|
186
|
+
# >> z = 10
|
187
|
+
# => 10
|
188
|
+
# =end
|
189
|
+
# more_ruby_code = 2
|
190
|
+
# RUBY
|
191
|
+
# >> groups = r.read_groups
|
192
|
+
# >> groups.size
|
193
|
+
# => 2
|
194
|
+
# >> groups.map{ |g| g.lines.first }
|
195
|
+
# => [" >> z = 10", " => 10"]
|
196
|
+
def read_groups(src_lines = @src_lines, mode = @mode, start_index = 0)
|
197
|
+
groups = []
|
198
|
+
(start_index).upto(src_lines.size) do |index|
|
199
|
+
line = src_lines[index]
|
200
|
+
case mode
|
201
|
+
when :ruby
|
202
|
+
case line
|
203
|
+
|
204
|
+
# Beginning of a multi-line comment section
|
205
|
+
when /^=begin/
|
206
|
+
groups +=
|
207
|
+
# Get statements, results, and directives as if inside a doctest
|
208
|
+
read_groups(src_lines, :doctest_with_end, index)
|
209
|
+
|
210
|
+
else
|
211
|
+
if g = match_group("\\s*#\\s*", src_lines, index)
|
212
|
+
groups << g
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
when :doctest
|
217
|
+
if g = match_group("\\s*", src_lines, index)
|
218
|
+
groups << g
|
219
|
+
end
|
220
|
+
|
221
|
+
when :doctest_with_end
|
222
|
+
break if line =~ /^=end/
|
223
|
+
if g = match_group("\\s*", src_lines, index)
|
224
|
+
groups << g
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
end
|
229
|
+
groups
|
230
|
+
end
|
231
|
+
|
232
|
+
def match_group(prefix, src_lines, index)
|
233
|
+
case src_lines[index]
|
234
|
+
|
235
|
+
# An irb '>>' marker after a '#' indicates an embedded doctest
|
236
|
+
when /^(#{prefix})>>(\s|\s*$)/
|
237
|
+
Statement.new(src_lines, index, @file_name)
|
238
|
+
|
239
|
+
# An irb '=>' marker after a '#' indicates an embedded result
|
240
|
+
when /^(#{prefix})=>\s/
|
241
|
+
Result.new(src_lines, index)
|
242
|
+
|
243
|
+
# Whenever we match a directive (e.g. 'doctest'), add that in as well
|
244
|
+
when /^(#{prefix})(#{SpecialDirective::NAMES_FOR_RX})(.*)$/
|
245
|
+
SpecialDirective.new(src_lines, index)
|
246
|
+
|
247
|
+
else
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# === Tests
|
253
|
+
#
|
254
|
+
# doctest: The organize_blocks method should separate Statement, Result and SpecialDirective
|
255
|
+
# objects into CodeBlocks.
|
256
|
+
# >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
|
257
|
+
# >> r.prepare_tests
|
258
|
+
#
|
259
|
+
# >> r.blocks.first.statements.map{|s| s.lines}
|
260
|
+
# => [[">> t = 1"], [">> t + 2"]]
|
261
|
+
#
|
262
|
+
# >> r.blocks.first.result.lines
|
263
|
+
# => ["=> 3"]
|
264
|
+
#
|
265
|
+
# >> r.blocks.last.statements.map{|s| s.lines}
|
266
|
+
# => [[">> u = 1"]]
|
267
|
+
#
|
268
|
+
# >> r.blocks.last.result
|
269
|
+
# => nil
|
270
|
+
#
|
271
|
+
# doctest: Two doctest directives--each having its own statement--should be separated properly
|
272
|
+
# by organize_blocks.
|
273
|
+
# >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
|
274
|
+
# >> r.prepare_tests
|
275
|
+
# >> r.blocks.map{|b| b.class}
|
276
|
+
# => [RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock,
|
277
|
+
# RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock]
|
278
|
+
#
|
279
|
+
# >> r.blocks[0].value
|
280
|
+
# => "one"
|
281
|
+
#
|
282
|
+
# >> r.blocks[1].statements.map{|s| s.lines}
|
283
|
+
# => [[">> t = 1"]]
|
284
|
+
#
|
285
|
+
# >> r.blocks[2].value
|
286
|
+
# => "two"
|
287
|
+
#
|
288
|
+
# >> r.blocks[3].statements.map{|s| s.lines}
|
289
|
+
# => [[">> t + 2"]]
|
290
|
+
def organize_blocks(groups = @groups)
|
291
|
+
blocks = []
|
292
|
+
current_statements = []
|
293
|
+
groups.each do |g|
|
294
|
+
case g
|
295
|
+
when Statement
|
296
|
+
current_statements << g
|
297
|
+
when Result
|
298
|
+
blocks << CodeBlock.new(current_statements, g)
|
299
|
+
current_statements = []
|
300
|
+
when SpecialDirective
|
301
|
+
case g.name
|
302
|
+
when "doctest:"
|
303
|
+
blocks << CodeBlock.new(current_statements) unless current_statements.empty?
|
304
|
+
current_statements = []
|
305
|
+
when "doctest_require:"
|
306
|
+
doctest_require = eval(g.value, TOPLEVEL_BINDING, @file_name, g.line_number)
|
307
|
+
if doctest_require.is_a? String
|
308
|
+
require_relative_to_file_name(doctest_require, @file_name)
|
309
|
+
end
|
310
|
+
when "!!!"
|
311
|
+
# ignore
|
312
|
+
end
|
313
|
+
blocks << g
|
314
|
+
end
|
315
|
+
end
|
316
|
+
blocks << CodeBlock.new(current_statements) unless current_statements.empty?
|
317
|
+
blocks
|
318
|
+
end
|
319
|
+
|
320
|
+
def require_relative_to_file_name(file_name, relative_to)
|
321
|
+
load_path = $:.dup
|
322
|
+
$:.unshift File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))
|
323
|
+
require File.basename(file_name)
|
324
|
+
ensure
|
325
|
+
$:.shift
|
326
|
+
end
|
327
|
+
|
328
|
+
# === Tests
|
329
|
+
#
|
330
|
+
# doctest: Tests should be organized into groups based on the 'doctest' SpecialDirective
|
331
|
+
# >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
|
332
|
+
# >> r.prepare_tests
|
333
|
+
# >> r.tests.size
|
334
|
+
# => 2
|
335
|
+
# >> r.tests[0].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
|
336
|
+
# => [[">> t = 1"]]
|
337
|
+
# >> r.tests[1].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
|
338
|
+
# => [[">> t + 2"]]
|
339
|
+
# >> r.tests[0].description
|
340
|
+
# => "one"
|
341
|
+
# >> r.tests[1].description
|
342
|
+
# => "two"
|
343
|
+
#
|
344
|
+
# doctest: Without a 'doctest' SpecialDirective, there is one Test called "Default Test".
|
345
|
+
# >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
|
346
|
+
# >> r.prepare_tests
|
347
|
+
# >> r.tests.size
|
348
|
+
# => 1
|
349
|
+
#
|
350
|
+
# >> r.tests.first.description
|
351
|
+
# => "Default Test"
|
352
|
+
#
|
353
|
+
# >> r.tests.first.code_blocks.size
|
354
|
+
# => 2
|
355
|
+
def organize_tests(blocks = @blocks)
|
356
|
+
tests = []
|
357
|
+
assigned_blocks = nil
|
358
|
+
unassigned_blocks = []
|
359
|
+
blocks.each do |g|
|
360
|
+
case g
|
361
|
+
when CodeBlock
|
362
|
+
(assigned_blocks || unassigned_blocks) << g
|
363
|
+
when SpecialDirective
|
364
|
+
case g.name
|
365
|
+
when "doctest:"
|
366
|
+
assigned_blocks = []
|
367
|
+
tests << Test.new(g.value, assigned_blocks)
|
368
|
+
when "!!!"
|
369
|
+
tests << g
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
tests << Test.new("Default Test", unassigned_blocks) unless unassigned_blocks.empty?
|
374
|
+
tests
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|