lib/dfect.rb in dfect-1.1.0 vs lib/dfect.rb in dfect-2.0.0
- old
+ new
@@ -1,27 +1,21 @@
-#--
-# Copyright protects this work.
-# See LICENSE file for details.
-#++
-
require 'yaml'
#
# YAML raises this error when we try to serialize a class:
#
# TypeError: can't dump anonymous class Class
#
# Work around this by representing a class by its name.
#
-class Class #:nodoc: all
+class Class # @private
alias __to_yaml__ to_yaml
undef to_yaml
def to_yaml opts = {}
begin
__to_yaml__
rescue TypeError => e
- warn e
self.name.to_yaml opts
end
end
end
@@ -40,41 +34,44 @@
end
module Dfect
class << self
##
- # Hash of test results, assembled by #run.
+ # Hash of test results, assembled by {Dfect.run}.
#
- # [:execution]
+ # [:trace]
# Hierarchical trace of all tests executed, where each test is
# represented by its description, is mapped to an Array of
# nested tests, and may contain zero or more assertion failures.
#
# Assertion failures are represented as a Hash:
#
- # ["fail"]
+ # [:fail]
# Description of the assertion failure.
#
- # ["code"]
+ # [:code]
# Source code surrounding the point of failure.
#
- # ["vars"]
+ # [:vars]
# Local variables visible at the point of failure.
#
- # ["call"]
+ # [:call]
# Stack trace leading to the point of failure.
#
- # [:statistics]
+ # [:stats]
# Hash of counts of major events in test execution:
#
- # [:passed_assertions]
+ # [:time]
+ # Number of seconds elapsed for test execution.
+ #
+ # [:pass]
# Number of assertions that held true.
#
- # [:failed_assertions]
+ # [:fail]
# Number of assertions that did not hold true.
#
- # [:uncaught_exceptions]
+ # [:error]
# Number of exceptions that were not rescued.
#
attr_reader :report
##
@@ -83,33 +80,36 @@
# [:debug]
# Launch an interactive debugger
# during assertion failures so
# the user can investigate them.
#
- # The default value is true.
+ # The default value is $DEBUG.
#
# [:quiet]
# Do not print the report
# after executing all tests.
#
# The default value is false.
#
attr_accessor :options
##
- # Defines a new test, composed of the given
+ # Defines a new test composed of the given
# description and the given block to execute.
#
- # A test may contain nested tests.
+ # This test may contain nested tests.
#
- # ==== Parameters
+ # Tests at the outer-most level are automatically
+ # insulated from the top-level Ruby environment.
#
- # [description]
- # A short summary of the test being defined.
+ # @param [Object, Array<Object>] description
#
- # ==== Examples
+ # A brief title or a series of objects
+ # that describe the test being defined.
#
+ # @example
+ #
# D "a new array" do
# D .< { @array = [] }
#
# D "must be empty" do
# T { @array.empty? }
@@ -122,189 +122,220 @@
# F { @array.empty? }
# end
# end
# end
#
- def D description = caller.first, &block
- raise ArgumentError, 'block must be given' unless block
- @curr_suite.tests << Suite::Test.new(description.to_s, block)
+ def D *description, &block
+ create_test @tests.empty?, *description, &block
end
##
- # :call-seq: <(&block)
+ # Defines a new test that is explicitly insulated from the tests
+ # that contain it and also from the top-level Ruby environment.
#
+ # This test may contain nested tests.
+ #
+ # @param description (see Dfect.D)
+ #
+ # @example
+ #
+ # D "a root-level test" do
+ # @outside = 1
+ # T { defined? @outside }
+ # T { @outside == 1 }
+ #
+ # D "an inner, non-insulated test" do
+ # T { defined? @outside }
+ # T { @outside == 1 }
+ # end
+ #
+ # D! "an inner, insulated test" do
+ # F { defined? @outside }
+ # F { @outside == 1 }
+ #
+ # @inside = 2
+ # T { defined? @inside }
+ # T { @inside == 2 }
+ # end
+ #
+ # F { defined? @inside }
+ # F { @inside == 2 }
+ # end
+ #
+ def D! *description, &block
+ create_test true, *description, &block
+ end
+
+ ##
+ # @overload def <(&block)
+ #
# Registers the given block to be executed
# before each nested test inside this test.
#
- # ==== Examples
+ # @example
#
# D .< { puts "before each nested test" }
#
# D .< do
# puts "before each nested test"
# end
#
def <(*args, &block)
if args.empty?
raise ArgumentError, 'block must be given' unless block
- @curr_suite.before_each << block
+ @suite.before_each << block
else
# the < method is being used as a check for inheritance
super
end
end
##
# Registers the given block to be executed
# after each nested test inside this test.
#
- # ==== Examples
+ # @example
#
# D .> { puts "after each nested test" }
#
# D .> do
# puts "after each nested test"
# end
#
def > &block
raise ArgumentError, 'block must be given' unless block
- @curr_suite.after_each << block
+ @suite.after_each << block
end
##
# Registers the given block to be executed
# before all nested tests inside this test.
#
- # ==== Examples
+ # @example
#
# D .<< { puts "before all nested tests" }
#
# D .<< do
# puts "before all nested tests"
# end
#
def << &block
raise ArgumentError, 'block must be given' unless block
- @curr_suite.before_all << block
+ @suite.before_all << block
end
##
# Registers the given block to be executed
# after all nested tests inside this test.
#
- # ==== Examples
+ # @example
#
# D .>> { puts "after all nested tests" }
#
# D .>> do
# puts "after all nested tests"
# end
#
def >> &block
raise ArgumentError, 'block must be given' unless block
- @curr_suite.after_all << block
+ @suite.after_all << block
end
##
- # Asserts that the result of the given block is
- # neither nil nor false and returns that result.
+ # Asserts that the given condition or the
+ # result of the given block is neither
+ # nil nor false and returns that result.
#
- # ==== Parameters
+ # @param condition
#
- # [message]
+ # The condition to be asserted. A block
+ # may be given in place of this parameter.
+ #
+ # @param message
+ #
# Optional message to show in the
# report if this assertion fails.
#
- # ==== Examples
+ # @example no message given
#
- # # no message specified:
- #
# T { true } # passes
# T { false } # fails
# T { nil } # fails
#
- # # message specified:
+ # @example message is given
#
- # T( "computers do not doublethink" ) { 2 + 2 != 5 } # passes
+ # T("computers do not doublethink") { 2 + 2 != 5 } # passes
#
- def T message = nil, &block
- assert_yield :assert, message, &block
+ def T condition = nil, message = nil, &block
+ assert_yield :assert, condition, message, &block
end
##
- # Asserts that the result of the given block is
- # either nil or false and returns that result.
+ # Asserts that the given condition or the
+ # result of the given block is either nil
+ # or false and returns that result.
#
- # ==== Parameters
+ # @param condition (see Dfect.T)
#
- # [message]
- # Optional message to show in the
- # report if this assertion fails.
+ # @param message (see Dfect.T)
#
- # ==== Examples
+ # @example no message given
#
- # # no message specified:
- #
# T! { true } # fails
# T! { false } # passes
# T! { nil } # passes
#
- # # message specified:
+ # @example message is given
#
- # T!( "computers do not doublethink" ) { 2 + 2 == 5 } # passes
+ # T!("computers do not doublethink") { 2 + 2 == 5 } # passes
#
- def T! message = nil, &block
- assert_yield :negate, message, &block
+ def T! condition = nil, message = nil, &block
+ assert_yield :negate, condition, message, &block
end
##
- # Returns true if the result of the given block is
- # neither nil nor false. Otherwise, returns false.
+ # Returns true if the given condition or
+ # the result of the given block is neither
+ # nil nor false. Otherwise, returns false.
#
- # ==== Parameters
+ # @param condition (see Dfect.T)
#
- # [message]
+ # @param message
+ #
# This parameter is optional and completely ignored.
#
- # ==== Examples
+ # @example no message given
#
- # # no message specified:
- #
# T? { true } # => true
# T? { false } # => false
# T? { nil } # => false
#
- # # message specified:
+ # @example message is given
#
- # T?( "computers do not doublethink" ) { 2 + 2 != 5 } # => true
+ # T?("computers do not doublethink") { 2 + 2 != 5 } # => true
#
- def T? message = nil, &block
- assert_yield :sample, message, &block
+ def T? condition = nil, message = nil, &block
+ assert_yield :sample, condition, message, &block
end
alias F T!
alias F! T
##
# Returns true if the result of the given block is
# either nil or false. Otherwise, returns false.
#
- # ==== Parameters
+ # @param message (see Dfect.T?)
#
- # [message]
- # This parameter is optional and completely ignored.
+ # @example no message given
#
- # ==== Examples
- #
- # # no message specified:
- #
# F? { true } # => false
# F? { false } # => true
# F? { nil } # => true
#
- # # message specified:
+ # @example message is given
#
# F?( "computers do not doublethink" ) { 2 + 2 == 5 } # => true
#
def F? message = nil, &block
not T? message, &block
@@ -313,307 +344,396 @@
##
# Asserts that one of the given
# kinds of exceptions is raised
# when the given block is executed.
#
- # If the block raises an exception,
- # then that exception is returned.
+ # @return
#
- # Otherwise, nil is returned.
+ # If the block raises an exception,
+ # then that exception is returned.
#
- # ==== Parameters
+ # Otherwise, nil is returned.
#
- # [message]
- # Optional message to show in the
- # report if this assertion fails.
+ # @param [...] kinds_then_message
#
- # [kinds]
- # Exception classes that must be raised by the given block.
+ # Exception classes that must be raised by the given block, optionally
+ # followed by a message to show in the report if this assertion fails.
#
- # If none are given, then StandardError is assumed (similar to how a
- # plain 'rescue' statement without any arguments catches StandardError).
+ # If no exception classes are given, then
+ # StandardError is assumed (similar to
+ # how a plain 'rescue' statement without
+ # any arguments catches StandardError).
#
- # ==== Examples
+ # @example no exceptions given
#
- # # no exceptions specified:
- #
# E { } # fails
# E { raise } # passes
#
- # # single exception specified:
+ # @example single exception given
#
- # E( ArgumentError ) { raise ArgumentError }
- # E( "argument must be invalid", ArgumentError ) { raise ArgumentError }
+ # E(ArgumentError) { raise ArgumentError }
+ # E(ArgumentError, "argument must be invalid") { raise ArgumentError }
#
- # # multiple exceptions specified:
+ # @example multiple exceptions given
#
- # E( SyntaxError, NameError ) { eval "..." }
- # E( "string must compile", SyntaxError, NameError ) { eval "..." }
+ # E(SyntaxError, NameError) { eval "..." }
+ # E(SyntaxError, NameError, "string must compile") { eval "..." }
#
- def E message = nil, *kinds, &block
- assert_raise :assert, message, *kinds, &block
+ def E *kinds_then_message, &block
+ assert_raise :assert, *kinds_then_message, &block
end
##
# Asserts that one of the given kinds of exceptions
# is not raised when the given block is executed.
#
- # If the block raises an exception,
- # then that exception is returned.
+ # @return (see Dfect.E)
#
- # Otherwise, nil is returned.
+ # @param kinds_then_message (see Dfect.E)
#
- # ==== Parameters
+ # @example no exceptions given
#
- # [message]
- # Optional message to show in the
- # report if this assertion fails.
- #
- # [kinds]
- # Exception classes that must not be raised by the given block.
- #
- # If none are given, then StandardError is assumed (similar to how a
- # plain 'rescue' statement without any arguments catches StandardError).
- #
- # ==== Examples
- #
- # # no exceptions specified:
- #
# E! { } # passes
# E! { raise } # fails
#
- # # single exception specified:
+ # @example single exception given
#
- # E!( ArgumentError ) { raise ArgumentError } # fails
- # E!( "argument must be invalid", ArgumentError ) { raise ArgumentError }
+ # E!(ArgumentError) { raise ArgumentError } # fails
+ # E!(ArgumentError, "argument must be invalid") { raise ArgumentError }
#
- # # multiple exceptions specified:
+ # @example multiple exceptions given
#
- # E!( SyntaxError, NameError ) { eval "..." }
- # E!( "string must compile", SyntaxError, NameError ) { eval "..." }
+ # E!(SyntaxError, NameError) { eval "..." }
+ # E!(SyntaxError, NameError, "string must compile") { eval "..." }
#
- def E! message = nil, *kinds, &block
- assert_raise :negate, message, *kinds, &block
+ def E! *kinds_then_message, &block
+ assert_raise :negate, *kinds_then_message, &block
end
##
# Returns true if one of the given kinds of
# exceptions is raised when the given block
# is executed. Otherwise, returns false.
#
- # ==== Parameters
+ # @param [...] kinds_then_message
#
- # [message]
- # This parameter is optional and completely ignored.
+ # Exception classes that must be raised by
+ # the given block, optionally followed by
+ # a message that is completely ignored.
#
- # [kinds]
- # Exception classes that must be raised by the given block.
+ # If no exception classes are given, then
+ # StandardError is assumed (similar to
+ # how a plain 'rescue' statement without
+ # any arguments catches StandardError).
#
- # If none are given, then StandardError is assumed (similar to how a
- # plain 'rescue' statement without any arguments catches StandardError).
+ # @example no exceptions given
#
- # ==== Examples
- #
- # # no exceptions specified:
- #
# E? { } # => false
# E? { raise } # => true
#
- # # single exception specified:
+ # @example single exception given
#
- # E?( ArgumentError ) { raise ArgumentError } # => true
+ # E?(ArgumentError) { raise ArgumentError } # => true
#
- # # multiple exceptions specified:
+ # @example multiple exceptions given
#
- # E?( SyntaxError, NameError ) { eval "..." } # => true
+ # E?(SyntaxError, NameError) { eval "..." } # => true
+ # E!(SyntaxError, NameError, "string must compile") { eval "..." }
#
- def E? message = nil, *kinds, &block
- assert_raise :sample, message, *kinds, &block
+ def E? *kinds_then_message, &block
+ assert_raise :sample, *kinds_then_message, &block
end
##
# Asserts that the given symbol is thrown
# when the given block is executed.
#
- # If a value is thrown along
- # with the expected symbol,
- # then that value is returned.
+ # @return
#
- # Otherwise, nil is returned.
+ # If a value is thrown along
+ # with the expected symbol,
+ # then that value is returned.
#
- # ==== Parameters
+ # Otherwise, nil is returned.
#
- # [message]
- # Optional message to show in the
- # report if this assertion fails.
+ # @param [Symbol] symbol
#
- # [symbol]
# Symbol that must be thrown by the given block.
#
- # ==== Examples
+ # @param message (see Dfect.T)
#
- # # no message specified:
+ # @example no message given
#
# C(:foo) { throw :foo, 123 } # passes, => 123
# C(:foo) { throw :bar, 456 } # fails, => 456
# C(:foo) { } # fails, => nil
#
- # # message specified:
+ # @example message is given
#
- # C( ":foo must be thrown", :foo ) { throw :bar, 789 } # fails, => nil
+ # C(:foo, ":foo must be thrown") { throw :bar, 789 } # fails, => nil
#
- def C message = nil, symbol = nil, &block
- assert_catch :assert, message, symbol, &block
+ def C symbol, message = nil, &block
+ assert_catch :assert, symbol, message, &block
end
##
# Asserts that the given symbol is not
# thrown when the given block is executed.
#
- # Returns nil, always.
+ # @return nil, always.
#
- # ==== Parameters
+ # @param [Symbol] symbol
#
- # [message]
- # Optional message to show in the
- # report if this assertion fails.
- #
- # [symbol]
# Symbol that must not be thrown by the given block.
#
- # ==== Examples
+ # @param message (see Dfect.T)
#
- # # no message specified:
+ # @example no message given
#
# C!(:foo) { throw :foo, 123 } # fails, => nil
# C!(:foo) { throw :bar, 456 } # passes, => nil
# C!(:foo) { } # passes, => nil
#
- # # message specified:
+ # @example message is given
#
- # C!( ":foo must be thrown", :foo ) { throw :bar, 789 } # passes, => nil
+ # C!(:foo, ":foo must be thrown") { throw :bar, 789 } # passes, => nil
#
- def C! message = nil, symbol = nil, &block
- assert_catch :negate, message, symbol, &block
+ def C! symbol, message = nil, &block
+ assert_catch :negate, symbol, message, &block
end
##
# Returns true if the given symbol is thrown when the
# given block is executed. Otherwise, returns false.
#
- # ==== Parameters
+ # @param symbol (see Dfect.C)
#
- # [message]
- # This parameter is optional and completely ignored.
+ # @param message (see Dfect.T?)
#
- # [symbol]
- # Symbol that must be thrown by the given block.
+ # @example no message given
#
- # ==== Examples
- #
- # # no message specified:
- #
# C?(:foo) { throw :foo, 123 } # => true
# C?(:foo) { throw :bar, 456 } # => false
# C?(:foo) { } # => false
#
- # # message specified:
+ # @example message is given
#
- # C?( ":foo must be thrown", :foo ) { throw :bar, 789 } # => false
+ # C?(:foo, ":foo must be thrown") { throw :bar, 789 } # => false
#
- def C? message = nil, symbol = nil, &block
- assert_catch :sample, message, symbol, &block
+ def C? symbol, message = nil, &block
+ assert_catch :sample, symbol, message, &block
end
##
- # Adds the given message to the report inside
+ # Adds the given messages to the report inside
# the section of the currently running test.
#
- # You can think of "S" as "say" or "status".
+ # You can think of "L" as "to log something".
#
- # ==== Parameters
+ # @param messages
#
- # [message]
# Objects to be added to the report.
#
- # ==== Examples
+ # @example single message given
#
- # S "establishing connection..."
+ # L "establishing connection..."
#
- # S "beginning calculation...", Math::PI, [1, 2, 3, ['a', 'b', 'c']]
+ # @example multiple messages given
#
- def S *message
- @exec_trace.concat message
+ # L "beginning calculation...", Math::PI, [1, 2, 3, ['a', 'b', 'c']]
+ #
+ def L *messages
+ @trace.concat messages
end
##
- # Executes all tests defined thus far and stores the results in #report.
+ # Mechanism for sharing code between tests.
#
- # ==== Parameters
+ # If a block is given, it is shared under
+ # the given identifier. Otherwise, the
+ # code block that was previously shared
+ # under the given identifier is injected
+ # into the closest insulated Dfect test
+ # that contains the call to this method.
#
- # [continue]
+ # @param [Symbol, Object] identifier
+ #
+ # An object that identifies shared code. This must be common
+ # knowledge to all parties that want to partake in the sharing.
+ #
+ # @example
+ #
+ # S :knowledge do
+ # #...
+ # end
+ #
+ # D "some test" do
+ # S :knowledge
+ # end
+ #
+ # D "another test" do
+ # S :knowledge
+ # end
+ #
+ def S identifier, &block
+ if block_given?
+ if already_shared = @share[identifier]
+ raise ArgumentError, "A code block #{already_shared.inspect} has already been shared under the identifier #{identifier.inspect}."
+ end
+
+ @share[identifier] = block
+
+ elsif block = @share[identifier]
+ if @tests.empty?
+ raise "Cannot inject code block #{block.inspect} shared under identifier #{identifier.inspect} outside of a Dfect test."
+ else
+ # find the closest insulated parent test; this should always
+ # succeed because root-level tests are insulated by default
+ test = @tests.reverse.find {|t| t.sandbox }
+ test.sandbox.instance_eval(&block)
+ end
+
+ else
+ raise ArgumentError, "No code block is shared under identifier #{identifier.inspect}."
+ end
+ end
+
+ ##
+ # Shares the given code block under the given
+ # identifier and then immediately injects that
+ # code block into the closest insulated Dfect
+ # test that contains the call to this method.
+ #
+ # @param identifier (see Dfect.S)
+ #
+ # @example
+ #
+ # D "some test" do
+ # S! :knowledge do
+ # #...
+ # end
+ # end
+ #
+ # D "another test" do
+ # S :knowledge
+ # end
+ #
+ def S! identifier, &block
+ raise 'block must be given' unless block_given?
+ S identifier, &block
+ S identifier
+ end
+
+ ##
+ # Checks whether any code has been shared under the given identifier.
+ #
+ def S? identifier
+ @share.key? identifier
+ end
+
+ ##
+ # Executes all tests defined thus far and
+ # stores the results in {Dfect.report}.
+ #
+ # @param [Boolean] continue
+ #
# If true, results from previous executions will not be cleared.
#
def run continue = true
# clear previous results
unless continue
- @exec_stats.clear
- @exec_trace.clear
- @test_stack.clear
+ @stats.clear
+ @trace.clear
+ @tests.clear
end
# make new results
+ start = Time.now
catch(:stop_dfect_execution) { execute }
+ finish = Time.now
+ @stats[:time] = finish - start
# print new results
- puts @report.to_yaml unless @options[:quiet]
+ unless @stats.key? :fail or @stats.key? :error
+ #
+ # show execution trace only if all tests passed.
+ # otherwise, we will be repeating already printed
+ # failure details and obstructing the developer!
+ #
+ display @trace
+ end
+
+ display @stats
end
##
- # Stops the execution of the #run method or raises an
- # exception if that method is not currently executing.
+ # Stops the execution of the {Dfect.run} method or raises
+ # an exception if that method is not currently executing.
#
def stop
throw :stop_dfect_execution
end
+ ##
+ # Returns the details of the failure that
+ # is currently being debugged by the user.
+ #
+ def info
+ @trace.last
+ end
+
private
- def assert_yield mode, message = nil, &block
+ def create_test insulate, *description, &block
raise ArgumentError, 'block must be given' unless block
- message ||=
+ description = description.join(' ')
+ sandbox = Object.new if insulate
+
+ @suite.tests << Suite::Test.new(description, block, sandbox)
+ end
+
+ def assert_yield mode, condition = nil, message = nil, &block
+ # first parameter is actually the message when block is given
+ message = condition if block
+
+ message ||= (
+ prefix = block ? 'block must yield' : 'condition must be'
case mode
- when :assert then 'block must yield true (!nil && !false)'
- when :negate then 'block must yield false (nil || false)'
+ when :assert then "#{prefix} true (!nil && !false)"
+ when :negate then "#{prefix} false (nil || false)"
end
+ )
passed = lambda do
- @exec_stats[:passed_assertions] += 1
+ @stats[:pass] += 1
end
failed = lambda do
- @exec_stats[:failed_assertions] += 1
+ @stats[:fail] += 1
debug block, message
end
- result = call(block)
+ result = block ? call(block) : condition
case mode
when :sample then return result ? true : false
when :assert then result ? passed.call : failed.call
when :negate then result ? failed.call : passed.call
end
result
end
- def assert_raise mode, message = nil, *kinds, &block
+ def assert_raise mode, *kinds_then_message, &block
raise ArgumentError, 'block must be given' unless block
- if message.is_a? Class
- kinds.unshift message
+ message = kinds_then_message.pop
+ kinds = kinds_then_message
+
+ if message.kind_of? Class
+ kinds << message
message = nil
end
kinds << StandardError if kinds.empty?
@@ -622,15 +742,15 @@
when :assert then "block must raise #{kinds.join ' or '}"
when :negate then "block must not raise #{kinds.join ' or '}"
end
passed = lambda do
- @exec_stats[:passed_assertions] += 1
+ @stats[:pass] += 1
end
failed = lambda do |exception|
- @exec_stats[:failed_assertions] += 1
+ @stats[:fail] += 1
if exception
# debug the uncaught exception...
debug_uncaught_exception block, exception
@@ -663,29 +783,22 @@
end
exception
end
- def assert_catch mode, message = nil, symbol = nil, &block
+ def assert_catch mode, symbol, message = nil, &block
raise ArgumentError, 'block must be given' unless block
- if message.is_a? Symbol and not symbol
- symbol = message
- message = nil
- end
-
- raise ArgumentError, 'symbol must be given' unless symbol
-
symbol = symbol.to_sym
message ||= "block must throw #{symbol.inspect}"
passed = lambda do
- @exec_stats[:passed_assertions] += 1
+ @stats[:pass] += 1
end
failed = lambda do
- @exec_stats[:failed_assertions] += 1
+ @stats[:fail] += 1
debug block, message
end
# if nothing was thrown, the result of catch()
# is simply the result of executing the block
@@ -717,43 +830,53 @@
result
end
##
+ # Prints the given object in YAML format.
+ #
+ def display object
+ unless @options[:quiet]
+ # stringify symbols in YAML output for better readability
+ puts object.to_yaml.gsub(/^([[:blank:]]*(- )?):(?=@?\w+: )/, '\1')
+ end
+ end
+
+ ##
# Executes the current test suite recursively.
#
def execute
- suite = @curr_suite
- trace = @exec_trace
+ suite = @suite
+ trace = @trace
suite.before_all.each {|b| call b }
suite.tests.each do |test|
suite.before_each.each {|b| call b }
- @test_stack.push test
+ @tests.push test
begin
# create nested suite
- @curr_suite = Suite.new
- @exec_trace = []
+ @suite = Suite.new
+ @trace = []
# populate nested suite
- call test.block
+ call test.block, test.sandbox
# execute nested suite
execute
ensure
# restore outer values
- @curr_suite = suite
+ @suite = suite
- trace << build_trace(@exec_trace)
- @exec_trace = trace
+ trace << build_exec_trace(@trace)
+ @trace = trace
end
- @test_stack.pop
+ @tests.pop
suite.after_each.each {|b| call b }
end
suite.after_all.each {|b| call b }
@@ -761,62 +884,72 @@
##
# Invokes the given block and debugs any
# exceptions that may arise as a result.
#
- def call block
+ def call block, sandbox = nil
begin
- block.call
+ @calls.push block
+
+ if sandbox
+ sandbox.instance_eval(&block)
+ else
+ block.call
+ end
+
rescue Exception => e
debug_uncaught_exception block, e
+
+ ensure
+ @calls.pop
end
end
- INTERNALS = File.dirname(__FILE__) #:nodoc:
+ INTERNALS = File.dirname(__FILE__) # @private
##
# Adds debugging information to the report.
#
- # ==== Parameters
+ # @param [Binding, Proc, #binding] context
#
- # [context]
- # Binding of code being debugged. This
- # can be either a Binding or Proc object.
+ # Binding of code being debugged. This can be either a Binding or
+ # Proc object, or nil if no binding is available---in which case,
+ # the binding of the inner-most enclosing test or hook will be used.
#
- # [message]
+ # @param message
+ #
# Message describing the failure
# in the code being debugged.
#
- # [backtrace]
+ # @param [Array<String>] backtrace
+ #
# Stack trace corresponding to point of
# failure in the code being debugged.
#
def debug context, message = nil, backtrace = caller
+ # inherit binding of enclosing test or hook
+ context ||= @calls.last
+
# allow a Proc to be passed instead of a binding
- if context.respond_to? :binding
+ if context and context.respond_to? :binding
context = context.binding
end
# omit internals from failure details
backtrace = backtrace.reject {|s| s.include? INTERNALS }
# record failure details in the report
- #
- # NOTE: using string keys here instead
- # of symbols because they make
- # the YAML output easier to read
- #
details = {
# user message
- 'fail' => message,
+ :fail => message,
# code snippet
- 'code' => (
+ :code => (
if frame = backtrace.first
file, line = frame.scan(/(.+?):(\d+(?=:|\z))/).first
- if source = @file_cache[file]
+ if source = @files[file]
line = line.to_i
radius = 5 # number of surrounding lines to show
region = [line - radius, 1].max ..
[line + radius, source.length].min
@@ -836,35 +969,39 @@
end
end
),
# variable values
- 'vars' => (
- names = eval('::Kernel.local_variables', context, __FILE__, __LINE__)
+ :vars => if context
+ names = eval('::Kernel.local_variables + self.instance_variables', context, __FILE__, __LINE__)
pairs = names.inject([]) do |pair, name|
variable = name.to_s
value = eval(variable, context, __FILE__, __LINE__)
- pair.push variable, value
+ pair.push variable.to_sym, value
end
Hash[*pairs]
- ),
+ end,
# stack trace
- 'call' => backtrace,
+ :call => backtrace,
}
- @exec_trace << details
+ @trace << details
# allow user to investigate the failure
- if @options[:debug]
- # show the failure to the user
- puts build_trace(details).to_yaml
+ if @options[:debug] and context
+ # show only the most helpful subset of the
+ # failure details, because the rest can be
+ # queried (on demand) inside the debugger
+ overview = details.dup
+ overview.delete :vars
+ overview.delete :call
+ display build_fail_trace(overview)
- # start the investigation
if Kernel.respond_to? :debugger
eval '::Kernel.debugger', context, __FILE__, __LINE__
else
IRB.setup nil
@@ -874,80 +1011,90 @@
catch :IRB_EXIT do
irb.eval_input
end
end
+ else
+ # show all failure details to the user
+ display build_fail_trace(details)
end
nil
end
##
# Debugs the given uncaught exception inside the given context.
#
def debug_uncaught_exception context, exception
- @exec_stats[:uncaught_exceptions] += 1
+ @stats[:error] += 1
debug context, exception, exception.backtrace
end
##
# Returns a report that associates the given
# failure details with the currently running test.
#
- def build_trace details
- if @test_stack.empty?
+ def build_exec_trace details
+ if @tests.empty?
details
else
- { @test_stack.last.desc => details }
+ { @tests.last.desc => details }
end
end
- #:stopdoc:
+ ##
+ # Returns a report that qualifies the given
+ # failure details with the current test stack.
+ #
+ def build_fail_trace details
+ @tests.reverse.inject(details) do |inner, outer|
+ { outer.desc => inner }
+ end
+ end
- class Suite
+ class Suite # @private
attr_reader :tests, :before_each, :after_each, :before_all, :after_all
def initialize
@tests = []
@before_each = []
@after_each = []
@before_all = []
@after_all = []
end
- Test = Struct.new :desc, :block
+ Test = Struct.new(:desc, :block, :sandbox) # @private
end
-
- #:startdoc:
end
- @options = {:debug => true, :quiet => false}
+ @options = {:debug => $DEBUG, :quiet => false}
- @exec_stats = Hash.new {|h,k| h[k] = 0 }
- @exec_trace = []
- @report = {:execution => @exec_trace, :statistics => @exec_stats}.freeze
+ @stats = Hash.new {|h,k| h[k] = 0 }
+ @trace = []
+ @report = {:trace => @trace, :stats => @stats}.freeze
- @curr_suite = class << self; Suite.new; end
+ @suite = class << self; Suite.new; end
+ @share = {}
+ @tests = []
+ @calls = []
+ @files = Hash.new {|h,k| h[k] = File.readlines(k) rescue nil }
- @test_stack = []
- @file_cache = Hash.new {|h,k| h[k] = File.readlines(k) rescue nil }
-
##
- # Allows before and after hooks to be specified via
- # the D() method syntax when this module is mixed-in:
+ # Allows before and after hooks to be specified via the
+ # following method syntax when this module is mixed-in:
#
# D .<< { puts "before all nested tests" }
# D .< { puts "before each nested test" }
# D .> { puts "after each nested test" }
# D .>> { puts "after all nested tests" }
#
D = self
# provide mixin-able assertion methods
- methods(false).grep(/^[[:upper:]][[:punct:]]?$/).each do |name|
+ methods(false).grep(/^[[:upper:]]?[[:punct:]]*$/).each do |name|
#
# XXX: using eval() on a string because Ruby 1.8's
# define_method() cannot take a block parameter
#
- eval "def #{name}(*a, &b) ::#{self}.#{name}(*a, &b) end", binding, __FILE__, __LINE__
+ module_eval "def #{name}(*a, &b) ::#{self.name}.#{name}(*a, &b) end", __FILE__, __LINE__
end
end