# Bacon -- small RSpec clone. # # "Truth will sooner come out from error than from confusion." ---Francis Bacon # Copyright (C) 2007, 2008 Christian Neukirchen # # Bacon is freely distributable under the terms of an MIT-style license. # See COPYING or http://www.opensource.org/licenses/mit-license.php. module Bacon VERSION = "1.0" Counter = Hash.new(0) ErrorLog = "" Shared = Hash.new { |_, name| raise NameError, "no such context: #{name.inspect}" } RestrictName = // unless defined? RestrictName RestrictContext = // unless defined? RestrictContext Backtraces = true unless defined? Backtraces def self.summary_on_exit return if Counter[:installed_summary] > 0 at_exit { handle_summary if $! raise $! elsif Counter[:errors] + Counter[:failed] > 0 exit 1 end } Counter[:installed_summary] += 1 end class < e rescued = true raise e ensure if Counter[:requirements] == prev_req and not rescued raise Error.new(:missing, "empty specification: #{@name} #{description}") end begin @after.each { |block| instance_eval(&block) } rescue Object => e raise e unless rescued end end rescue Object => e ErrorLog << "#{e.class}: #{e.message}\n" e.backtrace.find_all { |line| line !~ /bin\/bacon|\/bacon\.rb:\d+/ }. each_with_index { |line, i| ErrorLog << "\t#{line}#{i==0 ? ": #@name - #{description}" : ""}\n" } ErrorLog << "\n" if e.kind_of? Error Counter[e.count_as] += 1 e.count_as.to_s.upcase else Counter[:errors] += 1 "ERROR: #{e.class}" end else "" ensure Counter[:depth] -= 1 end end end def raise?(*args, &block); block.raise?(*args); end def throw?(*args, &block); block.throw?(*args); end def change?(*args, &block); block.change?(*args); end end end class Object def true?; false; end def false?; false; end end class TrueClass def true?; true; end end class FalseClass def false?; true; end end class Proc def raise?(*exceptions) exceptions = [RuntimeError] if exceptions.empty? call # Only to work in 1.9.0, rescue with splat doesn't work there right now rescue Object => e case e when *exceptions e else raise e end else false end def throw?(sym) catch(sym) { call return false } return true end def change? pre_result = yield called = call post_result = yield pre_result != post_result end end class Numeric def close?(to, delta) (to.to_f - self).abs <= delta.to_f rescue false end end class Object def should(*args, &block) Should.new(self).be(*args, &block) end end module Kernel private def describe(*args, &block) Bacon::Context.new(args.join(' '), &block) end def shared(name, &block) Bacon::Shared[name] = block end end class Should # Kills ==, ===, =~, eql?, equal?, frozen?, instance_of?, is_a?, # kind_of?, nil?, respond_to?, tainted? instance_methods.each { |name| undef_method name if name =~ /\?|^\W+$/ } def initialize(object) @object = object @negated = false end def not(*args, &block) @negated = !@negated if args.empty? self else be(*args, &block) end end def be(*args, &block) if args.empty? self else block = args.shift unless block_given? satisfy(*args, &block) end end alias a be alias an be def satisfy(*args, &block) if args.size == 1 && String === args.first description = args.shift else description = "" end r = yield(@object, *args) if Bacon::Counter[:depth] > 0 Bacon::Counter[:requirements] += 1 raise Bacon::Error.new(:failed, description) unless @negated ^ r end @negated ^ r ? r : false end def method_missing(name, *args, &block) name = "#{name}?" if name.to_s =~ /\w[^?]\z/ desc = @negated ? "not " : "" desc << @object.inspect << "." << name.to_s desc << "(" << args.map{|x|x.inspect}.join(", ") << ") failed" satisfy(desc) { |x| x.__send__(name, *args, &block) } end def equal(value) self == value end def match(value) self =~ value end def identical_to(value) self.equal? value end alias same_as identical_to def flunk(reason="Flunked") raise Bacon::Error.new(:failed, reason) end end