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/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
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,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("<", "&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
+ 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