$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
require 'rubydoctest'
require 'statement'
require 'result'
require 'special_directive'
require 'code_block'
require 'test'
module RubyDocTest
class Runner
attr_reader :groups, :blocks, :tests
@@color = {
:html => {
:red => %{%s},
:yellow => %{%s},
:green => %{%s}
},
: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"
}
}
# 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 '=>'.
attr_accessor :mode
# === 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("a\nb\n", "test.doctest")
# >> r.instance_variable_get("@src_lines")
# => ["a", "b"]
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
# 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
def prepare_tests
@groups = read_groups
@blocks = organize_blocks
@tests = organize_tests
eval(@src, TOPLEVEL_BINDING, @file_name) if @mode == :ruby
end
# === 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
def pass?
prepare_tests
@tests.all?{ |t| t.pass? }
end
# === Description
# Starts an IRB prompt when the "!!!" SpecialDirective is given.
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
def format_color(text, color)
@@color[RubyDocTest.output_format][color] % text.to_s
end
def escape(text)
case RubyDocTest.output_format
when :html
text.gsub("<", "<").gsub(">", ">")
else
text
end
end
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
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)
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
# === 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("a\nb\n >> c = 1\n => 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"]
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
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
# === Tests
#
# doctest: The organize_blocks method should separate Statement, Result and SpecialDirective
# objects into CodeBlocks.
# >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> 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: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
# >> r.prepare_tests
# >> r.blocks.map{|b| b.class}
# => [RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock,
# RubyDocTest::SpecialDirective, RubyDocTest::CodeBlock]
#
# >> r.blocks[0].value
# => "one"
#
# >> r.blocks[1].statements.map{|s| s.lines}
# => [[">> t = 1"]]
#
# >> r.blocks[2].value
# => "two"
#
# >> r.blocks[3].statements.map{|s| s.lines}
# => [[">> t + 2"]]
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 = []
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
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
# === Tests
#
# doctest: Tests should be organized into groups based on the 'doctest' SpecialDirective
# >> r = RubyDocTest::Runner.new("doctest: one\n>> t = 1\ndoctest: two\n>> t + 2", "test.doctest")
# >> r.prepare_tests
# >> r.tests.size
# => 2
# >> r.tests[0].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
# => [[">> t = 1"]]
# >> r.tests[1].code_blocks.map{|c| c.statements}.flatten.map{|s| s.lines}
# => [[">> t + 2"]]
# >> r.tests[0].description
# => "one"
# >> r.tests[1].description
# => "two"
#
# doctest: Without a 'doctest' SpecialDirective, there is one Test called "Default Test".
# >> r = RubyDocTest::Runner.new(">> t = 1\n>> t + 2\n=> 3\n>> u = 1", "test.doctest")
# >> r.prepare_tests
# >> r.tests.size
# => 1
#
# >> r.tests.first.description
# => "Default Test"
#
# >> r.tests.first.code_blocks.size
# => 2
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
end
end