require 'ae/assertion'
require 'ae/basic_object'
module AE
# Assertor is the underlying class of the whole system. It implements
# the flutent assertion notation.
#
# An Assertor is an Assertion Functor. A Functor is a succinct name for what
# is also known as Higher Order Function. In other words, it is a function
# that acts on a function. It is very similiar to a delegator in most
# respects, but is conditioned on the operation applied, rather then simply
# passing-off to an alternate reciever.
#
class Assertor < AE::BasicObject
# Initial settings of assertion counts.
ZERO_COUNTS = {:total=>0,:pass=>0,:fail=>0}
# Initialize assertion counts global variable.
$assertion_counts = ZERO_COUNTS.dup
# Returns Hash used to track assertion counts.
def self.counts
$assertion_counts
end
# Reset assertion counts.
#
# reset - Hash which can be used to set counts manually (optional).
#
# Returns the Hash of previous counts.
def self.recount(reset=nil)
old_counts = counts.dup
if reset
reset.each do |type, value|
counts[type.to_sym] = value
end
else
counts.replace(ZERO_COUNTS.dup)
end
return old_counts
end
# Increment assertion counts. If +pass+ is +true+ then +:total+
# and +:pass+ are increased. If +pass+ if +false+ then +:total+
# and +:fail+ are incremented.
def self.increment_counts(pass)
counts[:total] += 1
if pass
counts[:pass] += 1
else
counts[:fail] += 1
end
return counts
end
# Basic assertion. This method by-passes all the Assertor fluent
# constructs and performs the underlying assertion procedure. It
# is used by Assertor as the end call of an assertion.
def self.assert(pass, error=nil, negated=nil, backtrace=nil)
pass = negated ^ !!pass
increment_counts(pass)
if !pass
backtrace = backtrace || caller
raise_assertion(error, negated, backtrace)
end
return pass
end
# The intent of the method is to raise an assertion failure
# class that the test framework supports.
def self.raise_assertion(error, negated, backtrace=nil)
if not Exception === error
error = assertion_error.new(error)
end
error.set_negative(negated)
error.set_backtrace(backtrace || caller)
error.set_assertion(true)
fail error
end
# Returns the Exception class to be raised when an assertion fails.
def self.assertion_error
::Assertion
end
# NOT HAPPENING
## Is ::Assay defined. This is used for integration of the Assay library.
#def self.assay?
# @_assay ||= defined?(::Assay)
#end
#
#def self.message(sym, neg, *args, &blk)
# if method = Message.lookup(sym)
# method = "non_#{method}" if neg
# Message.send(method, *args, &blk)
# else
# nil
# end
#end
#
if ::RUBY_VERSION >= '1.9'
eval "private :==, :!, :!=" # using eval here b/c it's a syntax error in 1.8-
end
# New Assertor.
#
def initialize(delegate, opts={}) #, backtrace)
@delegate = delegate
@message = opts[:message]
@backtrace = opts[:backtrace] || caller #[1..-1]
@negated = !!opts[:negated]
end
# Negate the meaning of the assertion.
#
#--
# TODO: Should this return a new Assertor instead of in place negation?
#++
def not(msg=nil)
@negated = !@negated
@message = msg if msg
self
end
# Internal assert, provides all functionality associated
# with external #assert method. (See Assert#assert)
#
# NOTE: I'm calling YAGNI on using extra arguments to pass
# to the block. The interface is much nicer if a macro is
# created to handle any neccessry arguments. Eg.
#
# assert something(parameter)
#
# instead of
#
# assert something, parameter
#
# Returns +true+ or +false+ based on assertions success.
#--
# The use of #to_proc and #matches? as sepcial cases is not
# a robust solution.
#++
def assert(*args, &block)
return self if args.empty? && !block
target = block || args.shift
error = nil
# Lambda
if ::Proc === target || target.respond_to?(:to_proc)
block = target.to_proc
match = args.shift
result = block.arity > 0 ? block.call(@delegate) : block.call
if match
pass = (match == result)
error = @message || "#{match.inspect} == #{result.inspect}"
else
pass = result
error = @message || block.inspect # "#{result.inspect}"
end
# Matcher
elsif target.respond_to?(:matches?) # Matchers
pass = target.matches?(@delegate)
error = @message || matcher_message(target) #|| target.inspect
if target.respond_to?(:exception)
#error_class = target.failure_class
error = target.exception #(:backtrace=>@backtrace, :negated=>@negated)
end
# Truthiness
else
pass = target # truthiness
error = args.shift # optional message for TestUnit compatiability
end
__assert__(pass, error)
end
# Internal expect, provides all functionality associated
# with external #expect method. (See Expect#expect)
#
#--
# TODO: Should we deprecate the receiver matches in favor of #expected ?
# In other words, should the || @delegate
be dropped?
#++
def expect(*args, &block)
return self if args.empty? && !block # same as #assert
target = block || args.shift
error = nil
# Lambda
if ::Proc === target #|| target.respond_to?(:to_proc)
#block = target.to_proc
match = args.shift || @delegate
if exception?(match)
$DEBUG, debug = false, $DEBUG # b/c it always spits-out a NameError
begin
block.arity > 0 ? block.call(@delegate) : block.call
pass = false
error = "#{match} not raised"
rescue match => error
pass = true
error = "#{match} raised"
rescue ::Exception => error
pass = false
error = "#{match} expected but #{error.class} was raised"
ensure
$DEBUG = debug
end
else
result = block.arity > 0 ? block.call(@delegte) : block.call
pass = (match === result)
error = @message || "#{match.inspect} === #{result.inspect}"
end
# Matcher
elsif target.respond_to?(:matches?)
pass = target.matches?(@delegate)
error = @message || matcher_message(target) #|| target.inspect
if target.respond_to?(:exception)
#error_class = target.failure_class
error = target.exception #failure(:backtrace=>@backtrace, :negated=>@negated)
end
# Case Equals
else
pass = (target === @delegate)
error = @message || "#{target.inspect} === #{@delegate.inspect}"
end
__assert__(pass, error)
end
#
def flunk(message=nil, backtrace=nil)
__assert__(false, message || @message)
end
# Ruby seems to have a quark in it's implementation whereby
# this must be defined explicitly, otherwise it somehow
# skips #method_missing.
def =~(match)
method_missing(:"=~", match)
end
#
def send(op, *a, &b)
method_missing(op, *a, &b)
end
#
def inspect
@delegate.inspect
end
private
# Is the +object+ an Exception or an instance of one?
#--
# TODO: Should we use a more libreral determination of exception.
# e.g. respond_to?(:exception)
.
#++
def exception?(object)
::Exception === object or ::Class === object and object.ancestors.include?(::Exception)
end
# Converts a missing method into an Assertion.
#
# TODO: In future should probably be `@delegate.public_send(sym, *a, &b)`.
def method_missing(sym, *args, &block)
error = @message || compare_message(sym, *args, &block) || generic_message(sym, *args, &block)
pass = @delegate.__send__(sym, *args, &block)
__assert__(pass, error)
end
# Simple assert.
#--
# TODO: Can the handling of the message be simplified/improved?
#++
def __assert__(pass, error=nil)
Assertor.assert(pass, error, @negated, @backtrace)
end
#
def matcher_message(matcher)
if @negated
if matcher.respond_to?(:negative_failure_message)
return matcher.failure_message
end
else
if matcher.respond_to?(:failure_message)
return matcher.failure_message
end
end
return nil
end
COMPARISON_OPERATORS = { :"==" => :"!=" }
# Message to use when making a comparion assertion.
def compare_message(operator, *args, &blk)
return nil unless COMPARISON_OPERATORS.key?(operator)
prefix = ""
a, b = @delegate, args.first
if @negated
op = COMPARISON_OPERATORS[operator]
if op
operator = op
else
prefix = "NOT "
end
end
if a.size > 13 or b.size > 13
diff = ANSI::Diff.new(a,b)
prefix + "a #{operator} b\na) " + diff.diff1 + "\nb) " + diff.diff2
else
prefix + "#{a.inspect} #{operator} #{b.inspect}"
end
end
# Puts together a suitable error message.
#
def generic_message(op, *a, &b)
inspection = @delegate.send(:inspect)
if @negated
"! #{inspection} #{op} #{a.collect{|x| x.inspect}.join(',')}"
else
"#{inspection} #{op} #{a.collect{|x| x.inspect}.join(',')}"
end
#self.class.message(m)[@delegate, *a] )
end
end
end
# DO WE MAKE THESE EXCEPTIONS?
#class BasicObject
# def assert
# end
#end
# Copyright (c) 2008,2009 Thomas Sawyer