rdp-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.
@@ -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
@@ -0,0 +1,393 @@
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
+ end
160
+ puts \
161
+ "#{((index + 1).to_s + ".").ljust(3)} " +
162
+ "#{format_color(*status)} | " +
163
+ "#{t.description.split("\n").join(newline)}" +
164
+ (detail ? newline + detail : "")
165
+ end
166
+ end
167
+ puts \
168
+ "#{@blocks.select{ |b| b.is_a? CodeBlock }.size} comparisons, " +
169
+ "#{@tests.size} doctests, " +
170
+ "#{fail} failures, " +
171
+ "#{err} errors"
172
+ everything_passed
173
+ end
174
+
175
+ # === Tests
176
+ #
177
+ # doctest: Non-statement lines get ignored while statement / result lines are included
178
+ # Default mode is :doctest, so non-irb prompts should be ignored.
179
+ # >> r = RubyDocTest::Runner.new("a\nb\n >> c = 1\n => 1")
180
+ # >> groups = r.read_groups
181
+ # >> groups.size
182
+ # => 2
183
+ #
184
+ # doctest: Group types are correctly created
185
+ # >> groups.map{ |g| g.class }
186
+ # => [RubyDocTest::Statement, RubyDocTest::Result]
187
+ #
188
+ # doctest: A ruby document can have =begin and =end blocks in it
189
+ # >> r = RubyDocTest::Runner.new(<<-RUBY, "test.rb")
190
+ # some_ruby_code = 1
191
+ # =begin
192
+ # this is a normal ruby comment
193
+ # >> z = 10
194
+ # => 10
195
+ # =end
196
+ # more_ruby_code = 2
197
+ # RUBY
198
+ # >> groups = r.read_groups
199
+ # >> groups.size
200
+ # => 2
201
+ # >> groups.map{ |g| g.lines.first }
202
+ # => [" >> z = 10", " => 10"]
203
+ def read_groups(src_lines = @src_lines, mode = @mode, start_index = 0)
204
+ groups = []
205
+ (start_index).upto(src_lines.size) do |index|
206
+ line = src_lines[index]
207
+ case mode
208
+ when :ruby
209
+ case line
210
+
211
+ # Beginning of a multi-line comment section
212
+ when /^=begin/
213
+ groups +=
214
+ # Get statements, results, and directives as if inside a doctest
215
+ read_groups(src_lines, :doctest_with_end, index)
216
+
217
+ else
218
+ if g = match_group("\\s*#\\s*", src_lines, index)
219
+ groups << g
220
+ end
221
+
222
+ end
223
+ when :doctest
224
+ if g = match_group("\\s*", src_lines, index)
225
+ groups << g
226
+ end
227
+
228
+ when :doctest_with_end
229
+ break if line =~ /^=end/
230
+ if g = match_group("\\s*", src_lines, index)
231
+ groups << g
232
+ end
233
+
234
+ end
235
+ end
236
+ groups
237
+ end
238
+
239
+ def match_group(prefix, src_lines, index)
240
+ case src_lines[index]
241
+
242
+ # An irb '>>' marker after a '#' indicates an embedded doctest
243
+ when /^(#{prefix})>>(\s|\s*$)/
244
+ Statement.new(src_lines, index, @file_name)
245
+
246
+ # An irb '=>' marker after a '#' indicates an embedded result
247
+ when /^(#{prefix})=>\s/
248
+ Result.new(src_lines, index)
249
+
250
+ # Whenever we match a directive (e.g. 'doctest'), add that in as well
251
+ when /^(#{prefix})(#{SpecialDirective::NAMES_FOR_RX})(.*)$/
252
+ SpecialDirective.new(src_lines, index)
253
+
254
+ else
255
+ nil
256
+ end
257
+ end
258
+
259
+ # === Tests
260
+ #
261
+ # doctest: The organize_blocks method should separate Statement, Result and SpecialDirective
262
+ # objects into CodeBlocks.
263
+ # >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
264
+ # >> r.prepare_tests
265
+ #
266
+ # >> r.blocks.first.statements.map{|s| s.lines}
267
+ # => [[">> t = 1"], [">> t + 2"]]
268
+ #
269
+ # >> r.blocks.first.result.lines
270
+ # => ["=> 3"]
271
+ #
272
+ # >> r.blocks.last.statements.map{|s| s.lines}
273
+ # => [[">> u = 1"]]
274
+ #
275
+ # >> r.blocks.last.result
276
+ # => nil
277
+ #
278
+ # doctest: Two doctest directives--each having its own statement--should be separated properly
279
+ # by organize_blocks.
280
+ # >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
281
+ # >> r.prepare_tests
282
+ # >> r.blocks.map{|b| b.class}
283
+ # => [RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock,
284
+ # RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock]
285
+ #
286
+ # >> r.blocks[0].value
287
+ # => "one"
288
+ #
289
+ # >> r.blocks[1].statements.map{|s| s.lines}
290
+ # => [[">> t = 1"]]
291
+ #
292
+ # >> r.blocks[2].value
293
+ # => "two"
294
+ #
295
+ # >> r.blocks[3].statements.map{|s| s.lines}
296
+ # => [[">> t + 2"]]
297
+ def organize_blocks(groups = @groups)
298
+ blocks = []
299
+ current_statements = []
300
+ groups.each do |g|
301
+ case g
302
+ when Statement
303
+ current_statements << g
304
+ when Result
305
+ blocks << CodeBlock.new(current_statements, g)
306
+ current_statements = []
307
+ when SpecialDirective
308
+ case g.name
309
+ when "doctest:"
310
+ blocks << CodeBlock.new(current_statements) unless current_statements.empty?
311
+ current_statements = []
312
+ blocks << g
313
+ when "doctest_require:"
314
+ doctest_require = eval(g.value, TOPLEVEL_BINDING, @file_name, g.line_number)
315
+ if doctest_require.is_a? String
316
+ require_relative_to_file_name(doctest_require, @file_name)
317
+ end
318
+ blocks << g
319
+ when "!!!"
320
+ # ignore
321
+ unless RubyDocTest.ignore_interactive
322
+ fake_statement = Object.new
323
+ runner = self
324
+ (class << fake_statement; self; end).send(:define_method, :evaluate) do
325
+ runner.start_irb
326
+ end
327
+ current_statements << fake_statement
328
+ end
329
+ end
330
+ end
331
+ end
332
+ blocks << CodeBlock.new(current_statements) unless current_statements.empty?
333
+ blocks
334
+ end
335
+
336
+ def require_relative_to_file_name(file_name, relative_to)
337
+ load_path = $:.dup
338
+ $:.unshift File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))
339
+ require File.basename(file_name)
340
+ ensure
341
+ $:.shift
342
+ end
343
+
344
+ # === Tests
345
+ #
346
+ # doctest: Tests should be organized into groups based on the 'doctest' SpecialDirective
347
+ # >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
348
+ # >> r.prepare_tests
349
+ # >> r.tests.size
350
+ # => 2
351
+ # >> r.tests[0].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
352
+ # => [[">> t = 1"]]
353
+ # >> r.tests[1].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
354
+ # => [[">> t + 2"]]
355
+ # >> r.tests[0].description
356
+ # => "one"
357
+ # >> r.tests[1].description
358
+ # => "two"
359
+ #
360
+ # doctest: Without a 'doctest' SpecialDirective, there is one Test called "Default Test".
361
+ # >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
362
+ # >> r.prepare_tests
363
+ # >> r.tests.size
364
+ # => 1
365
+ #
366
+ # >> r.tests.first.description
367
+ # => "Default Test"
368
+ #
369
+ # >> r.tests.first.code_blocks.size
370
+ # => 2
371
+ def organize_tests(blocks = @blocks)
372
+ tests = []
373
+ assigned_blocks = nil
374
+ unassigned_blocks = []
375
+ blocks.each do |g|
376
+ case g
377
+ when CodeBlock
378
+ (assigned_blocks || unassigned_blocks) << g
379
+ when SpecialDirective
380
+ case g.name
381
+ when "doctest:"
382
+ assigned_blocks = []
383
+ tests << Test.new(g.value, assigned_blocks)
384
+ when "!!!"
385
+ tests << g
386
+ end
387
+ end
388
+ end
389
+ tests << Test.new("Default Test", unassigned_blocks) unless unassigned_blocks.empty?
390
+ tests
391
+ end
392
+ end
393
+ end