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