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

#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



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/runner.rb', line 225

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”]]



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/runner.rb', line 283

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:"
        blocks << CodeBlock.new(current_statements) unless current_statements.empty?
        current_statements = []
      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
      when "!!!"
        # ignore
      end
      blocks << g
    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



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/runner.rb', line 348

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 "!!!"
        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”]



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/runner.rb', line 189

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



313
314
315
316
317
318
319
# File 'lib/runner.rb', line 313

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)))
  require File.basename(file_name)
ensure
  $:.shift
end

#runObject



109
110
111
112
113
114
115
116
117
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
# File 'lib/runner.rb', line 109

def run
  prepare_tests
  newline = "\n       "
  everything_passed = true
  puts "=== Testing '#{@file_name}'..."
  ok, fail, err = 0, 0, 0
  @tests.each do |t|
    if SpecialDirective === t and t.name == "!!!"
      start_irb unless RubyDocTest.ignore_interactive
    else
      begin
        if t.pass?
          ok += 1
          status = ["OK".center(4), :green]
          detail = nil
        else
          fail += 1
          everything_passed = false
          status = ["FAIL".center(4), :red]
          detail = format_color(
            "Got: #{t.actual_result}#{newline}Expected: #{t.expected_result}" + newline +
              "  from #{@file_name}:#{t.first_failed.result.line_number}",
            :red)
          
        end
      rescue EvaluationError => e
        err += 1
        status = ["ERR".center(4), :yellow]
        exception_text = e.original_exception.to_s.split("\n").join(newline)
        if RubyDocTest.output_format == :html
          exception_text = exception_text.gsub("<", "&lt;").gsub(">", "&gt;")
        end
        detail = format_color(
          "#{e.original_exception.class.to_s}: #{exception_text}" + newline +
            "  from #{@file_name}:#{e.statement.line_number}" + newline +
            e.statement.source_code,
          :yellow)
      end
      puts \
        "#{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