1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 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 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
|
require 'ae/assertion'
require 'ae/basic_object'
require 'ae/ansi'
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
# TODO: Should #not return a new Assertor instead of in place negation?
# Negate the meaning of the assertion.
#
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.
#
def assert(*args, &block)
return self if !block && args.empty?
target = args.shift unless block
error = nil
# Block
if block
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
# Proc-style
elsif proc_assertion?(target)
pass, error = proc_apply(target)
# Assay-style assertions
#elsif assay_assertion?(target)
# pass, error = assay_assertion_apply(target)
# RSpec-style matchers
elsif rspec_matcher?(target)
pass, error = rspec_matcher_apply(target)
# Truthiness
else
pass = target # truthiness
error = args.shift # optional message for TestUnit compatiability
end
__assert__(pass, error)
end
# TODO: Should we deprecate the receiver matches in favor of #expected ?
# In other words, should <code>|| @delegate</code> be dropped?
# Internal expect, provides all functionality associated
# with external #expect method. (See Expect#expect)
#
def expect(*args, &block)
return self if !block && args.empty? # same as #assert
pass = false
error = nil
if block
match = args.shift || @delegate # TODO: see above
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 Equality
else
target = args.shift
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
# AE-STYLE ASSERTIONS
#
def proc_assertion?(target)
::Proc === target || target.respond_to?(:call) || target.respond_to?(:to_proc)
end
#
def proc_apply(target)
call = target.method(:call) rescue target.to_proc
pass = call.arity != 0 ? call.call(@delegate) : call.call
error = @message || (
to_s = target.method(:to_s)
to_s.arity == 0 ? to_s.call : to_s.call(@negated)
)
return pass, error
end
# ASSAY-STYLE ASSERTIONS
# (not yet supported b/c api is not 100%)
# Is the `assertion` object an assay-style assertion?
def assay_assertion?(assertion)
assertion.respond_to?(:exception) && assertion.respond_to?(:pass?)
end
#
def assay_assertion_apply(assay)
if @negated
pass = assay.fail?(@delegate)
error = assay #.exception(@message || )
else
pass = assay.pass?(@delegate)
error = assay #.exception(@message || )
end
return pass, error
end
# RSPEC-STYLE MATCHERS
# Is `target` an Rspec-style Matcher?
def rspec_matcher?(target)
target.respond_to?(:matches?)
end
#
def rspec_matcher_apply(matcher)
pass = matcher.matches?(@delegate)
error = @message || rspec_matcher_message(matcher)
return pass, error
end
# TODO: Is there anything to be done with matcher.description?
#
def rspec_matcher_message(matcher)
if @negated
if matcher.respond_to?(:failure_message_for_should_not)
return matcher.failure_message_for_should_not
end
if matcher.respond_to?(:negative_failure_message)
return matcher.negative_failure_message
end
end
if matcher.respond_to?(:failure_message_for_should)
return matcher.failure_message_for_should
end
if matcher.respond_to?(:failure_message)
return matcher.failure_message
end
return matcher.to_s # TODO: or just `nil` ?
end
# TODO: Should we use a more libreral determination of exception.
# e.g. <code>respond_to?(:exception)</code>.
# Is the +object+ an Exception or an instance of one?
def exception?(object)
::Exception === object or ::Class === object and object.ancestors.include?(::Exception)
end
# TODO: In future should probably be `@delegate.public_send(sym, *a, &b)`.
# Converts a missing method into an Assertion.
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
# TODO: Can the handling of the message be simplified/improved?
# Simple assert.
def __assert__(pass, error=nil)
Assertor.assert(pass, error, @negated, @backtrace)
end
#
COMPARISON_OPERATORS = { :"==" => :"!=" }
# Message to use when making a comparion assertion.
#
# NOTE: This message utilizes the ANSI gem to produce colorized
# comparisons. If you need to remove color output (for non-ANSI
# terminals) you can either set `AE.ansi = false` or use the
# ANSI library's master switch to deactive all ANSI codes,
# which can be set in your test helper.
#
# @param operator [Symbol] operator/method
#
# @see http://rubyworks.github.com/ansi
def compare_message(operator, *args, &blk)
return nil unless COMPARISON_OPERATORS.key?(operator)
prefix = ""
a, b = @delegate.inspect, args.first.inspect
if @negated
op = COMPARISON_OPERATORS[operator]
if op
operator = op
else
prefix = "NOT "
end
end
if AE.ansi?
diff = ::ANSI::Diff.new(a,b)
a = diff.diff1
b = diff.diff2
end
if a.size > 13 or b.size > 13
prefix + "a #{operator} b\na) " + a + "\nb) " + b
else
prefix + "#{a} #{operator} #{b}"
end
end
# Puts together a suitable error message.
#
# @param op [Symbol] operator/method
#
# @return [String] 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
# @see http://redmine.ruby-lang.org/issues/3768
def self.const_missing(const)
::Object.const_get(const)
end
end
end
# DO WE MAKE THESE EXCEPTIONS?
#class BasicObject
# def assert
# end
#end
# Copyright (c) 2008 Thomas Sawyer
|