rubydoctest 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/config/hoe.rb CHANGED
@@ -1,15 +1,13 @@
1
1
  require 'rubydoctest/version'
2
2
 
3
- AUTHOR = ['Tom Locke', 'Dr Nic Williams'] # can also be an array of Authors
4
- EMAIL = "drnicwilliams@gmail.com"
3
+ AUTHOR = ['Duane Johnson', 'Tom Locke', 'Dr Nic Williams'] # can also be an array of Authors
4
+ EMAIL = "duane.johnson@gmail.com"
5
5
  DESCRIPTION = "Ruby version of Python's doctest tool, but a bit different."
6
6
  GEM_NAME = 'rubydoctest' # what ppl will type to install your gem
7
7
  RUBYFORGE_PROJECT = 'rubydoctest' # The unix name for your project
8
8
  HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
9
  DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
- EXTRA_DEPENDENCIES = [
11
- # ['activesupport', '>= 1.3.1']
12
- ] # An array of rubygem dependencies [name, version]
10
+ EXTRA_DEPENDENCIES = []
13
11
 
14
12
  @config_file = "~/.rubyforge/user-config.yml"
15
13
  @config = nil
data/lib/code_block.rb ADDED
@@ -0,0 +1,68 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'statement'
5
+ require 'result'
6
+
7
+ module RubyDocTest
8
+ # A +CodeBlock+ is a group of one or more ruby statements, followed by an optional result.
9
+ # For example:
10
+ # >> a = 1 + 1
11
+ # >> a - 3
12
+ # => -1
13
+ class CodeBlock
14
+ attr_reader :statements, :result, :passed
15
+
16
+ def initialize(statements = [], result = nil)
17
+ @statements = statements
18
+ @result = result
19
+ end
20
+
21
+ # === Tests
22
+ # doctest: Single statement with result should pass
23
+ # >> ss = [RubyDocTest::Statement.new([">> a = 1"])]
24
+ # >> r = RubyDocTest::Result.new(["=> 1"])
25
+ # >> cb = RubyDocTest::CodeBlock.new(ss, r)
26
+ # >> cb.pass?
27
+ # => true
28
+ #
29
+ # doctest: Single statement without result should pass by default
30
+ # >> ss = [RubyDocTest::Statement.new([">> a = 1"])]
31
+ # >> cb = RubyDocTest::CodeBlock.new(ss)
32
+ # >> cb.pass?
33
+ # => true
34
+ #
35
+ # doctest: Multi-line statement with result should pass
36
+ # >> ss = [RubyDocTest::Statement.new([">> a = 1"]),
37
+ # RubyDocTest::Statement.new([">> 'a' + a.to_s"])]
38
+ # >> r = RubyDocTest::Result.new(["=> 'a1'"])
39
+ # >> cb = RubyDocTest::CodeBlock.new(ss, r)
40
+ # >> cb.pass?
41
+ # => true
42
+ def pass?
43
+ if @computed
44
+ @passed
45
+ else
46
+ @computed = true
47
+ @passed =
48
+ begin
49
+ actual_results = @statements.map{ |s| s.evaluate }
50
+ @result ? @result.matches?(actual_results.last) : true
51
+ end
52
+ end
53
+ end
54
+
55
+ def actual_result
56
+ @statements.last.actual_result
57
+ end
58
+
59
+ def expected_result
60
+ @result.expected_result
61
+ end
62
+
63
+ def lines
64
+ @statements.map{ |s| s.lines }.flatten +
65
+ @result.lines
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ def is_doctest_require_successful?
2
+ true
3
+ end
data/lib/lines.rb ADDED
@@ -0,0 +1,143 @@
1
+ module RubyDocTest
2
+ # === Description
3
+ # Keeps track of which lines within a document belong to a group. Line groups are
4
+ # determined by their indentation level, as in the Python programming language.
5
+ #
6
+ # === Example
7
+ # This line and the next one
8
+ # are part of the same group
9
+ #
10
+ # But this line is separate from
11
+ # this line.
12
+ #
13
+ # === Note
14
+ # This class also considers one '#' character (comment) as an indentation character,
15
+ # i.e. similar to how whitespace is treated.
16
+ class Lines
17
+ def initialize(doc_lines, line_index = 0)
18
+ @doc_lines, @line_index = doc_lines, line_index
19
+ end
20
+
21
+
22
+ def line_number
23
+ @line_index + 1
24
+ end
25
+
26
+ # === Tests
27
+ # doctest: Return an array of 1 line if there is only one line
28
+ # >> l = RubyDocTest::Lines.new(["line 1"])
29
+ # >> l.lines
30
+ # => ["line 1"]
31
+ #
32
+ # doctest: Remove indentation from lines 2 to the end of this Lines group.
33
+ # >> l = RubyDocTest::Lines.new(["line 1", " line 2", " line 3", " line 4"])
34
+ # >> l.lines
35
+ # => ["line 1", "line 2", "line 3", " line 4"]
36
+ def lines
37
+ r = range
38
+ size = r.last - r.first + 1
39
+ if size > 1
40
+ # Remove indentation from all lines after the first,
41
+ # as measured from the 2nd line's indentation level
42
+ idt2 = indentation(@doc_lines, @line_index + 1)
43
+ [@doc_lines[range.first]] +
44
+ @doc_lines[(range.first + 1)..(range.last)].
45
+ map{ |l| $1 if l =~ /^#{Regexp.escape(idt2)}(.*)/ }
46
+ else
47
+ @doc_lines[range]
48
+ end
49
+ end
50
+
51
+
52
+ # === Description
53
+ # Calculate the range of python-like indentation within this Lines group
54
+ #
55
+ # === Tests
56
+ # >> l = RubyDocTest::Lines.new([])
57
+ #
58
+ # doctest: Return a range of one line when there is only one line to begin with
59
+ # >> l.range %w(a), 0
60
+ # => 0..0
61
+ #
62
+ # doctest: Return a range of one line when there are two lines, side by side
63
+ # >> l.range %w(a b), 0
64
+ # => 0..0
65
+ # >> l.range %w(a b), 1
66
+ # => 1..1
67
+ #
68
+ # doctest: Return a range of two lines when there are two lines, the second blank
69
+ # >> l.range ["a", ""], 0
70
+ # => 0..1
71
+ #
72
+ # doctest: Return a range of two lines when the second is indented
73
+ # >> l.range ["a", " b"], 0
74
+ # => 0..1
75
+ #
76
+ # doctest: Indentation can also include the ?> marker
77
+ # >> l.range [">> 1 +", "?> 2"], 0
78
+ # => 0..1
79
+ def range(doc_lines = @doc_lines, start_index = @line_index)
80
+ end_index = start_index
81
+ idt = indentation(doc_lines, start_index)
82
+ # Find next lines that are blank, or have indentation more than the first line
83
+ remaining_lines(doc_lines, start_index + 1).each do |current_line|
84
+ if current_line =~ /^(#{Regexp.escape(idt)}(\s+|\?>\s)|\s*$)/
85
+ end_index += 1
86
+ else
87
+ break
88
+ end
89
+ end
90
+ # Compute the range from what we found
91
+ start_index..end_index
92
+ end
93
+
94
+ def inspect
95
+ "#<#{self.class} lines=#{lines.inspect}>"
96
+ end
97
+
98
+ protected
99
+
100
+ # === Tests
101
+ # >> l = RubyDocTest::Lines.new([])
102
+ #
103
+ # doctest: Get a whitespace indent from a line with whitespace
104
+ # >> l.send :indentation, [" a"], 0
105
+ # => " "
106
+ #
107
+ # doctest: Get a whitespace and '#' indent from a comment line
108
+ # >> l.send :indentation, [" # a"], 0
109
+ # => " # "
110
+ def indentation(doc_lines = @doc_lines, line_index = @line_index)
111
+ if doc_lines[line_index]
112
+ doc_lines[line_index][/^(\s*#\s*|\s*)(\?>\s)?/]
113
+ else
114
+ ""
115
+ end
116
+ end
117
+
118
+
119
+ # === Description
120
+ # Get lines from +start_index+ up to the end of the document.
121
+ #
122
+ # === Tests
123
+ # >> l = RubyDocTest::Lines.new([])
124
+ #
125
+ # doctest: Return an empty array if start_index is out of bounds
126
+ # >> l.send :remaining_lines, [], 1
127
+ # => []
128
+ # >> l.send :remaining_lines, [], -1
129
+ # => []
130
+ #
131
+ # doctest: Return the specified line at start_index, up to and including the
132
+ # last line of +doc_lines+.
133
+ # >> l.send :remaining_lines, %w(a b c), 1
134
+ # => %w(b c)
135
+ # >> l.send :remaining_lines, %w(a b c), 2
136
+ # => %w(c)
137
+ #
138
+ def remaining_lines(doc_lines = @doc_lines, start_index = @line_index)
139
+ return [] if start_index < 0 or start_index >= doc_lines.size
140
+ doc_lines[start_index..-1]
141
+ end
142
+ end
143
+ end
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
- class RubyDocTest
7
+ module RubyDocTest
7
8
 
8
9
  class << self
9
- attr_accessor :trace
10
- end
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
- # next_line # get first line
27
- end
28
-
29
- def run
30
- run_file
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
- # There's only one result marker - stripped above
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
- if RubyDocTest.trace
239
- raise
240
- else
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