Class: RubyDocTest::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/runner.rb

Constant Summary collapse

@@color =
{
  :html => {
    :red    => %{<font color="red">%s</font>},
    :yellow => %{<font color="#C0C000">%s</font>},
    :green  => %{<font color="green">%s</font>}
  },
  :ansi => {
    :red    => %{\e[31m%s\e[0m},
    :yellow => %{\e[33m%s\e[0m},
    :green  => %{\e[32m%s\e[0m}
  },
  :plain => {
    :red    => "%s",
    :yellow => "%s",
    :green  => "%s"
  }
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(src, file_name = "test.doctest", initial_mode = nil) ⇒ Runner

Tests

doctest: Runner mode should default to :doctest and :ruby from the filename >> r = RubyDocTest::Runner.new(“”, “test.doctest”) >> r.mode

> :doctest

>> r = RubyDocTest::Runner.new(“”, “test.rb”) >> r.mode

> :ruby

doctest: The src_lines should be separated into an array >> r = RubyDocTest::Runner.new(“anbn”, “test.doctest”) >> r.instance_variable_get(“@src_lines”)

> [“a”, “b”]


61
62
63
64
65
66
67
68
# File 'lib/runner.rb', line 61

def initialize(src, file_name = "test.doctest", initial_mode = nil)
  @src, @file_name = src, file_name
  @mode = initial_mode || (File.extname(file_name) == ".rb" ? :ruby : :doctest)
  
  @src_lines = src.split("\n")
  @groups, @blocks = [], []
  $rubydoctest = self
end

Instance Attribute Details

#blocksObject (readonly)

Returns the value of attribute blocks


13
14
15
# File 'lib/runner.rb', line 13

def blocks
  @blocks
end

#groupsObject (readonly)

Returns the value of attribute groups


13
14
15
# File 'lib/runner.rb', line 13

def groups
  @groups
end

#modeObject

The evaluation mode, either :doctest or :ruby.

Modes:

:doctest
  - The the Runner expects the file to contain text (e.g. a markdown file).
    In addition, it assumes that the text will occasionally be interspersed
    with irb lines which it should eval, e.g. '>>' and '=>'.

:ruby
  - The Runner expects the file to be a Ruby source file.  The source may contain
    comments that are interspersed with irb lines to eval, e.g. '>>' and '=>'.

44
45
46
# File 'lib/runner.rb', line 44

def mode
  @mode
end

#testsObject (readonly)

Returns the value of attribute tests


13
14
15
# File 'lib/runner.rb', line 13

def tests
  @tests
end

Instance Method Details

#escape(text) ⇒ Object


109
110
111
112
113
114
115
116
# File 'lib/runner.rb', line 109

def escape(text)
  case RubyDocTest.output_format
  when :html
    text.gsub("<", "&lt;").gsub(">", "&gt;")
  else
    text
  end
end

#format_color(text, color) ⇒ Object


105
106
107
# File 'lib/runner.rb', line 105

def format_color(text, color)
  @@color[RubyDocTest.output_format][color] % text.to_s
end

#match_group(prefix, src_lines, index) ⇒ Object


243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/runner.rb', line 243

def match_group(prefix, src_lines, index)
  case src_lines[index]
  
  # An irb '>>' marker after a '#' indicates an embedded doctest
  when /^(#{prefix})>>(\s|\s*$)/
    Statement.new(src_lines, index, @file_name)
  
  # An irb '=>' marker after a '#' indicates an embedded result
  when /^(#{prefix})=>\s/
    Result.new(src_lines, index)
  
  # Whenever we match a directive (e.g. 'doctest'), add that in as well
  when /^(#{prefix})(#{SpecialDirective::NAMES_FOR_RX})(.*)$/
    SpecialDirective.new(src_lines, index)
  
  else
    nil
  end
end

#organize_blocks(groups = @groups) ⇒ Object

Tests

doctest: The organize_blocks method should separate Statement, Result and SpecialDirective

objects into CodeBlocks.

>> r = RubyDocTest::Runner.new(“>> t = 1n>> t + 2n=> 3n>> u = 1”, “test.doctest”) >> r.prepare_tests

>> r.blocks.first.statements.map{|s| s.lines}

> [[“>> t = 1”], [“>> t + 2”]]

>> r.blocks.first.result.lines

> [“=> 3”]

>> r.blocks.last.statements.map{|s| s.lines}

> [[“>> u = 1”]]

>> r.blocks.last.result

> nil

doctest: Two doctest directives–each having its own statement–should be separated properly

by organize_blocks.

>> r = RubyDocTest::Runner.new(“doctest: onen>> t = 1ndoctest: twon>> t + 2”, “test.doctest”) >> r.prepare_tests >> r.blocks.map{|b| b.class}

> [RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock,

RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock]

>> r.blocks.value

> “one”

>> r.blocks.statements.map{|s| s.lines}

> [[“>> t = 1”]]

>> r.blocks.value

> “two”

>> r.blocks.statements.map{|s| s.lines}

> [[“>> t + 2”]]


301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/runner.rb', line 301

def organize_blocks(groups = @groups)
  blocks = []
  current_statements = []
  groups.each do |g|
    case g
    when Statement
      current_statements << g
    when Result
      blocks << CodeBlock.new(current_statements, g)
      current_statements = []
    when SpecialDirective
      case g.name
      when "doctest:", "it:"
        blocks << CodeBlock.new(current_statements) unless current_statements.empty?
        current_statements = []
        blocks << g
      when "doctest_require:"
        doctest_require = eval(g.value, TOPLEVEL_BINDING, @file_name, g.line_number)
        if doctest_require.is_a? String
          require_relative_to_file_name(doctest_require, @file_name)
        end
        blocks << g
      when "!!!"
        # ignore
        unless RubyDocTest.ignore_interactive
          fake_statement = Object.new
          runner = self
          (class << fake_statement; self; end).send(:define_method, :evaluate) do
            runner.start_irb
          end
          current_statements << fake_statement
        end
      end
    end
  end
  blocks << CodeBlock.new(current_statements) unless current_statements.empty?
  blocks
end

#organize_tests(blocks = @blocks) ⇒ Object

Tests

doctest: Tests should be organized into groups based on the 'doctest' SpecialDirective >> r = RubyDocTest::Runner.new(“doctest: onen>> t = 1ndoctest: twon>> t + 2”, “test.doctest”) >> r.prepare_tests >> r.tests.size

> 2

>> r.tests.code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}

> [[“>> t = 1”]]

>> r.tests.code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}

> [[“>> t + 2”]]

>> r.tests.description

> “one”

>> r.tests.description

> “two”

doctest: Without a 'doctest' SpecialDirective, there is one Test called “Default Test”. >> r = RubyDocTest::Runner.new(“>> t = 1n>> t + 2n=> 3n>> u = 1”, “test.doctest”) >> r.prepare_tests >> r.tests.size

> 1

>> r.tests.first.description

> “Default Test”

>> r.tests.first.code_blocks.size

> 2

doctest: When using the “it:” directive, it should re-append “it” to the description; >> r = RubyDocTest::Runner.new(“it: should behaven>> t = 1n>> t + 2n=> 3n>> u = 1”, “test.doctest”) >> r.prepare_tests >> r.tests.size

> 1

>> r.tests.first.description

> “it should behave”

>> r.tests.first.code_blocks.size

> 2


390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/runner.rb', line 390

def organize_tests(blocks = @blocks)
  tests = []
  assigned_blocks = nil
  unassigned_blocks = []
  blocks.each do |g|
    case g
    when CodeBlock
      (assigned_blocks || unassigned_blocks) << g
    when SpecialDirective
      case g.name
      when "doctest:"
        assigned_blocks = []
        tests << Test.new(g.value, assigned_blocks)
      when "it:"
        assigned_blocks = []
        tests << Test.new("it #{g.value}", assigned_blocks)
      when "!!!"
        tests << g
      end
    end
  end
  tests << Test.new("Default Test", unassigned_blocks) unless unassigned_blocks.empty?
  tests
end

#pass?Boolean

Tests

doctest: Run through a simple inline doctest (rb) file and see if it passes >> file = File.join(File.dirname(__FILE__), “..”, “test”, “inline.rb”) >> r = RubyDocTest::Runner.new(IO.read(file), “inline.rb”) >> r.pass?

> true

Returns:

  • (Boolean)

88
89
90
91
# File 'lib/runner.rb', line 88

def pass?
  prepare_tests
  @tests.all?{ |t| t.pass? }
end

#prepare_testsObject

doctest: Using the doctest_require: SpecialDirective should require a file relative to the current one. >> r = RubyDocTest::Runner.new(“# doctest_require: 'doctest_require.rb'”, __FILE__) >> r.prepare_tests >> is_doctest_require_successful?

> true


75
76
77
78
79
80
# File 'lib/runner.rb', line 75

def prepare_tests
  @groups = read_groups
  @blocks = organize_blocks
  @tests = organize_tests
  eval(@src, TOPLEVEL_BINDING, @file_name) if @mode == :ruby
end

#read_groups(src_lines = @src_lines, mode = @mode, start_index = 0) ⇒ Object

Tests

doctest: Non-statement lines get ignored while statement / result lines are included

Default mode is :doctest, so non-irb prompts should be ignored.

>> r = RubyDocTest::Runner.new(“anbn >> c = 1n => 1”) >> groups = r.read_groups >> groups.size

> 2

doctest: Group types are correctly created >> groups.map{ |g| g.class }

> [RubyDocTest::Statement, RubyDocTest::Result]

doctest: A ruby document can have =begin and =end blocks in it >> r = RubyDocTest::Runner.new(<<-RUBY, “test.rb”)

some_ruby_code = 1
=begin
 this is a normal ruby comment
 >> z = 10
 => 10
=end
more_ruby_code = 2
RUBY

>> groups = r.read_groups >> groups.size

> 2

>> groups.map{ |g| g.lines.first }

> [“ >> z = 10”, “ => 10”]


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/runner.rb', line 207

def read_groups(src_lines = @src_lines, mode = @mode, start_index = 0)
  groups = []
  (start_index).upto(src_lines.size) do |index|
    line = src_lines[index]
    case mode
    when :ruby
      case line
      
      # Beginning of a multi-line comment section
      when /^=begin/
        groups +=
          # Get statements, results, and directives as if inside a doctest
          read_groups(src_lines, :doctest_with_end, index)
      
      else
        if g = match_group("\\s*#\\s*", src_lines, index)
          groups << g
        end
      
      end
    when :doctest
      if g = match_group("\\s*", src_lines, index)
        groups << g
      end
      
    when :doctest_with_end
      break if line =~ /^=end/
      if g = match_group("\\s*", src_lines, index)
        groups << g
      end
      
    end
  end
  groups
end

#require_relative_to_file_name(file_name, relative_to) ⇒ Object


340
341
342
343
344
345
346
347
348
349
# File 'lib/runner.rb', line 340

def require_relative_to_file_name(file_name, relative_to)
  load_path = $:.dup
  $:.unshift File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))
  if RubyDocTest.verbose
    puts "doctest_require: [#{File.expand_path(File.join(File.dirname(relative_to), File.dirname(file_name)))}] #{File.basename(file_name)}"
  end
  require File.basename(file_name)
ensure
  $:.shift
end

#runObject


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/runner.rb', line 118

def run
  prepare_tests
  newline = "\n           "
  everything_passed = true
  puts "=== Testing '#{@file_name}'..."
  ok, fail, err = 0, 0, 0
  @tests.each_with_index do |t, index|
    if SpecialDirective === t and t.name == "!!!"
      start_irb unless RubyDocTest.ignore_interactive
    elsif RubyDocTest.tests.nil? or RubyDocTest.tests.include?(index + 1)
      begin
        if t.pass?
          ok += 1
          status = ["OK".center(4), :green]
          detail = nil
        else
          fail += 1
          everything_passed = false
          status = ["FAIL".center(4), :red]

          result_raw = t.first_failed.actual_result
          got = if result_raw =~ /\n$/ && result_raw.count("\n") > 1
                  "Got: <<-__END__\n#{result_raw}__END__\n       "
                else
                  "Got:      #{t.actual_result}#{newline}"
                end
          detail = format_color(
            "#{got}Expected: #{t.expected_result}" + newline +
              "  from #{@file_name}:#{t.first_failed.result.line_number}",
            :red)
          
        end
      rescue EvaluationError => e
        err += 1
        everything_passed = false
        status = ["ERR".center(4), :yellow]
        exception_text = e.original_exception.to_s.split("\n").join(newline)
        detail = format_color(
          "#{escape e.original_exception.class.to_s}: #{escape exception_text}" + newline +
            "  from #{@file_name}:#{e.statement.line_number}" + newline +
            e.statement.source_code,
          :yellow)
        if RubyDocTest.verbose
          detail += format_color(newline + e.original_exception.backtrace.join("\n"), :red)
        end
      end
      puts \
        "#{((index + 1).to_s + ".").ljust(3)} " +
        "#{format_color(*status)} | " +
        "#{t.description.split("\n").join(newline)}" +
        (detail ? newline + detail : "")
    end
  end
  puts \
    "#{@blocks.select{ |b| b.is_a? CodeBlock }.size} comparisons, " +
    "#{@tests.size} doctests, " +
    "#{fail} failures, " +
    "#{err} errors"
  everything_passed
end

#start_irbObject

Description

Starts an IRB prompt when the “!!!” SpecialDirective is given.


95
96
97
98
99
100
101
102
103
# File 'lib/runner.rb', line 95

def start_irb
  IRB.init_config(nil)
  IRB.conf[:PROMPT_MODE] = :SIMPLE
  irb = IRB::Irb.new(IRB::WorkSpace.new(TOPLEVEL_BINDING))
  IRB.conf[:MAIN_CONTEXT] = irb.context
  catch(:IRB_EXIT) do
    irb.eval_input
  end
end