rubydoctest 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,8 @@
1
1
  module Rubydoctest #:nodoc:
2
2
  module VERSION #:nodoc:
3
- MAJOR = 0
4
- MINOR = 2
5
- TINY = 1
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
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("<", "&lt;").gsub(">", "&gt;")
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
@@ -0,0 +1,44 @@
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 SpecialDirective < Lines
8
+ NAMES = ["doctest:", "!!!", "doctest_require:"]
9
+ NAMES_FOR_RX = NAMES.map{ |n| Regexp.escape(n) }.join("|")
10
+
11
+ # === Test
12
+ #
13
+ # doctest: The name of the directive should be detected in the first line
14
+ # >> s = RubyDocTest::SpecialDirective.new(["doctest: Testing Stuff", "Other Stuff"])
15
+ # >> s.name
16
+ # => "doctest:"
17
+ def name
18
+ if m = lines.first.match(/^#{Regexp.escape(indentation)}(#{NAMES_FOR_RX})/)
19
+ m[1]
20
+ end
21
+ end
22
+
23
+ # === Test
24
+ #
25
+ # doctest: The value of the directive should be detected in the first line
26
+ # >> s = RubyDocTest::SpecialDirective.new(["doctest: Testing Stuff", "Other Stuff"])
27
+ # >> s.value
28
+ # => "Testing Stuff"
29
+ #
30
+ # >> s = RubyDocTest::SpecialDirective.new([" # doctest: Testing Stuff", " # Other Stuff"])
31
+ # >> s.value
32
+ # => "Testing Stuff"
33
+ #
34
+ # doctest: Multiple lines for the directive value should work as well
35
+ # >> s = RubyDocTest::SpecialDirective.new(["doctest: Testing Stuff", " On Two Lines"])
36
+ # >> s.value
37
+ # => "Testing Stuff\nOn Two Lines"
38
+ def value
39
+ if m = lines.join("\n").match(/^#{Regexp.escape(indentation)}(#{NAMES_FOR_RX})(.*)/m)
40
+ m[2].strip
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/statement.rb ADDED
@@ -0,0 +1,75 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubydoctest'
5
+ require 'lines'
6
+
7
+ module RubyDocTest
8
+ class EvaluationError < Exception
9
+ attr_reader :statement, :original_exception
10
+ def initialize(statement, original_exception)
11
+ @statement, @original_exception = statement, original_exception
12
+ end
13
+ end
14
+
15
+ class Statement < Lines
16
+
17
+ attr_reader :actual_result
18
+
19
+ # === Tests
20
+ #
21
+ # doctest: The FILENAME ruby constant should be replaced by the name of the file
22
+ # >> __FILE__[/statement\.rb$/]
23
+ # => "statement.rb"
24
+ def initialize(doc_lines, line_index = 0, file_name = nil)
25
+ @file_name = file_name
26
+ super(doc_lines, line_index)
27
+ end
28
+
29
+ # === Tests
30
+ #
31
+ # doctest: A statement should parse out a '>>' irb prompt
32
+ # >> s = RubyDocTest::Statement.new([">> a = 1"])
33
+ # >> s.source_code
34
+ # => "a = 1"
35
+ #
36
+ # doctest: More than one line should get included, if indentation so indicates
37
+ # >> s = RubyDocTest::Statement.new([">> b = 1 +", " 1", "not part of the statement"])
38
+ # >> s.source_code
39
+ # => "b = 1 +\n1"
40
+ #
41
+ # doctest: Lines indented by ?> should have the ?> removed.
42
+ # >> s = RubyDocTest::Statement.new([">> b = 1 +", "?> 1"])
43
+ # >> s.source_code
44
+ # => "b = 1 +\n1"
45
+ def source_code
46
+ lines.first =~ /^#{Regexp.escape(indentation)}>>\s(.*)$/
47
+ first = [$1]
48
+ remaining = (lines[1..-1] || [])
49
+ (first + remaining).join("\n")
50
+ end
51
+
52
+ # === Test
53
+ #
54
+ # doctest: Evaluating a multi-line statement should be ok
55
+ # >> s = RubyDocTest::Statement.new([">> b = 1 +", " 1", "not part of the statement"])
56
+ # >> s.evaluate
57
+ # => 2
58
+ #
59
+ # doctest: Evaluating a syntax error should raise an EvaluationError
60
+ # >> s = RubyDocTest::Statement.new([">> b = 1 +"])
61
+ # >> begin s.evaluate; :fail; rescue RubyDocTest::EvaluationError; :ok end
62
+ # => :ok
63
+ def evaluate
64
+ sc = source_code.gsub("__FILE__", @file_name.inspect)
65
+ # puts "EVAL: #{sc}"
66
+ @actual_result = eval(sc, TOPLEVEL_BINDING, __FILE__, __LINE__)
67
+ rescue Exception => e
68
+ if RubyDocTest.trace
69
+ raise e.class, e.to_s + "\n" + e.backtrace.first
70
+ else
71
+ raise EvaluationError.new(self, e)
72
+ end
73
+ end
74
+ end
75
+ end