Adding spec libs, so we can use them some day

git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2283 980ebf18-57e1-0310-9a29-db15c13687c0
This commit is contained in:
luke 2007-03-17 02:48:41 +00:00
Родитель 8ea6adaeb1
Коммит ba23a5ac27
73 изменённых файлов: 5095 добавлений и 0 удалений

8
test/lib/spec.rb Normal file
Просмотреть файл

@ -0,0 +1,8 @@
require 'spec/deprecated'
require 'spec/version'
require 'spec/callback'
require 'spec/matchers'
require 'spec/expectations'
require 'spec/mocks'
require 'spec/runner'
require 'spec/translator'

11
test/lib/spec/callback.rb Normal file
Просмотреть файл

@ -0,0 +1,11 @@
require 'spec/callback/callback_container'
require 'spec/callback/extensions/module'
require 'spec/callback/extensions/object'
# Callback is a fork of Brian Takita's "callback library" (see http://callback.rubyforge.org),
# which Brian graciously contributed to RSpec in order to avoid the dependency.
#
# RSpec uses Callback internally to create hooks to Spec::Runner events. If you're interested
# in a simple, powerful API for generating callback events, check out http://callback.rubyforge.org.
module Callback
end

Просмотреть файл

@ -0,0 +1,60 @@
module Callback
class CallbackContainer
def initialize
@callback_registry = Hash.new do |hash, key|
hash[key] = Array.new
end
end
# Defines the callback with the key in this container.
def define(key, callback_proc=nil, &callback_block)
callback = extract_callback(callback_block, callback_proc) do
raise "You must define the callback that accepts the call method."
end
@callback_registry[key] << callback
callback
end
# Undefines the callback with the key in this container.
def undefine(key, callback_proc)
callback = extract_callback(callback_proc) do
raise "You may only undefine callbacks that use the call method."
end
@callback_registry[key].delete callback
callback
end
# Notifies the callbacks for the key. Arguments may be passed.
# An error handler may be passed in as a block. If there is an error, the block is called with
# error object as an argument.
# An array of the return values of the callbacks is returned.
def notify(key, *args, &error_handler)
@callback_registry[key].collect do |callback|
begin
callback.call(*args)
rescue Exception => e
yield(e) if error_handler
end
end
end
# Clears all of the callbacks in this container.
def clear
@callback_registry.clear
end
protected
def extract_callback(first_choice_callback, second_choice_callback = nil)
callback = nil
if first_choice_callback
callback = first_choice_callback
elsif second_choice_callback
callback = second_choice_callback
end
unless callback.respond_to? :call
yield
end
return callback
end
end
end

Просмотреть файл

@ -0,0 +1,24 @@
module Callback
module ModuleMethods
# For each event_name submitted, defines a callback event with this name.
# Client code can then register as a callback listener using object.event_name.
def callback_events(*event_names)
event_names.each do |event_name|
define_callback_event(event_name)
end
end
private
def define_callback_event(event_name)
module_eval <<-EOS
def #{event_name}(&block)
register_callback(:#{event_name}, &block)
end
EOS
end
end
end
class Module
include Callback::ModuleMethods
end

Просмотреть файл

@ -0,0 +1,37 @@
module Callback
module InstanceMethods
# Registers a callback for the event on the object. The callback can either be a block or a proc.
# When the callbacks are notified, the return value of the proc is passed to the caller.
def register_callback(event, callback_proc=nil, &callback_block)
callbacks.define(event, callback_proc, &callback_block)
end
# Removes the callback from the event. The callback proc must be the same
# object as the one that was passed to register_callback.
def unregister_callback(event, callback_proc)
callbacks.undefine(event, callback_proc)
end
protected
# Notifies the callbacks registered with the event on the object. Arguments can be passed to the callbacks.
# An error handler may be passed in as a block. If there is an error, the block is called with
# error object as an argument.
# An array of the return values of the callbacks is returned.
def notify_callbacks(event, *args, &error_handler)
callbacks.notify(event, *args, &error_handler)
end
def notify_class_callbacks(event, *args, &error_handler)
self.class.send(:notify_callbacks, event, *args, &error_handler)
end
# The CallbackContainer for this object.
def callbacks
@callbacks ||= CallbackContainer.new
end
end
end
class Object
include Callback::InstanceMethods
end

Просмотреть файл

@ -0,0 +1,3 @@
def deprecated(&block)
block.call unless ENV['RSPEC_DISABLE_DEPRECATED_FEATURES'] == 'true'
end

Просмотреть файл

@ -0,0 +1,59 @@
require 'spec/deprecated'
require 'spec/matchers'
require 'spec/expectations/sugar'
require 'spec/expectations/errors'
require 'spec/expectations/extensions'
require 'spec/expectations/should'
require 'spec/expectations/handler'
module Spec
# Spec::Expectations lets you set expectations on your objects.
#
# result.should == 37
# team.should have(11).players_on_the_field
#
# == How Expectations work.
#
# Spec::Expectations adds two methods to Object:
#
# should(matcher=nil)
# should_not(matcher=nil)
#
# Both methods take an optional Expression Matcher (See Spec::Matchers).
#
# When +should+ receives an Expression Matcher, it calls <tt>matches?(self)</tt>. If
# it returns +true+, the spec passes and execution continues. If it returns
# +false+, then the spec fails with the message returned by <tt>matcher.failure_message</tt>.
#
# Similarly, when +should_not+ receives a matcher, it calls <tt>matches?(self)</tt>. If
# it returns +false+, the spec passes and execution continues. If it returns
# +true+, then the spec fails with the message returned by <tt>matcher.negative_failure_message</tt>.
#
# RSpec ships with a standard set of useful matchers, and writing your own
# matchers is quite simple. See Spec::Matchers for details.
module Expectations
class << self
attr_accessor :differ
# raises a Spec::Expectations::ExpectationNotMetError with message
#
# When a differ has been assigned and fail_with is passed
# <code>expected</code> and <code>target</code>, passes them
# to the differ to append a diff message to the failure message.
def fail_with(message, expected=nil, target=nil) # :nodoc:
if Array === message && message.length == 3
message, expected, target = message[0], message[1], message[2]
end
unless (differ.nil? || expected.nil? || target.nil?)
if expected.is_a?(String)
message << "\nDiff:" << self.differ.diff_as_string(target.to_s, expected)
elsif !target.is_a?(Proc)
message << "\nDiff:" << self.differ.diff_as_object(target, expected)
end
end
Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message))
end
end
end
end

Просмотреть файл

@ -0,0 +1,62 @@
begin
require 'rubygems'
require 'diff/lcs' #necessary due to loading bug on some machines - not sure why - DaC
require 'diff/lcs/hunk'
rescue LoadError ; raise "You must gem install diff-lcs to use diffing" ; end
require 'pp'
module Spec
module Expectations
module Differs
# TODO add colour support
# TODO add some rdoc
class Default
def initialize(format=:unified,context_lines=nil,colour=nil)
context_lines ||= 3
colour ||= false
@format,@context_lines,@colour = format,context_lines,colour
end
# This is snagged from diff/lcs/ldiff.rb (which is a commandline tool)
def diff_as_string(data_old, data_new)
data_old = data_old.split(/\n/).map! { |e| e.chomp }
data_new = data_new.split(/\n/).map! { |e| e.chomp }
output = ""
diffs = Diff::LCS.diff(data_old, data_new)
return output if diffs.empty?
oldhunk = hunk = nil
file_length_difference = 0
diffs.each do |piece|
begin
hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, @context_lines,
file_length_difference)
file_length_difference = hunk.file_length_difference
next unless oldhunk
# Hunks may overlap, which is why we need to be careful when our
# diff includes lines of context. Otherwise, we might print
# redundant lines.
if (@context_lines > 0) and hunk.overlaps?(oldhunk)
hunk.unshift(oldhunk)
else
output << oldhunk.diff(@format)
end
ensure
oldhunk = hunk
output << "\n"
end
end
#Handle the last remaining hunk
output << oldhunk.diff(@format) << "\n"
end
def diff_as_object(target,expected)
diff_as_string(PP.pp(target,""), PP.pp(expected,""))
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,6 @@
module Spec
module Expectations
class ExpectationNotMetError < StandardError
end
end
end

Просмотреть файл

@ -0,0 +1,3 @@
require 'spec/expectations/extensions/object'
require 'spec/expectations/extensions/proc'
require 'spec/expectations/extensions/string_and_symbol'

Просмотреть файл

@ -0,0 +1,109 @@
module Spec
module Expectations
# rspec adds #should and #should_not to every Object (and,
# implicitly, every Class).
module ObjectExpectations
# :call-seq:
# should(matcher)
# should == expected
# should =~ expected
#
# receiver.should(matcher)
# => Passes if matcher.matches?(receiver)
#
# receiver.should == expected #any value
# => Passes if (receiver == expected)
#
# receiver.should =~ regexp
# => Passes if (receiver =~ regexp)
#
# See Spec::Matchers for more information about matchers
#
# == Warning
#
# NOTE that this does NOT support receiver.should != expected.
# Instead, use receiver.should_not == expected
def should(matcher=nil, &block)
return ExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher
Should::Should.new(self)
end
# :call-seq:
# should_not(matcher)
# should_not == expected
# should_not =~ expected
#
# receiver.should_not(matcher)
# => Passes unless matcher.matches?(receiver)
#
# receiver.should_not == expected
# => Passes unless (receiver == expected)
#
# receiver.should_not =~ regexp
# => Passes unless (receiver =~ regexp)
#
# See Spec::Matchers for more information about matchers
def should_not(matcher=nil, &block)
return NegativeExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher
should.not
end
deprecated do
# Deprecated: use should have(n).items (see Spec::Matchers)
# This will be removed in 0.9
def should_have(expected)
should.have(expected)
end
alias_method :should_have_exactly, :should_have
# Deprecated: use should have_at_least(n).items (see Spec::Matchers)
# This will be removed in 0.9
def should_have_at_least(expected)
should.have.at_least(expected)
end
# Deprecated: use should have_at_most(n).items (see Spec::Matchers)
# This will be removed in 0.9
def should_have_at_most(expected)
should.have.at_most(expected)
end
# Deprecated: use should include(expected) (see Spec::Matchers)
# This will be removed in 0.9
def should_include(expected)
should.include(expected)
end
# Deprecated: use should_not include(expected) (see Spec::Matchers)
# This will be removed in 0.9
def should_not_include(expected)
should.not.include(expected)
end
# Deprecated: use should be(expected) (see Spec::Matchers)
# This will be removed in 0.9
def should_be(expected = :___no_arg)
should.be(expected)
end
# Deprecated: use should_not be(expected) (see Spec::Matchers)
# This will be removed in 0.9
def should_not_be(expected = :___no_arg)
should_not.be(expected)
end
end
end
end
end
class Object
include Spec::Expectations::ObjectExpectations
deprecated do
include Spec::Expectations::UnderscoreSugar
end
end
deprecated do
Object.handle_underscores_for_rspec!
end

Просмотреть файл

@ -0,0 +1,57 @@
module Spec
module Expectations
module ProcExpectations
# Given a receiver and a message (Symbol), specifies that the result
# of sending that message that receiver should change after
# executing the proc.
#
# lambda { @team.add player }.should_change(@team.players, :size)
# lambda { @team.add player }.should_change(@team.players, :size).by(1)
# lambda { @team.add player }.should_change(@team.players, :size).to(23)
# lambda { @team.add player }.should_change(@team.players, :size).from(22).to(23)
#
# You can use a block instead of a message and receiver.
#
# lambda { @team.add player }.should_change{@team.players.size}
# lambda { @team.add player }.should_change{@team.players.size}.by(1)
# lambda { @team.add player }.should_change{@team.players.size}.to(23)
# lambda { @team.add player }.should_change{@team.players.size}.from(22).to(23)
def should_change(receiver=nil, message=nil, &block)
should.change(receiver, message, &block)
end
# Given a receiver and a message (Symbol), specifies that the result
# of sending that message that receiver should NOT change after
# executing the proc.
#
# lambda { @team.add player }.should_not_change(@team.players, :size)
#
# You can use a block instead of a message and receiver.
#
# lambda { @team.add player }.should_not_change{@team.players.size}
def should_not_change(receiver, message)
should.not.change(receiver, message)
end
def should_raise(exception=Exception, message=nil)
should.raise(exception, message)
end
def should_not_raise(exception=Exception, message=nil)
should.not.raise(exception, message)
end
def should_throw(symbol)
should.throw(symbol)
end
def should_not_throw(symbol=:___this_is_a_symbol_that_will_likely_never_occur___)
should.not.throw(symbol)
end
end
end
end
class Proc
include Spec::Expectations::ProcExpectations
end

Просмотреть файл

@ -0,0 +1,17 @@
module Spec
module Expectations
module StringHelpers
def starts_with?(prefix)
to_s[0..(prefix.length - 1)] == prefix
end
end
end
end
class String
include Spec::Expectations::StringHelpers
end
class Symbol
include Spec::Expectations::StringHelpers
end

Просмотреть файл

@ -0,0 +1,47 @@
module Spec
module Expectations
module MatcherHandlerHelper
def describe(matcher)
matcher.respond_to?(:description) ? matcher.description : "[#{matcher.class.name} does not provide a description]"
end
end
class ExpectationMatcherHandler
class << self
include MatcherHandlerHelper
def handle_matcher(actual, matcher, &block)
unless matcher.nil?
match = matcher.matches?(actual, &block)
::Spec::Matchers.generated_description = "should #{describe(matcher)}"
Spec::Expectations.fail_with(matcher.failure_message) unless match
end
end
end
end
class NegativeExpectationMatcherHandler
class << self
include MatcherHandlerHelper
def handle_matcher(actual, matcher, &block)
unless matcher.nil?
unless matcher.respond_to?(:negative_failure_message)
Spec::Expectations.fail_with(
<<-EOF
Matcher does not support should_not.
See Spec::Matchers for more information
about matchers.
EOF
)
end
match = matcher.matches?(actual, &block)
::Spec::Matchers.generated_description = "should not #{describe(matcher)}"
Spec::Expectations.fail_with(matcher.negative_failure_message) if match
end
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,5 @@
require 'spec/expectations/should/base'
require 'spec/expectations/should/have'
require 'spec/expectations/should/not'
require 'spec/expectations/should/should'
require 'spec/expectations/should/change'

Просмотреть файл

@ -0,0 +1,64 @@
module Spec
module Expectations
module Should
class Base
#== and =~ will stay after the new syntax
def ==(expected)
__delegate_method_missing_to_target "==", "==", expected
end
def =~(expected)
__delegate_method_missing_to_target "=~", "=~", expected
end
#<, <=, >=, > are all implemented in Spec::Matchers::Be
# and will be removed with 0.9
deprecated do
def <(expected)
__delegate_method_missing_to_target "<", "<", expected
end
def <=(expected)
__delegate_method_missing_to_target "<=", "<=", expected
end
def >=(expected)
__delegate_method_missing_to_target ">=", ">=", expected
end
def >(expected)
__delegate_method_missing_to_target ">", ">", expected
end
end
def default_message(expectation, expected=nil)
return "expected #{expected.inspect}, got #{@target.inspect} (using #{expectation})" if expectation == '=='
"expected #{expectation} #{expected.inspect}, got #{@target.inspect}" unless expectation == '=='
end
def fail_with_message(message, expected=nil, target=nil)
Spec::Expectations.fail_with(message, expected, target)
end
def find_supported_sym(original_sym)
["#{original_sym}?", "#{original_sym}s?"].each do |alternate_sym|
return alternate_sym.to_s if @target.respond_to?(alternate_sym.to_s)
end
end
deprecated do
def method_missing(original_sym, *args, &block)
if original_sym.to_s =~ /^not_/
return Not.new(@target).__send__(sym, *args, &block)
end
if original_sym.to_s =~ /^have_/
return have.__send__(original_sym.to_s[5..-1].to_sym, *args, &block)
end
__delegate_method_missing_to_target original_sym, find_supported_sym(original_sym), *args
end
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,69 @@
module Spec
module Expectations
module Should
class Change < Base
def initialize(target, receiver=nil, message=nil, &block)
@block = block
@target = target
@receiver = receiver
@message = message
execute_change
evaluate_change
end
def execute_change
@before_change = @block.nil? ? @receiver.send(@message) : @block.call
@target.call
@after_change = @block.nil? ? @receiver.send(@message) : @block.call
end
def message
@message.nil? ? 'result' : @message
end
def evaluate_change
if @before_change == @after_change
fail_with_message "#{message} should have changed, but is still #{@after_change.inspect}"
end
end
def from(value)
if @before_change != value
fail_with_message "#{message} should have initially been #{value.inspect}, but was #{@before_change.inspect}"
end
self
end
def to(value)
if @after_change != value
fail_with_message "#{message} should have been changed to #{value.inspect}, but is now #{@after_change.inspect}"
end
self
end
def by(expected_delta)
if actual_delta != expected_delta
fail_with_message "#{message} should have been changed by #{expected_delta}, but was changed by #{actual_delta}"
end
self
end
private
def actual_delta
@after_change - @before_change
end
end
class NotChange < Change
def evaluate_change
if @before_change != @after_change
fail_with_message "#{@message} should not have changed, but did change from #{@before_change.inspect} to #{@after_change.inspect}"
end
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,128 @@
module Spec
module Expectations
module Should
class Have
def initialize(target, relativity=:exactly, expected=nil)
@target = target
init_collection_handler(target, relativity, expected)
init_item_handler(target)
end
def init_collection_handler(target, relativity, expected)
@collection_handler = CollectionHandler.new(target, relativity, expected)
end
def init_item_handler(target)
@item_handler = PositiveItemHandler.new(target)
end
def method_missing(sym, *args)
if @collection_handler.wants_to_handle(sym)
@collection_handler.handle_message(sym, *args)
elsif @item_handler.wants_to_handle(sym)
@item_handler.handle_message(sym, *args)
else
Spec::Expectations.fail_with("target does not respond to #has_#{sym}?")
end
end
end
class NotHave < Have
def init_item_handler(target)
@item_handler = NegativeItemHandler.new(target)
end
end
class CollectionHandler
def initialize(target, relativity=:exactly, expected=nil)
@target = target
@expected = expected == :no ? 0 : expected
@at_least = (relativity == :at_least)
@at_most = (relativity == :at_most)
end
def at_least(expected_number=nil)
@at_least = true
@at_most = false
@expected = expected_number == :no ? 0 : expected_number
self
end
def at_most(expected_number=nil)
@at_least = false
@at_most = true
@expected = expected_number == :no ? 0 : expected_number
self
end
def method_missing(sym, *args)
if @target.respond_to?(sym)
handle_message(sym, *args)
end
end
def wants_to_handle(sym)
respond_to?(sym) || @target.respond_to?(sym)
end
def handle_message(sym, *args)
return at_least(args[0]) if sym == :at_least
return at_most(args[0]) if sym == :at_most
Spec::Expectations.fail_with(build_message(sym, args)) unless as_specified?(sym, args)
end
def build_message(sym, args)
message = "expected"
message += " at least" if @at_least
message += " at most" if @at_most
message += " #{@expected} #{sym}, got #{actual_size_of(collection(sym, args))}"
end
def as_specified?(sym, args)
return actual_size_of(collection(sym, args)) >= @expected if @at_least
return actual_size_of(collection(sym, args)) <= @expected if @at_most
return actual_size_of(collection(sym, args)) == @expected
end
def collection(sym, args)
@target.send(sym, *args)
end
def actual_size_of(collection)
return collection.length if collection.respond_to? :length
return collection.size if collection.respond_to? :size
end
end
class ItemHandler
def wants_to_handle(sym)
@target.respond_to?("has_#{sym}?")
end
def initialize(target)
@target = target
end
def fail_with(message)
Spec::Expectations.fail_with(message)
end
end
class PositiveItemHandler < ItemHandler
def handle_message(sym, *args)
fail_with(
"expected #has_#{sym}?(#{args.collect{|arg| arg.inspect}.join(', ')}) to return true, got false"
) unless @target.send("has_#{sym}?", *args)
end
end
class NegativeItemHandler < ItemHandler
def handle_message(sym, *args)
fail_with(
"expected #has_#{sym}?(#{args.collect{|arg| arg.inspect}.join(', ')}) to return false, got true"
) if @target.send("has_#{sym}?", *args)
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,74 @@
module Spec
module Expectations
module Should
class Not < Base #:nodoc:
def initialize(target)
@target = target
@be_seen = false
end
deprecated do
#Gone for 0.9
def be(expected = :___no_arg)
@be_seen = true
return self if (expected == :___no_arg)
fail_with_message(default_message("should not be", expected)) if (@target.equal?(expected))
end
#Gone for 0.9
def have(expected_number=nil)
NotHave.new(@target, :exactly, expected_number)
end
#Gone for 0.9
def change(receiver, message)
NotChange.new(@target, receiver, message)
end
#Gone for 0.9
def raise(exception=Exception, message=nil)
begin
@target.call
rescue exception => e
return unless message.nil? || e.message == message || (message.is_a?(Regexp) && e.message =~ message)
if e.kind_of?(exception)
failure_message = "expected no "
failure_message << exception.to_s
unless message.nil?
failure_message << " with "
failure_message << "message matching " if message.is_a?(Regexp)
failure_message << message.inspect
end
failure_message << ", got " << e.inspect
fail_with_message(failure_message)
end
rescue
true
end
end
#Gone for 0.9
def throw(symbol=:___this_is_a_symbol_that_will_likely_never_occur___)
begin
catch symbol do
@target.call
return true
end
fail_with_message("expected #{symbol.inspect} not to be thrown, but it was")
rescue NameError
true
end
end
def __delegate_method_missing_to_target original_sym, actual_sym, *args
::Spec::Matchers.generated_description = "should not #{original_sym} #{args[0].inspect}"
return unless @target.__send__(actual_sym, *args)
fail_with_message(default_message("not #{original_sym}", args[0]))
end
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,81 @@
module Spec
module Expectations
module Should # :nodoc:
class Should < Base
def initialize(target, expectation=nil)
@target = target
@be_seen = false
end
deprecated do
#Gone for 0.9
def not
Not.new(@target)
end
#Gone for 0.9
def be(expected = :___no_arg)
@be_seen = true
return self if (expected == :___no_arg)
if Symbol === expected
fail_with_message(default_message("should be", expected)) unless (@target.equal?(expected))
else
fail_with_message("expected #{expected}, got #{@target} (using .equal?)") unless (@target.equal?(expected))
end
end
#Gone for 0.9
def have(expected_number=nil)
Have.new(@target, :exactly, expected_number)
end
#Gone for 0.9
def change(receiver=nil, message=nil, &block)
Change.new(@target, receiver, message, &block)
end
#Gone for 0.9
def raise(exception=Exception, message=nil)
begin
@target.call
rescue exception => e
unless message.nil?
if message.is_a?(Regexp)
e.message.should =~ message
else
e.message.should == message
end
end
return
rescue => e
fail_with_message("expected #{exception}#{message.nil? ? "" : " with #{message.inspect}"}, got #{e.inspect}")
end
fail_with_message("expected #{exception}#{message.nil? ? "" : " with #{message.inspect}"} but nothing was raised")
end
#Gone for 0.9
def throw(symbol)
begin
catch symbol do
@target.call
fail_with_message("expected #{symbol.inspect} to be thrown, but nothing was thrown")
end
rescue NameError => e
fail_with_message("expected #{symbol.inspect} to be thrown, got #{e.inspect}")
end
end
end
private
def __delegate_method_missing_to_target(original_sym, actual_sym, *args)
::Spec::Matchers.generated_description = "should #{original_sym} #{args[0].inspect}"
return if @target.send(actual_sym, *args)
fail_with_message(default_message(original_sym, args[0]), args[0], @target)
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,47 @@
deprecated do
module Spec
module Expectations
# This module adds syntactic sugar that allows usage of should_* instead of should.*
module UnderscoreSugar
def handle_underscores_for_rspec! # :nodoc:
original_method_missing = instance_method(:method_missing)
class_eval do
def method_missing(sym, *args, &block)
_method_missing(sym, args, block)
end
define_method :_method_missing do |sym, args, block|
return original_method_missing.bind(self).call(sym, *args, &block) unless sym.to_s =~ /^should_/
if sym.to_s =~ /^should_not_/
if __matcher.respond_to?(__strip_should_not(sym))
return should_not(__matcher.__send__(__strip_should_not(sym), *args, &block))
else
return Spec::Expectations::Should::Not.new(self).__send__(__strip_should_not(sym), *args, &block) if sym.to_s =~ /^should_not_/
end
else
if __matcher.respond_to?(__strip_should(sym))
return should(__matcher.__send__(__strip_should(sym), *args, &block))
else
return Spec::Expectations::Should::Should.new(self).__send__(__strip_should(sym), *args, &block)
end
end
end
def __strip_should(sym) # :nodoc
sym.to_s[7..-1]
end
def __strip_should_not(sym) # :nodoc
sym.to_s[11..-1]
end
def __matcher
@matcher ||= Spec::Matchers::Matcher.new
end
end
end
end
end
end
end

160
test/lib/spec/matchers.rb Normal file
Просмотреть файл

@ -0,0 +1,160 @@
require 'spec/deprecated'
require 'spec/callback'
require 'spec/matchers/be'
require 'spec/matchers/be_close'
require 'spec/matchers/change'
require 'spec/matchers/eql'
require 'spec/matchers/equal'
require 'spec/matchers/has'
require 'spec/matchers/have'
require 'spec/matchers/include'
require 'spec/matchers/match'
require 'spec/matchers/raise_error'
require 'spec/matchers/respond_to'
require 'spec/matchers/satisfy'
require 'spec/matchers/throw_symbol'
module Spec
# RSpec ships with a number of useful Expression Matchers. An Expression Matcher
# is any object that responds to the following methods:
#
# matches?(actual)
# failure_message
# negative_failure_message #optional
# description #optional
#
# See Spec::Expectations to learn how to use these as Expectation Matchers.
# See Spec::Mocks to learn how to use them as Mock Argument Constraints.
#
# == Predicates
#
# In addition to those Expression Matchers that are defined explicitly, RSpec will
# create custom Matchers on the fly for any arbitrary predicate, giving your specs
# a much more natural language feel.
#
# A Ruby predicate is a method that ends with a "?" and returns true or false.
# Common examples are +empty?+, +nil?+, and +instance_of?+.
#
# All you need to do is write +should be_+ followed by the predicate without
# the question mark, and RSpec will figure it out from there. For example:
#
# [].should be_empty => [].empty? #passes
# [].should_not be_empty => [].empty? #fails
#
# In addtion to prefixing the predicate matchers with "be_", you can also use "be_a_"
# and "be_an_", making your specs read much more naturally:
#
# "a string".should be_an_instance_of(String) =>"a string".instance_of?(String) #passes
#
# 3.should be_a_kind_of(Fixnum) => 3.kind_of?(Numeric) #passes
# 3.should be_a_kind_of(Numeric) => 3.kind_of?(Numeric) #passes
# 3.should be_an_instance_of(Fixnum) => 3.instance_of?(Fixnum) #passes
# 3.should_not be_instance_of(Numeric) => 3.instance_of?(Numeric) #fails
#
# RSpec will also create custom matchers for predicates like +has_key?+. To
# use this feature, just state that the object should have_key(:key) and RSpec will
# call has_key?(:key) on the target. For example:
#
# {:a => "A"}.should have_key(:a) => {:a => "A"}.has_key?(:a) #passes
# {:a => "A"}.should have_key(:b) => {:a => "A"}.has_key?(:b) #fails
#
# You can use this feature to invoke any predicate that begins with "has_", whether it is
# part of the Ruby libraries (like +Hash#has_key?+) or a method you wrote on your own class.
#
# == Custom Expression Matchers
#
# When you find that none of the stock Expression Matchers provide a natural
# feeling expectation, you can very easily write your own.
#
# For example, imagine that you are writing a game in which players can
# be in various zones on a virtual board. To specify that bob should
# be in zone 4, you could say:
#
# bob.current_zone.should eql(Zone.new("4"))
#
# But you might find it more expressive to say:
#
# bob.should be_in_zone("4")
#
# and/or
#
# bob.should_not be_in_zone("3")
#
# To do this, you would need to write a class like this:
#
# class BeInZone
# def initialize(expected)
# @expected = expected
# end
# def matches?(actual)
# @actual = actual
# bob.current_zone.eql?(Zone.new(@expected))
# end
# def failure_message
# "expected #{@actual.inspect} to be in Zone #{@expected}"
# end
# def negative_failure_message
# "expected #{@actual.inspect} not to be in Zone #{@expected}"
# end
# end
#
# ... and a method like this:
#
# def be_in_zone(expected)
# BeInZone.new(expected)
# end
#
# And then expose the method to your specs. This is normally done
# by including the method and the class in a module, which is then
# included in your spec:
#
# module CustomGameMatchers
# class BeInZone
# ...
# end
#
# def be_in_zone(expected)
# ...
# end
# end
#
# context "Player behaviour" do
# include CustomGameMatchers
# ...
# end
module Matchers
class << self
callback_events :description_generated
def generated_description=(name)
notify_callbacks(:description_generated, name)
end
end
def method_missing(sym, *args, &block) # :nodoc:
return Matchers::Be.new(sym, *args) if sym.starts_with?("be_")
return Matchers::Has.new(sym, *args) if sym.starts_with?("have_")
super
end
deprecated do
# This supports sugar delegating to Matchers
class Matcher #:nodoc:
include Matchers
def respond_to?(sym)
if sym.to_s[0..2] == "be_"
return true
else
super
end
end
end
end
class MatcherError < StandardError
end
end
end

Просмотреть файл

@ -0,0 +1,161 @@
module Spec
module Matchers
class Be #:nodoc:
def initialize(expected=nil, *args)
@expected = parse_expected(expected)
@args = args
@comparison = ""
end
def matches?(actual)
@actual = actual
return true if match_or_compare unless handling_predicate?
if handling_predicate?
begin
return @result = actual.__send__(predicate, *@args)
rescue => predicate_error
# This clause should be empty, but rcov will not report it as covered
# unless something (anything) is executed within the clause
rcov_error_report = "http://eigenclass.org/hiki.rb?rcov-0.8.0"
end
# This supports should_exist > target.exists? in the old world.
# We should consider deprecating that ability as in the new world
# you can't write "should exist" unless you have your own custom matcher.
begin
return @result = actual.__send__(present_tense_predicate, *@args)
rescue
raise predicate_error
end
end
return false
end
def failure_message
return "expected #{@comparison}#{expected}, got #{@actual.inspect}" unless handling_predicate?
return "expected #{predicate}#{args_to_s} to return true, got #{@result.inspect}"
end
def negative_failure_message
return "expected not #{expected}, got #{@actual.inspect}" unless handling_predicate?
return "expected #{predicate}#{args_to_s} to return false, got #{@result.inspect}"
end
def expected
return true if @expected == :true
return false if @expected == :false
return "nil" if @expected == :nil
return @expected.inspect
end
def match_or_compare
return @actual == true if @expected == :true
return @actual == false if @expected == :false
return @actual.nil? if @expected == :nil
return @actual < @expected if @less_than
return @actual <= @expected if @less_than_or_equal
return @actual >= @expected if @greater_than_or_equal
return @actual > @expected if @greater_than
return @actual.equal?(@expected)
end
def <(expected)
@less_than = true
@comparison = "< "
@expected = expected
self
end
def <=(expected)
@less_than_or_equal = true
@comparison = "<= "
@expected = expected
self
end
def >=(expected)
@greater_than_or_equal = true
@comparison = ">= "
@expected = expected
self
end
def >(expected)
@greater_than = true
@comparison = "> "
@expected = expected
self
end
def description
"be #{@comparison}#{@expected}"
end
private
def parse_expected(expected)
if Symbol === expected
["be_an_","be_a_","be_"].each do |prefix|
@handling_predicate = true
return "#{expected.to_s.sub(prefix,"")}".to_sym if expected.starts_with?(prefix)
end
end
return expected
end
def predicate
"#{@expected.to_s}?".to_sym
end
def present_tense_predicate
"#{@expected.to_s}s?".to_sym
end
def args_to_s
return "" if @args.empty?
transformed_args = @args.collect{|a| a.inspect}
return "(#{transformed_args.join(', ')})"
end
def handling_predicate?
return false if [:true, :false, :nil].include?(@expected)
return @handling_predicate
end
end
# :call-seq:
# should be_true
# should be_false
# should be_nil
# should be_arbitrary_predicate(*args)
# should_not be_nil
# should_not be_arbitrary_predicate(*args)
#
# Given true, false, or nil, will pass if actual is
# true, false or nil (respectively).
#
# Predicates are any Ruby method that ends in a "?" and returns true or false.
# Given be_ followed by arbitrary_predicate (without the "?"), RSpec will match
# convert that into a query against the target object.
#
# The arbitrary_predicate feature will handle any predicate
# prefixed with "be_an_" (e.g. be_an_instance_of), "be_a_" (e.g. be_a_kind_of)
# or "be_" (e.g. be_empty), letting you choose the prefix that best suits the predicate.
#
# == Examples
#
# target.should be_true
# target.should be_false
# target.should be_nil
# target.should_not be_nil
#
# collection.should be_empty #passes if target.empty?
# "this string".should be_an_intance_of(String)
#
# target.should_not be_empty #passes unless target.empty?
# target.should_not be_old_enough(16) #passes unless target.old_enough?(16)
def be(*args)
Matchers::Be.new(*args)
end
end
end

Просмотреть файл

@ -0,0 +1,37 @@
module Spec
module Matchers
class BeClose #:nodoc:
def initialize(expected, delta)
@expected = expected
@delta = delta
end
def matches?(actual)
@actual = actual
(@actual - @expected).abs < @delta
end
def failure_message
"expected #{@expected} +/- (<#{@delta}), got #{@actual}"
end
def description
"be close to #{@expected} (+- #{@delta})"
end
end
# :call-seq:
# should be_close(expected, delta)
# should_not be_close(expected, delta)
#
# Passes if actual == expected +/- delta
#
# == Example
#
# result.should be_close(3.0, 0.5)
def be_close(expected, delta)
Matchers::BeClose.new(expected, delta)
end
end
end

Просмотреть файл

@ -0,0 +1,120 @@
module Spec
module Matchers
#Based on patch from Wilson Bilkovich
class Change #:nodoc:
def initialize(receiver=nil, message=nil, &block)
@receiver = receiver
@message = message
@block = block
end
def matches?(target, &block)
if block
raise MatcherError.new(<<-EOF
block passed to should or should_not change must use {} instead of do/end
EOF
)
end
@target = target
execute_change
return false if @from && (@from != @before)
return false if @to && (@to != @after)
return (@before + @amount == @after) if @amount
return @before != @after
end
def execute_change
@before = @block.nil? ? @receiver.send(@message) : @block.call
@target.call
@after = @block.nil? ? @receiver.send(@message) : @block.call
end
def failure_message
if @to
"#{result} should have been changed to #{@to.inspect}, but is now #{@after.inspect}"
elsif @from
"#{result} should have initially been #{@from.inspect}, but was #{@before.inspect}"
elsif @amount
"#{result} should have been changed by #{@amount.inspect}, but was changed by #{actual_delta.inspect}"
else
"#{result} should have changed, but is still #{@before.inspect}"
end
end
def result
@message || "result"
end
def actual_delta
@after - @before
end
def negative_failure_message
"#{result} should not have changed, but did change from #{@before.inspect} to #{@after.inspect}"
end
def by(amount)
@amount = amount
self
end
def to(to)
@to = to
self
end
def from (from)
@from = from
self
end
end
# :call-seq:
# should change(receiver, message, &block)
# should change(receiver, message, &block).by(value)
# should change(receiver, message, &block).from(old).to(new)
# should_not change(receiver, message, &block)
#
# Allows you to specify that a Proc will cause some value to change.
#
# == Examples
#
# lambda {
# team.add_player(player)
# }.should change(roster, :count)
#
# lambda {
# team.add_player(player)
# }.should change(roster, :count).by(1)
#
# string = "string"
# lambda {
# string.reverse
# }.should change { string }.from("string").to("gnirts")
#
# lambda {
# person.happy_birthday
# }.should change(person, :birthday).from(32).to(33)
#
# lambda {
# employee.develop_great_new_social_networking_app
# }.should change(employee, :title).from("Mail Clerk").to("CEO")
#
# Evaluates +receiver.message+ or +block+ before and
# after it evaluates the c object (generated by the lambdas in the examples above).
#
# Then compares the values before and after the +receiver.message+ and
# evaluates the difference compared to the expected difference.
#
# == Warning
# +should_not+ +change+ only supports the form with no subsequent calls to
# +be+, +to+ or +from+.
#
# blocks passed to +should+ +change+ and +should_not+ +change+
# must use the <tt>{}</tt> form (<tt>do/end</tt> is not supported)
def change(target=nil, message=nil, &block)
Matchers::Change.new(target, message, &block)
end
end
end

Просмотреть файл

@ -0,0 +1,43 @@
module Spec
module Matchers
class Eql #:nodoc:
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.eql?(@expected)
end
def failure_message
return "expected #{@expected.inspect}, got #{@actual.inspect} (using .eql?)", @expected, @actual
end
def negative_failure_message
return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .eql?)", @expected, @actual
end
def description
"eql #{@expected.inspect}"
end
end
# :call-seq:
# should eql(expected)
# should_not eql(expected)
#
# Passes if actual and expected are of equal value, but not necessarily the same object.
#
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more information about equality in Ruby.
#
# == Examples
#
# 5.should eql(5)
# 5.should_not eql(3)
def eql(expected)
Matchers::Eql.new(expected)
end
end
end

Просмотреть файл

@ -0,0 +1,43 @@
module Spec
module Matchers
class Equal #:nodoc:
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.equal?(@expected)
end
def failure_message
return "expected #{@expected.inspect}, got #{@actual.inspect} (using .equal?)", @expected, @actual
end
def negative_failure_message
return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .equal?)", @expected, @actual
end
def description
"equal #{@expected.inspect}"
end
end
# :call-seq:
# should equal(expected)
# should_not equal(expected)
#
# Passes if actual and expected are the same object (object identity).
#
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more information about equality in Ruby.
#
# == Examples
#
# 5.should equal(5) #Fixnums are equal
# "5".should_not equal("5") #Strings that look the same are not the same object
def equal(expected)
Matchers::Equal.new(expected)
end
end
end

Просмотреть файл

@ -0,0 +1,44 @@
module Spec
module Matchers
class Has #:nodoc:
def initialize(sym, *args)
@sym = sym
@args = args
end
def matches?(target)
@target = target
begin
return target.send(predicate, *@args)
rescue => @error
# This clause should be empty, but rcov will not report it as covered
# unless something (anything) is executed within the clause
rcov_error_report = "http://eigenclass.org/hiki.rb?rcov-0.8.0"
end
return false
end
def failure_message
raise @error if @error
"expected ##{predicate}(#{@args[0].inspect}) to return true, got false"
end
def negative_failure_message
raise @error if @error
"expected ##{predicate}(#{@args[0].inspect}) to return false, got true"
end
def description
"have key #{@args[0].inspect}"
end
private
def predicate
"#{@sym.to_s.sub("have_","has_")}?".to_sym
end
end
end
end

Просмотреть файл

@ -0,0 +1,140 @@
module Spec
module Matchers
class Have #:nodoc:
def initialize(expected, relativity=:exactly)
@expected = (expected == :no ? 0 : expected)
@relativity = relativity
end
def relativities
@relativities ||= {
:exactly => "",
:at_least => "at least ",
:at_most => "at most "
}
end
def method_missing(sym, *args, &block)
@collection_name = sym
@args = args
@block = block
self
end
def matches?(collection_owner)
if collection_owner.respond_to?(collection_name)
collection = collection_owner.send(collection_name, *@args, &@block)
elsif (collection_owner.respond_to?(:length) || collection_owner.respond_to?(:size))
collection = collection_owner
else
collection_owner.send(collection_name, *@args, &@block)
end
@actual = collection.length if collection.respond_to?(:length)
@actual = collection.size if collection.respond_to?(:size)
return @actual >= @expected if @relativity == :at_least
return @actual <= @expected if @relativity == :at_most
return @actual == @expected
end
def failure_message
"expected #{relative_expectation} #{collection_name}, got #{@actual}"
end
def negative_failure_message
if @relativity == :exactly
return "expected target not to have #{@expected} #{collection_name}, got #{@actual}"
elsif @relativity == :at_most
return <<-EOF
Isn't life confusing enough?
Instead of having to figure out the meaning of this:
should_not have_at_most(#{@expected}).#{collection_name}
We recommend that you use this instead:
should have_at_least(#{@expected + 1}).#{collection_name}
EOF
elsif @relativity == :at_least
return <<-EOF
Isn't life confusing enough?
Instead of having to figure out the meaning of this:
should_not have_at_least(#{@expected}).#{collection_name}
We recommend that you use this instead:
should have_at_most(#{@expected - 1}).#{collection_name}
EOF
end
end
def description
"have #{relative_expectation} #{collection_name}"
end
private
def collection_name
@collection_name
end
def relative_expectation
"#{relativities[@relativity]}#{@expected}"
end
end
# :call-seq:
# should have(number).named_collection__or__sugar
# should_not have(number).named_collection__or__sugar
#
# Passes if receiver is a collection with the submitted
# number of items OR if the receiver OWNS a collection
# with the submitted number of items.
#
# If the receiver OWNS the collection, you must use the name
# of the collection. So if a <tt>Team</tt> instance has a
# collection named <tt>#players</tt>, you must use that name
# to set the expectation.
#
# If the receiver IS the collection, you can use any name
# you like for <tt>named_collection</tt>. We'd recommend using
# either "elements", "members", or "items" as these are all
# standard ways of describing the things IN a collection.
#
# This also works for Strings, letting you set an expectation
# about its length
#
# == Examples
#
# # Passes if team.players.size == 11
# team.should have(11).players
#
# # Passes if [1,2,3].length == 3
# [1,2,3].should have(3).items #"items" is pure sugar
#
# # Passes if "this string".length == 11
# "this string".should have(11).characters #"characters" is pure sugar
def have(n)
Matchers::Have.new(n)
end
alias :have_exactly :have
# :call-seq:
# should have_at_least(number).items
#
# Exactly like have() with >=.
#
# == Warning
#
# +should_not+ +have_at_least+ is not supported
def have_at_least(n)
Matchers::Have.new(n, :at_least)
end
# :call-seq:
# should have_at_most(number).items
#
# Exactly like have() with <=.
#
# == Warning
#
# +should_not+ +have_at_most+ is not supported
def have_at_most(n)
Matchers::Have.new(n, :at_most)
end
end
end

Просмотреть файл

@ -0,0 +1,50 @@
module Spec
module Matchers
class Include #:nodoc:
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
actual.include?(@expected)
end
def failure_message
_message
end
def negative_failure_message
_message("not ")
end
def description
"include #{@expected.inspect}"
end
private
def _message(maybe_not="")
"expected #{@actual.inspect} #{maybe_not}to include #{@expected.inspect}"
end
end
# :call-seq:
# should include(expected)
# should_not include(expected)
#
# Passes if actual includes expected. This works for
# collections and Strings
#
# == Examples
#
# [1,2,3].should include(3)
# [1,2,3].should_not include(4)
# "spread".should include("read")
# "spread".should_not include("red")
def include(expected)
Matchers::Include.new(expected)
end
end
end

Просмотреть файл

@ -0,0 +1,41 @@
module Spec
module Matchers
class Match #:nodoc:
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
return true if actual =~ @expected
return false
end
def failure_message
return "expected #{@actual.inspect} to match #{@expected.inspect}", @expected, @actual
end
def negative_failure_message
return "expected #{@actual.inspect} not to match #{@expected.inspect}", @expected, @actual
end
def description
"match #{@expected.inspect}"
end
end
# :call-seq:
# should match(regexp)
# should_not match(regexp)
#
# Given a Regexp, passes if actual =~ regexp
#
# == Examples
#
# email.should match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
def match(regexp)
Matchers::Match.new(regexp)
end
end
end

Просмотреть файл

@ -0,0 +1,100 @@
module Spec
module Matchers
class RaiseError #:nodoc:
def initialize(exception=Exception, message=nil)
@expected_error = exception
@expected_message = message
end
def matches?(proc)
@raised_expected_error = false
@raised_other = false
begin
proc.call
rescue @expected_error => @actual_error
if @expected_message.nil?
@raised_expected_error = true
else
case @expected_message
when Regexp
if @expected_message =~ @actual_error.message
@raised_expected_error = true
else
@raised_other = true
end
else
if @actual_error.message == @expected_message
@raised_expected_error = true
else
@raised_other = true
end
end
end
rescue => @actual_error
@raised_other = true
ensure
return @raised_expected_error
end
end
def failure_message
return "expected #{expected_error}#{actual_error}" if @raised_other || !@raised_expected_error
end
def negative_failure_message
"expected no #{expected_error}#{actual_error}"
end
def description
"raise #{expected_error}"
end
private
def expected_error
case @expected_message
when nil
@expected_error
when Regexp
"#{@expected_error} with message matching #{@expected_message.inspect}"
else
"#{@expected_error} with #{@expected_message.inspect}"
end
end
def actual_error
@actual_error.nil? ? " but nothing was raised" : ", got #{@actual_error.inspect}"
end
end
# :call-seq:
# should raise_error()
# should raise_error(NamedError)
# should raise_error(NamedError, String)
# should raise_error(NamedError, Regexp)
# should_not raise_error()
# should_not raise_error(NamedError)
# should_not raise_error(NamedError, String)
# should_not raise_error(NamedError, Regexp)
#
# With no args, matches if any error is raised.
# With a named error, matches only if that specific error is raised.
# With a named error and messsage specified as a String, matches only if both match.
# With a named error and messsage specified as a Regexp, matches only if both match.
#
# == Examples
#
# lambda { do_something_risky }.should raise_error
# lambda { do_something_risky }.should raise_error(PoorRiskDecisionError)
# lambda { do_something_risky }.should raise_error(PoorRiskDecisionError, "that was too risky")
# lambda { do_something_risky }.should raise_error(PoorRiskDecisionError, /oo ri/)
#
# lambda { do_something_risky }.should_not raise_error
# lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError)
# lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError, "that was too risky")
# lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError, /oo ri/)
def raise_error(error=Exception, message=nil)
Matchers::RaiseError.new(error, message)
end
end
end

Просмотреть файл

@ -0,0 +1,35 @@
module Spec
module Matchers
class RespondTo #:nodoc:
def initialize(sym)
@sym = sym
end
def matches?(target)
return target.respond_to?(@sym)
end
def failure_message
"expected target to respond to #{@sym.inspect}"
end
def negative_failure_message
"expected target not to respond to #{@sym.inspect}"
end
def description
"respond to ##{@sym.to_s}"
end
end
# :call-seq:
# should respond_to(:sym)
# should_not respond_to(:sym)
#
# Matches if the target object responds to :sym
def respond_to(sym)
Matchers::RespondTo.new(sym)
end
end
end

Просмотреть файл

@ -0,0 +1,47 @@
module Spec
module Matchers
class Satisfy #:nodoc:
def initialize(&block)
@block = block
end
def matches?(actual, &block)
@block = block if block
@actual = actual
@block.call(actual)
end
def failure_message
"expected #{@actual} to satisfy block"
end
def negative_failure_message
"expected #{@actual} not to satisfy block"
end
end
# :call-seq:
# should satisfy {}
# should_not satisfy {}
#
# Passes if the submitted block returns true. Yields target to the
# block.
#
# Generally speaking, this should be thought of as a last resort when
# you can't find any other way to specify the behaviour you wish to
# specify.
#
# If you do find yourself in such a situation, you could always write
# a custom matcher, which would likely make your specs more expressive.
#
# == Examples
#
# 5.should satisfy { |n|
# n > 3
# }
def satisfy(&block)
Matchers::Satisfy.new(&block)
end
end
end

Просмотреть файл

@ -0,0 +1,75 @@
module Spec
module Matchers
class ThrowSymbol #:nodoc:
def initialize(expected=nil)
@expected = expected
end
def matches?(proc)
begin
proc.call
rescue NameError => e
@actual = extract_sym_from_name_error(e)
ensure
if @expected.nil?
return @actual.nil? ? false : true
else
return @actual == @expected
end
end
end
def failure_message
if @actual
"expected #{expected}, got #{@actual.inspect}"
else
"expected #{expected} but nothing was thrown"
end
end
def negative_failure_message
if @expected
"expected #{expected} not to be thrown"
else
"expected no Symbol, got :#{@actual}"
end
end
def description
"throw #{expected}"
end
private
def expected
@expected.nil? ? "a Symbol" : @expected.inspect
end
def extract_sym_from_name_error(error)
return "#{error.message.split("`").last.split("'").first}".to_sym
end
end
# :call-seq:
# should throw_symbol()
# should throw_symbol(:sym)
# should_not throw_symbol()
# should_not throw_symbol(:sym)
#
# Given a Symbol argument, matches if a proc throws the specified Symbol.
#
# Given no argument, matches if a proc throws any Symbol.
#
# == Examples
#
# lambda { do_something_risky }.should throw_symbol
# lambda { do_something_risky }.should throw_symbol(:that_was_risky)
#
# lambda { do_something_risky }.should_not throw_symbol
# lambda { do_something_risky }.should_not throw_symbol(:that_was_risky)
def throw_symbol(sym=nil)
Matchers::ThrowSymbol.new(sym)
end
end
end

232
test/lib/spec/mocks.rb Normal file
Просмотреть файл

@ -0,0 +1,232 @@
require 'spec/mocks/methods'
require 'spec/mocks/mock_handler'
require 'spec/mocks/mock'
require 'spec/mocks/argument_expectation'
require 'spec/mocks/message_expectation'
require 'spec/mocks/order_group'
require 'spec/mocks/errors'
require 'spec/mocks/error_generator'
require 'spec/mocks/extensions/object'
module Spec
# == Mocks and Stubs
#
# RSpec will create Mock Objects and Stubs for you at runtime, or attach stub/mock behaviour
# to any of your real objects (Partial Mock/Stub). Because the underlying implementation
# for mocks and stubs is the same, you can intermingle mock and stub
# behaviour in either dynamically generated mocks or your pre-existing classes.
# There is a semantic difference in how they are created, however,
# which can help clarify the role it is playing within a given spec.
#
# == Mock Objects
#
# Mocks are objects that allow you to set and verify expectations that they will
# receive specific messages during run time. They are very useful for specifying how the subject of
# the spec interacts with its collaborators. This approach is widely known as "interaction
# testing".
#
# Mocks are also very powerful as a design tool. As you are
# driving the implementation of a given class, Mocks provide an anonymous
# collaborator that can change in behaviour as quickly as you can write an expectation in your
# spec. This flexibility allows you to design the interface of a collaborator that often
# does not yet exist. As the shape of the class being specified becomes more clear, so do the
# requirements for its collaborators - often leading to the discovery of new types that are
# needed in your system.
#
# Read Endo-Testing[http://www.mockobjects.com/files/endotesting.pdf] for a much
# more in depth description of this process.
#
# == Stubs
#
# Stubs are objects that allow you to set "stub" responses to
# messages. As Martin Fowler points out on his site,
# mocks_arent_stubs[http://www.martinfowler.com/articles/mocksArentStubs.html].
# Paraphrasing Fowler's paraphrasing
# of Gerard Meszaros: Stubs provide canned responses to messages they might receive in a test, while
# mocks allow you to specify and, subsquently, verify that certain messages should be received during
# the execution of a test.
#
# == Partial Mocks/Stubs
#
# RSpec also supports partial mocking/stubbing, allowing you to add stub/mock behaviour
# to instances of your existing classes. This is generally
# something to be avoided, because changes to the class can have ripple effects on
# seemingly unrelated specs. When specs fail due to these ripple effects, the fact
# that some methods are being mocked can make it difficult to understand why a
# failure is occurring.
#
# That said, partials do allow you to expect and
# verify interactions with class methods such as +#find+ and +#create+
# on Ruby on Rails model classes.
#
# == Further Reading
#
# There are many different viewpoints about the meaning of mocks and stubs. If you are interested
# in learning more, here is some recommended reading:
#
# * Mock Objects: http://www.mockobjects.com/
# * Endo-Testing: http://www.mockobjects.com/files/endotesting.pdf
# * Mock Roles, Not Objects: http://www.mockobjects.com/files/mockrolesnotobjects.pdf
# * Test Double Patterns: http://xunitpatterns.com/Test%20Double%20Patterns.html
# * Mocks aren't stubs: http://www.martinfowler.com/articles/mocksArentStubs.html
#
# == Creating a Mock
#
# You can create a mock in any specification (or setup) using:
#
# mock(name, options={})
#
# The optional +options+ argument is a +Hash+. Currently the only supported
# option is +:null_object+. Setting this to true instructs the mock to ignore
# any messages it hasnt been told to expect – and quietly return itself. For example:
#
# mock("person", :null_object => true)
#
# == Creating a Stub
#
# You can create a stub in any specification (or setup) using:
#
# stub(name, stub_methods_and_values_hash)
#
# For example, if you wanted to create an object that always returns
# "More?!?!?!" to "please_sir_may_i_have_some_more" you would do this:
#
# stub("Mr Sykes", :please_sir_may_i_have_some_more => "More?!?!?!")
#
# == Creating a Partial Mock
#
# You don't really "create" a partial mock, you simply add method stubs and/or
# mock expectations to existing classes and objects:
#
# Factory.should_receive(:find).with(id).and_return(value)
# obj.stub!(:to_i).and_return(3)
# etc ...
#
# == Expecting Messages
#
# my_mock.should_receive(:sym)
# my_mock.should_not_receive(:sym)
#
# == Expecting Arguments
#
# my_mock.should_receive(:sym).with(*args)
# my_mock.should_not_receive(:sym).with(*args)
#
# == Argument Constraints using Expression Matchers
#
# Arguments that are passed to #with are compared with actual arguments received
# using == by default. In cases in which you want to specify things about the arguments
# rather than the arguments themselves, you can use any of the Expression Matchers.
# They don't all make syntactic sense (they were primarily designed for use with
# Spec::Expectations), but you are free to create your own custom Spec::Matchers.
#
# Spec::Mocks does provide one additional Matcher method named #ducktype.
#
# In addition, Spec::Mocks adds some keyword Symbols that you can use to
# specify certain kinds of arguments:
#
# my_mock.should_receive(:sym).with(:no_args)
# my_mock.should_receive(:sym).with(:any_args)
# my_mock.should_receive(:sym).with(1, :numeric, "b") #2nd argument can any type of Numeric
# my_mock.should_receive(:sym).with(1, :boolean, "b") #2nd argument can true or false
# my_mock.should_receive(:sym).with(1, :string, "b") #2nd argument can be any String
# my_mock.should_receive(:sym).with(1, /abc/, "b") #2nd argument can be any String matching the submitted Regexp
# my_mock.should_receive(:sym).with(1, :anything, "b") #2nd argument can be anything at all
# my_mock.should_receive(:sym).with(1, ducktype(:abs, :div), "b")
# #2nd argument can be object that responds to #abs and #div
#
# == Receive Counts
#
# my_mock.should_receive(:sym).once
# my_mock.should_receive(:sym).twice
# my_mock.should_receive(:sym).exactly(n).times
# my_mock.should_receive(:sym).at_least(:once)
# my_mock.should_receive(:sym).at_least(:twice)
# my_mock.should_receive(:sym).at_least(n).times
# my_mock.should_receive(:sym).at_most(:once)
# my_mock.should_receive(:sym).at_most(:twice)
# my_mock.should_receive(:sym).at_most(n).times
# my_mock.should_receive(:sym).any_number_of_times
#
# == Ordering
#
# my_mock.should_receive(:sym).ordered
# my_mock.should_receive(:other_sym).ordered
# #This will fail if the messages are received out of order
#
# == Setting Reponses
#
# Whether you are setting a mock expectation or a simple stub, you can tell the
# object precisely how to respond:
#
# my_mock.should_receive(:sym).and_return(value)
# my_mock.should_receive(:sym).exactly(3).times.and_return(value1, value2, value3)
# # returns value1 the first time, value2 the second, etc
# my_mock.should_receive(:sym).and_return { ... } #returns value returned by the block
# my_mock.should_receive(:sym).and_raise(error)
# #error can be an instantiated object or a class
# #if it is a class, it must be instantiable with no args
# my_mock.should_receive(:sym).and_throw(:sym)
# my_mock.should_receive(:sym).and_yield([array,of,values,to,yield])
#
# Any of these responses can be applied to a stub as well, but stubs do
# not support any qualifiers about the message received (i.e. you can't specify arguments
# or receive counts):
#
# my_mock.stub!(:sym).and_return(value)
# my_mock.stub!(:sym).and_return(value1, value2, value3)
# my_mock.stub!(:sym).and_raise(error)
# my_mock.stub!(:sym).and_throw(:sym)
# my_mock.stub!(:sym).and_yield([array,of,values,to,yield])
#
# == Arbitrary Handling
#
# Once in a while you'll find that the available expectations don't solve the
# particular problem you are trying to solve. Imagine that you expect the message
# to come with an Array argument that has a specific length, but you don't care
# what is in it. You could do this:
#
# my_mock.should_receive(:sym) do |arg|
# arg.should be_an_istance_of(Array)
# arg.length.should == 7
# end
#
# Note that this would fail if the number of arguments received was different from
# the number of block arguments (in this case 1).
#
# == Combining Expectation Details
#
# Combining the message name with specific arguments, receive counts and responses
# you can get quite a bit of detail in your expectations:
#
# my_mock.should_receive(:<<).with("illegal value").once.and_raise(ArgumentError)
module Mocks
# Shortcut for creating an instance of Spec::Mocks::Mock.
def mock(name, options={})
Spec::Mocks::Mock.new(name, options)
end
# Shortcut for creating an instance of Spec::Mocks::Mock with
# predefined method stubs.
#
# == Examples
#
# stub_thing = stub("thing", :a => "A")
# stub_thing.a == "A" => true
#
# stub_person = stub("thing", :name => "Joe", :email => "joe@domain.com")
# stub_person.name => "Joe"
# stub_person.email => "joe@domain.com"
def stub(name, stubs={})
object_stub = mock(name)
stubs.each { |key, value| object_stub.stub!(key).and_return(value) }
object_stub
end
# Shortcut for creating an instance of Spec::Mocks::DuckTypeArgConstraint
def duck_type(*args)
return Spec::Mocks::DuckTypeArgConstraint.new(*args)
end
end
end

Просмотреть файл

@ -0,0 +1,132 @@
module Spec
module Mocks
class MatcherConstraint
def initialize(matcher)
@matcher = matcher
end
def matches?(value)
@matcher.matches?(value)
end
end
class LiteralArgConstraint
def initialize(literal)
@literal_value = literal
end
def matches?(value)
@literal_value == value
end
end
class RegexpArgConstraint
def initialize(regexp)
@regexp = regexp
end
def matches?(value)
return value =~ @regexp unless value.is_a?(Regexp)
value == @regexp
end
end
class AnyArgConstraint
def initialize(ignore)
end
def matches?(value)
true
end
end
class NumericArgConstraint
def initialize(ignore)
end
def matches?(value)
value.is_a?(Numeric)
end
end
class BooleanArgConstraint
def initialize(ignore)
end
def matches?(value)
return true if value.is_a?(TrueClass)
return true if value.is_a?(FalseClass)
false
end
end
class StringArgConstraint
def initialize(ignore)
end
def matches?(value)
value.is_a?(String)
end
end
class DuckTypeArgConstraint
def initialize(*methods_to_respond_do)
@methods_to_respond_do = methods_to_respond_do
end
def matches?(value)
@methods_to_respond_do.all? { |sym| value.respond_to?(sym) }
end
end
class ArgumentExpectation
attr_reader :args
@@constraint_classes = Hash.new { |hash, key| LiteralArgConstraint}
@@constraint_classes[:anything] = AnyArgConstraint
@@constraint_classes[:numeric] = NumericArgConstraint
@@constraint_classes[:boolean] = BooleanArgConstraint
@@constraint_classes[:string] = StringArgConstraint
def initialize(args)
@args = args
if [:any_args] == args then @expected_params = nil
elsif [:no_args] == args then @expected_params = []
else @expected_params = process_arg_constraints(args)
end
end
def process_arg_constraints(constraints)
constraints.collect do |constraint|
convert_constraint(constraint)
end
end
def convert_constraint(constraint)
return @@constraint_classes[constraint].new(constraint) if constraint.is_a?(Symbol)
return constraint if constraint.is_a?(DuckTypeArgConstraint)
return MatcherConstraint.new(constraint) if is_matcher?(constraint)
return RegexpArgConstraint.new(constraint) if constraint.is_a?(Regexp)
return LiteralArgConstraint.new(constraint)
end
def is_matcher?(obj)
return obj.respond_to?(:matches?) && obj.respond_to?(:description)
end
def check_args(args)
return true if @expected_params.nil?
return true if @expected_params == args
return constraints_match?(args)
end
def constraints_match?(args)
return false if args.length != @expected_params.length
@expected_params.each_index { |i| return false unless @expected_params[i].matches?(args[i]) }
return true
end
end
end
end

Просмотреть файл

@ -0,0 +1,85 @@
module Spec
module Mocks
class ErrorGenerator
attr_writer :opts
def initialize(target, name)
@target = target
@name = name
end
def opts
@opts ||= {}
end
def raise_unexpected_message_error(sym, *args)
__raise "#{intro} received unexpected message :#{sym}#{arg_message(*args)}"
end
def raise_unexpected_message_args_error(expectation, *args)
#this is either :no_args or an Array
expected_args = (expectation.expected_args == :no_args ? "(no args)" : format_args(*expectation.expected_args))
actual_args = args.empty? ? "(no args)" : format_args(*args)
__raise "#{intro} expected #{expectation.sym.inspect} with #{expected_args} but received it with #{actual_args}"
end
def raise_expectation_error(sym, expected_received_count, actual_received_count, *args)
__raise "#{intro} expected :#{sym}#{arg_message(*args)} #{count_message(expected_received_count)}, but received it #{count_message(actual_received_count)}"
end
def raise_out_of_order_error(sym)
__raise "#{intro} received :#{sym} out of order"
end
def raise_block_failed_error(sym, detail)
__raise "#{intro} received :#{sym} but passed block failed with: #{detail}"
end
def raise_missing_block_error(args_to_yield)
__raise "#{intro} asked to yield |#{arg_list(*args_to_yield)}| but no block was passed"
end
def raise_wrong_arity_error(args_to_yield, arity)
__raise "#{intro} yielded |#{arg_list(*args_to_yield)}| to block with arity of #{arity}"
end
private
def intro
@name ? "Mock '#{@name}'" : @target.to_s
end
def __raise(message)
message = opts[:message] unless opts[:message].nil?
Kernel::raise(Spec::Mocks::MockExpectationError, message)
end
def arg_message(*args)
" with " + format_args(*args)
end
def format_args(*args)
return "(no args)" if args.empty? || args == [:no_args]
return "(any args)" if args == [:any_args]
"(" + arg_list(*args) + ")"
end
def arg_list(*args)
args.collect do |arg|
arg.respond_to?(:description) ? arg.description : arg.inspect
end.join(", ")
end
def count_message(count)
return "at least #{pretty_print(count.abs)}" if count < 0
return pretty_print(count)
end
def pretty_print(count)
return "once" if count == 1
return "twice" if count == 2
return "#{count} times"
end
end
end
end

Просмотреть файл

@ -0,0 +1,10 @@
module Spec
module Mocks
class MockExpectationError < StandardError
end
class AmbiguousReturnError < StandardError
end
end
end

Просмотреть файл

@ -0,0 +1,3 @@
class Object
include Spec::Mocks::Methods
end

Просмотреть файл

@ -0,0 +1,231 @@
module Spec
module Mocks
class BaseExpectation
attr_reader :sym
def initialize(error_generator, expectation_ordering, expected_from, sym, method_block, expected_received_count=1, opts={})
@error_generator = error_generator
@error_generator.opts = opts
@expected_from = expected_from
@sym = sym
@method_block = method_block
@return_block = lambda {}
@received_count = 0
@expected_received_count = expected_received_count
@args_expectation = ArgumentExpectation.new([:any_args])
@consecutive = false
@exception_to_raise = nil
@symbol_to_throw = nil
@order_group = expectation_ordering
@at_least = nil
@at_most = nil
@args_to_yield = nil
end
def expected_args
@args_expectation.args
end
def and_return(*values, &return_block)
Kernel::raise AmbiguousReturnError unless @method_block.nil?
if values.size == 0
value = nil
elsif values.size == 1
value = values[0]
else
value = values
@consecutive = true
@expected_received_count = values.size if @expected_received_count != :any &&
@expected_received_count < values.size
end
@return_block = block_given? ? return_block : lambda { value }
end
def and_raise(exception=Exception)
@exception_to_raise = exception
end
def and_throw(symbol)
@symbol_to_throw = symbol
end
def and_yield(*args)
@args_to_yield = args
end
def matches(sym, args)
@sym == sym and @args_expectation.check_args(args)
end
def invoke(args, block)
@order_group.handle_order_constraint self
begin
if @exception_to_raise.class == Class
@exception_instance_to_raise = @exception_to_raise.new
else
@exception_instance_to_raise = @exception_to_raise
end
Kernel::raise @exception_to_raise unless @exception_to_raise.nil?
Kernel::throw @symbol_to_throw unless @symbol_to_throw.nil?
if !@method_block.nil?
return invoke_method_block(args)
elsif !@args_to_yield.nil?
return invoke_with_yield(block)
elsif @consecutive
return invoke_consecutive_return_block(args, block)
else
return invoke_return_block(args, block)
end
ensure
@received_count += 1
end
end
protected
def invoke_method_block(args)
begin
@method_block.call(*args)
rescue => detail
@error_generator.raise_block_failed_error @sym, detail.message
end
end
def invoke_with_yield(block)
if block.nil?
@error_generator.raise_missing_block_error @args_to_yield
end
if block.arity > -1 && @args_to_yield.length != block.arity
@error_generator.raise_wrong_arity_error @args_to_yield, block.arity
end
block.call(*@args_to_yield)
end
def invoke_consecutive_return_block(args, block)
args << block unless block.nil?
value = @return_block.call(*args)
index = [@received_count, value.size-1].min
value[index]
end
def invoke_return_block(args, block)
args << block unless block.nil?
value = @return_block.call(*args)
value
end
end
class MessageExpectation < BaseExpectation
def matches_name_but_not_args(sym, args)
@sym == sym and not @args_expectation.check_args(args)
end
def verify_messages_received
return if @expected_received_count == :any
return if (@at_least) && (@received_count >= @expected_received_count)
return if (@at_most) && (@received_count <= @expected_received_count)
return if @expected_received_count == @received_count
begin
@error_generator.raise_expectation_error(@sym, @expected_received_count, @received_count, *@args_expectation.args)
rescue => error
error.backtrace.insert(0, @expected_from)
Kernel::raise error
end
end
def with(*args, &block)
@method_block = block if block
@args_expectation = ArgumentExpectation.new(args)
self
end
def exactly(n)
set_expected_received_count :exactly, n
self
end
def at_least(n)
set_expected_received_count :at_least, n
self
end
def at_most(n)
set_expected_received_count :at_most, n
self
end
def times(&block)
@method_block = block if block
self
end
def any_number_of_times(&block)
@method_block = block if block
@expected_received_count = :any
self
end
def never
@expected_received_count = 0
self
end
def once(&block)
@method_block = block if block
@expected_received_count = 1
self
end
def twice(&block)
@method_block = block if block
@expected_received_count = 2
self
end
def ordered(&block)
@method_block = block if block
@order_group.register(self)
@ordered = true
self
end
def negative_expectation_for?(sym)
return false
end
protected
def set_expected_received_count(relativity, n)
@at_least = (relativity == :at_least)
@at_most = (relativity == :at_most)
@expected_received_count = 1 if n == :once
@expected_received_count = 2 if n == :twice
@expected_received_count = n if n.kind_of? Numeric
end
end
class NegativeMessageExpectation < MessageExpectation
def initialize(message, expectation_ordering, expected_from, sym, method_block)
super(message, expectation_ordering, expected_from, sym, method_block, 0)
end
def negative_expectation_for?(sym)
return @sym == sym
end
end
class MethodStub < BaseExpectation
def initialize(message, expectation_ordering, expected_from, sym, method_block)
super(message, expectation_ordering, expected_from, sym, method_block, 0)
@expected_received_count = :any
end
end
end
end

Просмотреть файл

@ -0,0 +1,40 @@
module Spec
module Mocks
module Methods
def should_receive(sym, opts={}, &block)
__mock_handler.add_message_expectation(opts[:expected_from] || caller(1)[0], sym, opts, &block)
end
def should_not_receive(sym, &block)
__mock_handler.add_negative_message_expectation(caller(1)[0], sym, &block)
end
def stub!(sym)
__mock_handler.add_stub(caller(1)[0], sym)
end
def received_message?(sym, *args, &block) #:nodoc:
__mock_handler.received_message?(sym, *args, &block)
end
def __verify #:nodoc:
__mock_handler.verify
end
def __reset_mock #:nodoc:
__mock_handler.reset
end
def method_missing(sym, *args, &block) #:nodoc:
__mock_handler.instance_eval {@messages_received << [sym, args, block]}
super(sym, *args, &block)
end
private
def __mock_handler
@mock_handler ||= MockHandler.new(self, @name, @options)
end
end
end
end

Просмотреть файл

@ -0,0 +1,26 @@
module Spec
module Mocks
class Mock
include Methods
# Creates a new mock with a +name+ (that will be used in error messages only)
# == Options:
# * <tt>:null_object</tt> - if true, the mock object acts as a forgiving null object allowing any message to be sent to it.
def initialize(name, options={})
@name = name
@options = options
end
def method_missing(sym, *args, &block)
__mock_handler.instance_eval {@messages_received << [sym, args, block]}
begin
return self if __mock_handler.null_object?
super(sym, *args, &block)
rescue NoMethodError
__mock_handler.raise_unexpected_message_error sym, *args
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,166 @@
module Spec
module Mocks
class MockHandler
DEFAULT_OPTIONS = {
:null_object => false,
:auto_verify => true
}
def initialize(target, name, options={})
@target = target
@name = name
@error_generator = ErrorGenerator.new target, name
@expectation_ordering = OrderGroup.new @error_generator
@expectations = []
@messages_received = []
@stubs = []
@proxied_methods = []
@options = options ? DEFAULT_OPTIONS.dup.merge(options) : DEFAULT_OPTIONS
end
def null_object?
@options[:null_object]
end
def add_message_expectation(expected_from, sym, opts={}, &block)
__add expected_from, sym, block
@expectations << MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil, 1, opts)
@expectations.last
end
def add_negative_message_expectation(expected_from, sym, &block)
__add expected_from, sym, block
@expectations << NegativeMessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil)
@expectations.last
end
def add_stub(expected_from, sym)
__add expected_from, sym, nil
@stubs.unshift MethodStub.new(@error_generator, @expectation_ordering, expected_from, sym, nil)
@stubs.first
end
def verify #:nodoc:
begin
verify_expectations
ensure
reset
end
end
def reset
clear_expectations
clear_stubs
reset_proxied_methods
clear_proxied_methods
end
def received_message?(sym, *args, &block)
return true if @messages_received.find {|array| array == [sym, args, block]}
return false
end
def has_negative_expectation?(sym)
@expectations.detect {|expectation| expectation.negative_expectation_for?(sym)}
end
def message_received(sym, *args, &block)
if expectation = find_matching_expectation(sym, *args)
expectation.invoke(args, block)
elsif stub = find_matching_method_stub(sym)
stub.invoke([], block)
elsif expectation = find_almost_matching_expectation(sym, *args)
raise_unexpected_message_args_error(expectation, *args) unless has_negative_expectation?(sym) unless null_object?
else
@target.send :method_missing, sym, *args, &block
end
end
def raise_unexpected_message_args_error(expectation, *args)
@error_generator.raise_unexpected_message_args_error expectation, *args
end
def raise_unexpected_message_error(sym, *args)
@error_generator.raise_unexpected_message_error sym, *args
end
private
def __add(expected_from, sym, block)
# TODO - this is the only reference in the 'spec/mocks' to the Runner
current_spec = Runner::Specification.current
current_spec.after_teardown {verify} if current_spec && @options[:auto_verify]
define_expected_method(sym)
end
def define_expected_method(sym)
if target_responds_to?(sym) && !@proxied_methods.include?(sym)
@proxied_methods << sym
metaclass.__send__(:alias_method, munge(sym), sym)
end
metaclass_eval(<<-EOF, __FILE__, __LINE__)
def #{sym}(*args, &block)
__mock_handler.message_received :#{sym}, *args, &block
end
EOF
end
def target_responds_to?(sym)
return @target.send(munge(:respond_to?),sym) if @already_proxied_respond_to
return @already_proxied_respond_to = true if sym == :respond_to?
return @target.respond_to?(sym)
end
def munge(sym)
"proxied_by_rspec__#{sym.to_s}".to_sym
end
def clear_expectations
@expectations.clear
end
def clear_stubs
@stubs.clear
end
def clear_proxied_methods
@proxied_methods.clear
end
def metaclass_eval(str, filename, lineno)
metaclass.class_eval(str, filename, lineno)
end
def metaclass
(class << @target; self; end)
end
def verify_expectations
@expectations.each do |expectation|
expectation.verify_messages_received
end
end
def reset_proxied_methods
@proxied_methods.each do |sym|
metaclass.__send__(:alias_method, sym, munge(sym))
metaclass.__send__(:undef_method, munge(sym))
end
end
def find_matching_expectation(sym, *args)
@expectations.find {|expectation| expectation.matches(sym, args)}
end
def find_almost_matching_expectation(sym, *args)
@expectations.find {|expectation| expectation.matches_name_but_not_args(sym, args)}
end
def find_matching_method_stub(sym)
@stubs.find {|stub| stub.matches(sym, [])}
end
end
end
end

Просмотреть файл

@ -0,0 +1,29 @@
module Spec
module Mocks
class OrderGroup
def initialize error_generator
@error_generator = error_generator
@ordering = Array.new
end
def register(expectation)
@ordering << expectation
end
def ready_for?(expectation)
return @ordering.first == expectation
end
def consume
@ordering.shift
end
def handle_order_constraint expectation
return unless @ordering.include? expectation
return consume if ready_for?(expectation)
@error_generator.raise_out_of_order_error expectation.sym
end
end
end
end

Просмотреть файл

@ -0,0 +1,173 @@
#!/usr/bin/env ruby
# Define a task library for running RSpec contexts.
require 'rake'
require 'rake/tasklib'
module Spec
module Rake
# A Rake task that runs a set of RSpec contexts.
#
# Example:
#
# Spec::Rake::SpecTask.new do |t|
# t.warning = true
# t.rcov = true
# end
#
# This will create a task that can be run with:
#
# rake spec
#
class SpecTask < ::Rake::TaskLib
# Name of spec task. (default is :spec)
attr_accessor :name
# Array of directories to be added to $LOAD_PATH before running the
# specs. Defaults to ['<the absolute path to RSpec's lib directory>']
attr_accessor :libs
# If true, requests that the specs be run with the warning flag set.
# E.g. warning=true implies "ruby -w" used to run the specs. Defaults to false.
attr_accessor :warning
# Glob pattern to match spec files. (default is 'spec/**/*_spec.rb')
attr_accessor :pattern
# Array of commandline options to pass to RSpec. Defaults to [].
attr_accessor :spec_opts
# Where RSpec's output is written. Defaults to STDOUT.
attr_accessor :out
# Whether or not to use RCov (default is false)
# See http://eigenclass.org/hiki.rb?rcov
attr_accessor :rcov
# Array of commandline options to pass to RCov. Defaults to ['--exclude', 'lib\/spec,bin\/spec'].
# Ignored if rcov=false
attr_accessor :rcov_opts
# Directory where the RCov report is written. Defaults to "coverage"
# Ignored if rcov=false
attr_accessor :rcov_dir
# Array of commandline options to pass to ruby. Defaults to [].
attr_accessor :ruby_opts
# Whether or not to fail Rake when an error occurs (typically when specs fail).
# Defaults to true.
attr_accessor :fail_on_error
# A message to print to stdout when there are failures.
attr_accessor :failure_message
# Explicitly define the list of spec files to be included in a
# spec. +list+ is expected to be an array of file names (a
# FileList is acceptable). If both +pattern+ and +spec_files+ are
# used, then the list of spec files is the union of the two.
def spec_files=(list)
@spec_files = list
end
# Create a specing task.
def initialize(name=:spec)
@name = name
@libs = [File.expand_path(File.dirname(__FILE__) + '/../../../lib')]
@pattern = nil
@spec_files = nil
@spec_opts = []
@warning = false
@ruby_opts = []
@out = nil
@fail_on_error = true
@rcov = false
@rcov_opts = ['--exclude', 'lib\/spec,bin\/spec,config\/boot.rb']
@rcov_dir = "coverage"
yield self if block_given?
@pattern = 'spec/**/*_spec.rb' if @pattern.nil? && @spec_files.nil?
define
end
def define
spec_script = File.expand_path(File.dirname(__FILE__) + '/../../../bin/spec')
lib_path = @libs.join(File::PATH_SEPARATOR)
actual_name = Hash === name ? name.keys.first : name
unless ::Rake.application.last_comment
desc "Run RSpec for #{actual_name}" + (@rcov ? " using RCov" : "")
end
task @name do
RakeFileUtils.verbose(@verbose) do
ruby_opts = @ruby_opts.clone
ruby_opts.push( "-I\"#{lib_path}\"" )
ruby_opts.push( "-S rcov" ) if @rcov
ruby_opts.push( "-w" ) if @warning
redirect = @out.nil? ? "" : " > \"#{@out}\""
unless spec_file_list.empty?
# ruby [ruby_opts] -Ilib -S rcov [rcov_opts] bin/spec -- [spec_opts] examples
# or
# ruby [ruby_opts] -Ilib bin/spec [spec_opts] examples
begin
ruby(
ruby_opts.join(" ") + " " +
rcov_option_list +
(@rcov ? %[ -o "#{@rcov_dir}" ] : "") +
'"' + spec_script + '"' + " " +
(@rcov ? "-- " : "") +
spec_file_list.collect { |fn| %["#{fn}"] }.join(' ') + " " +
spec_option_list + " " +
redirect
)
rescue => e
puts @failure_message if @failure_message
raise e if @fail_on_error
end
end
end
end
if @rcov
desc "Remove rcov products for #{actual_name}"
task paste("clobber_", actual_name) do
rm_r @rcov_dir rescue nil
end
clobber_task = paste("clobber_", actual_name)
task :clobber => [clobber_task]
task actual_name => clobber_task
end
self
end
def rcov_option_list # :nodoc:
return "" unless @rcov
ENV['RCOVOPTS'] || @rcov_opts.join(" ") || ""
end
def spec_option_list # :nodoc:
ENV['RSPECOPTS'] || @spec_opts.join(" ") || ""
end
def spec_file_list # :nodoc:
if ENV['SPEC']
FileList[ ENV['SPEC'] ]
else
result = []
result += @spec_files.to_a if @spec_files
result += FileList[ @pattern ].to_a if @pattern
FileList[result]
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,47 @@
module RCov
# A task that can verify that the RCov coverage doesn't
# drop below a certain threshold. It should be run after
# running Spec::Rake::SpecTask.
class VerifyTask < Rake::TaskLib
# Name of the task. Defaults to :verify_rcov
attr_accessor :name
# Path to the index.html file generated by RCov, which
# is the file containing the total coverage.
# Defaults to 'coverage/index.html'
attr_accessor :index_html
# Whether or not to output details. Defaults to true.
attr_accessor :verbose
# The threshold value (in percent) for coverage. If the
# actual coverage is not equal to this value, the task will raise an
# exception.
attr_accessor :threshold
def initialize(name=:verify_rcov)
@name = name
@index_html = 'coverage/index.html'
@verbose = true
yield self if block_given?
raise "Threshold must be set" if @threshold.nil?
define
end
def define
desc "Verify that rcov coverage is at least #{threshold}%"
task @name do
total_coverage = nil
File.open(index_html).each_line do |line|
if line =~ /<tt.*>(\d+\.\d+)%<\/tt>&nbsp;<\/td>/
total_coverage = eval($1)
break
end
end
puts "Coverage: #{total_coverage}% (threshold: #{threshold}%)" if verbose
raise "Coverage must be at least #{threshold}% but was #{total_coverage}%" if total_coverage < threshold
raise "Coverage has increased above the threshold of #{threshold}% to #{total_coverage}%. You should update your threshold value." if total_coverage > threshold
end
end
end
end

132
test/lib/spec/runner.rb Normal file
Просмотреть файл

@ -0,0 +1,132 @@
require 'spec/runner/formatter'
require 'spec/runner/context'
require 'spec/runner/context_eval'
require 'spec/runner/specification'
require 'spec/runner/execution_context'
require 'spec/runner/context_runner'
require 'spec/runner/option_parser'
require 'spec/runner/command_line'
require 'spec/runner/drb_command_line'
require 'spec/runner/backtrace_tweaker'
require 'spec/runner/reporter'
require 'spec/runner/spec_matcher'
require 'spec/runner/extensions/object'
require 'spec/runner/extensions/kernel'
require 'spec/runner/spec_should_raise_handler'
require 'spec/runner/spec_parser'
module Spec
# == Contexts and Specifications
#
# Rather than expressing examples in classes, RSpec uses a custom domain specific language to express
# examples using contexts and specifications.
#
# A context is the equivalent of a fixture in xUnit-speak. It is a metaphor for the context
# in which you will run your executable example - a set of known objects in a known starting state.
#
# context "A new account" do
#
# setup do
# @account = Account.new
# end
#
# specify "should have a balance of $0" do
# @account.balance.should_eql Money.new(0, :dollars)
# end
#
# end
#
# We use the setup block to set up the context (given), and then the specify method to
# hold the example code that expresses the event (when) and the expected outcome (then).
#
# == Helper Methods
#
# A primary goal of RSpec is to keep the examples clear. We therefore prefer
# less indirection than you might see in xUnit examples and in well factored, DRY production code. We feel
# that duplication is OK if removing it makes it harder to understand an example without
# having to look elsewhere to understand its context.
#
# That said, RSpec does support some level of encapsulating common code in helper
# methods that can exist within a context or within an included module.
#
# == Setup and Teardown
#
# You can use setup, teardown, context_setup and context_teardown within a context:
#
# context "..." do
# context_setup do
# ...
# end
#
# setup do
# ...
# end
#
# specify "number one" do
# ...
# end
#
# specify "number two" do
# ...
# end
#
# teardown do
# ...
# end
#
# context_teardown do
# ...
# end
#
# end
#
# The <tt>setup</tt> block will run before each of the specs, once for each spec. Likewise,
# the <tt>teardown</tt> block will run after each of the specs.
#
# It is also possible to specify a <tt>context_setup</tt> and <tt>context_teardown</tt>
# block that will run only once for each context, respectively before the first <code>setup</code>
# and after the last <code>teardown</code>. The use of these is generally discouraged, because it
# introduces dependencies between the specs. Still, it might prove useful for very expensive operations
# if you know what you are doing.
#
# == Local helper methods
#
# You can include local helper methods by simply expressing them within a context:
#
# context "..." do
#
# specify "..." do
# helper_method
# end
#
# def helper_method
# ...
# end
#
# end
#
# == Included helper methods
#
# You can include helper methods in multiple contexts by expressing them within
# a module, and then including that module in your context:
#
# module AccountExampleHelperMethods
# def helper_method
# ...
# end
# end
#
# context "A new account" do
# include AccountExampleHelperMethods
# setup do
# @account = Account.new
# end
#
# specify "should have a balance of $0" do
# helper_method
# @account.balance.should eql(Money.new(0, :dollars))
# end
# end
module Runner
end
end

Просмотреть файл

@ -0,0 +1,55 @@
module Spec
module Runner
class BacktraceTweaker
def clean_up_double_slashes(line)
line.gsub!('//','/')
end
end
class NoisyBacktraceTweaker < BacktraceTweaker
def tweak_backtrace(error, spec_name)
return if error.backtrace.nil?
error.backtrace.each do |line|
clean_up_double_slashes(line)
end
end
end
# Tweaks raised Exceptions to mask noisy (unneeded) parts of the backtrace
class QuietBacktraceTweaker < BacktraceTweaker
unless defined?(IGNORE_PATTERNS)
root_dir = File.expand_path(File.join(__FILE__, '..', '..', '..', '..'))
spec_files = Dir["#{root_dir}/lib/spec/*"].map do |path|
subpath = path[root_dir.length..-1]
/#{subpath}/
end
IGNORE_PATTERNS = spec_files + [
/\/lib\/ruby\//,
/bin\/spec:/,
/bin\/rcov:/,
/lib\/rspec_on_rails/,
/vendor\/rails/,
# TextMate's Ruby and RSpec plugins
/Ruby\.tmbundle\/Support\/tmruby.rb:/,
/RSpec\.tmbundle\/Support\/lib/,
/temp_textmate\./
]
end
def tweak_backtrace(error, spec_name)
return if error.backtrace.nil?
error.backtrace.collect! do |line|
clean_up_double_slashes(line)
IGNORE_PATTERNS.each do |ignore|
if line =~ ignore
line = nil
break
end
end
line
end
error.backtrace.compact!
end
end
end
end

Просмотреть файл

@ -0,0 +1,34 @@
require 'spec/runner/option_parser'
module Spec
module Runner
# Facade to run specs without having to fork a new ruby process (using `spec ...`)
class CommandLine
# Runs specs. +argv+ is the commandline args as per the spec commandline API, +err+
# and +out+ are the streams output will be written to. +exit+ tells whether or
# not a system exit should be called after the specs are run and
# +warn_if_no_files+ tells whether or not a warning (the help message)
# should be printed to +err+ in case no files are specified.
def self.run(argv, err, out, exit=true, warn_if_no_files=true)
old_context_runner = defined?($context_runner) ? $context_runner : nil
$context_runner = OptionParser.new.create_context_runner(argv, err, out, warn_if_no_files)
return if $context_runner.nil? # This is the case if we use --drb
# If ARGV is a glob, it will actually each over each one of the matching files.
argv.each do |file_or_dir|
if File.directory?(file_or_dir)
Dir["#{file_or_dir}/**/*.rb"].each do |file|
load file
end
elsif File.file?(file_or_dir)
load file_or_dir
else
raise "File or directory not found: #{file_or_dir}"
end
end
$context_runner.run(exit)
$context_runner = old_context_runner
end
end
end
end

Просмотреть файл

@ -0,0 +1,154 @@
module Spec
module Runner
class ContextEvalModule < Module
end
class Context
module InstanceMethods
def initialize(description, &context_block)
@description = description
@context_eval_module = ContextEvalModule.new
@context_eval_module.extend ContextEval::ModuleMethods
@context_eval_module.include ContextEval::InstanceMethods
before_context_eval
@context_eval_module.class_eval(&context_block)
end
def before_context_eval
end
def inherit_context_eval_module_from(klass)
@context_eval_module.inherit klass
end
alias :inherit :inherit_context_eval_module_from
def include(mod)
@context_eval_module.include(mod)
end
def run(reporter, dry_run=false)
reporter.add_context(@description)
prepare_execution_context_class
errors = run_context_setup(reporter, dry_run)
specifications.each do |specification|
specification_execution_context = execution_context(specification)
specification_execution_context.copy_instance_variables_from(@once_only_execution_context_instance, []) unless context_setup_block.nil?
specification.run(reporter, setup_block, teardown_block, dry_run, specification_execution_context)
end unless errors.length > 0
run_context_teardown(reporter, dry_run)
end
def number_of_specs
specifications.length
end
def matches?(full_description)
matcher ||= SpecMatcher.new(@description)
specifications.each do |spec|
return true if spec.matches?(matcher, full_description)
end
return false
end
def run_single_spec(full_description)
return if @description == full_description
matcher = SpecMatcher.new(@description)
specifications.reject! do |spec|
!spec.matches?(matcher, full_description)
end
end
def methods
my_methods = super
my_methods |= @context_eval_module.methods
my_methods
end
protected
def method_missing(*args)
@context_eval_module.method_missing(*args)
end
def context_setup_block
@context_eval_module.send :context_setup_block
end
def context_teardown_block
@context_eval_module.send :context_teardown_block
end
def specifications
@context_eval_module.send :specifications
end
def setup_block
@context_eval_module.send :setup_block
end
def teardown_block
@context_eval_module.send :teardown_block
end
def prepare_execution_context_class
weave_in_context_modules
execution_context_class
end
def weave_in_context_modules
mods = context_modules
context_eval_module = @context_eval_module
execution_context_class.class_eval do
include context_eval_module
mods.each do |mod|
include mod
end
end
end
def context_modules
@context_eval_module.send :context_modules
end
def execution_context_class
@context_eval_module.send :execution_context_class
end
def execution_context specification
execution_context_class.new(specification)
end
def run_context_setup(reporter, dry_run)
errors = []
unless dry_run
begin
@once_only_execution_context_instance = execution_context(nil)
@once_only_execution_context_instance.instance_eval(&context_setup_block)
rescue => e
errors << e
location = "context_setup"
reporter.spec_finished(location, e, location) if reporter
end
end
errors
end
def run_context_teardown(reporter, dry_run)
unless dry_run
begin
@once_only_execution_context_instance ||= execution_context(nil)
@once_only_execution_context_instance.instance_eval(&context_teardown_block)
rescue => e
location = "context_teardown"
reporter.spec_finished(location, e, location) if reporter
end
end
end
end
include InstanceMethods
end
end
end

Просмотреть файл

@ -0,0 +1,142 @@
module Spec
module Runner
module ContextEval
module ModuleMethods
def inherit(klass)
@context_superclass = klass
derive_execution_context_class_from_context_superclass
end
def include(mod)
context_modules << mod
mod.send :included, self
end
def context_setup(&block)
context_setup_parts << block
end
def context_teardown(&block)
context_teardown_parts << block
end
def setup(&block)
setup_parts << block
end
def teardown(&block)
teardown_parts << block
end
def specify(spec_name=:__generate_description, opts={}, &block)
specifications << Specification.new(spec_name, opts, &block)
end
def methods
my_methods = super
my_methods |= context_superclass.methods
my_methods
end
protected
def method_missing(method_name, *args)
if context_superclass.respond_to?(method_name)
return execution_context_class.send(method_name, *args)
end
super
end
private
def context_setup_block
parts = context_setup_parts.dup
add_context_superclass_method(:context_setup, parts)
create_block_from_parts(parts)
end
def context_teardown_block
parts = context_teardown_parts.dup
add_context_superclass_method(:context_teardown, parts)
create_block_from_parts(parts)
end
def setup_block
parts = setup_parts.dup
add_context_superclass_method(:setup, parts)
create_block_from_parts(parts)
end
def teardown_block
parts = teardown_parts.dup
add_context_superclass_method(:teardown, parts)
create_block_from_parts(parts)
end
def execution_context_class
@execution_context_class ||= derive_execution_context_class_from_context_superclass
end
def derive_execution_context_class_from_context_superclass
@execution_context_class = Class.new(context_superclass)
@execution_context_class.class_eval do
include ::Spec::Runner::ExecutionContext::InstanceMethods
end
end
def context_superclass
@context_superclass ||= Object
end
def context_modules
@context_modules ||= [Spec::Matchers, Spec::Mocks]
end
def specifications
@specifications ||= []
end
def context_setup_parts
@context_setup_parts ||= []
end
def context_teardown_parts
@context_teardown_parts ||= []
end
def setup_parts
@setup_parts ||= []
end
def teardown_parts
@teardown_parts ||= []
end
def add_context_superclass_method sym, parts
superclass_method = begin
context_superclass.instance_method(sym)
rescue
nil
end
parts.unshift superclass_method if superclass_method
end
def create_block_from_parts(parts)
proc do
parts.each do |part|
if part.is_a?(UnboundMethod)
part.bind(self).call
else
instance_eval(&part)
end
end
end
end
end
module InstanceMethods
end
end
end
end

Просмотреть файл

@ -0,0 +1,55 @@
module Spec
module Runner
class ContextRunner
def initialize(options)
@contexts = []
@options = options
end
def add_context(context)
return unless spec_description.nil? || context.matches?(spec_description)
context.run_single_spec(spec_description) if context.matches?(spec_description)
@contexts << context
end
# Runs all contexts and returns the number of failures.
def run(exit_when_done)
@options.reporter.start(number_of_specs)
begin
@contexts.each do |context|
context.run(@options.reporter, @options.dry_run)
end
rescue Interrupt
ensure
@options.reporter.end
end
failure_count = @options.reporter.dump
if(failure_count == 0 && !@options.heckle_runner.nil?)
heckle_runner = @options.heckle_runner
@options.heckle_runner = nil
context_runner = self.class.new(@options)
context_runner.instance_variable_set(:@contexts, @contexts)
heckle_runner.heckle_with(context_runner)
end
if(exit_when_done)
exit_code = (failure_count == 0) ? 0 : 1
exit(exit_code)
end
failure_count
end
def number_of_specs
@contexts.inject(0) {|sum, context| sum + context.number_of_specs}
end
private
def spec_description
@options.spec_name
end
end
end
end

Просмотреть файл

@ -0,0 +1,21 @@
require "drb/drb"
module Spec
module Runner
# Facade to run specs by connecting to a DRB server
class DrbCommandLine
# Runs specs on a DRB server. Note that this API is similar to that of
# CommandLine - making it possible for clients to use both interchangeably.
def self.run(argv, stderr, stdout, exit=true, warn_if_no_files=true)
begin
DRb.start_service
rails_spec_server = DRbObject.new_with_uri("druby://localhost:8989")
rails_spec_server.run(argv, stderr, stdout)
rescue DRb::DRbConnError
stderr.puts "No server is running"
exit 1 if exit
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,17 @@
module Spec
module Runner
class ExecutionContext
module InstanceMethods
def initialize(*args) #:nodoc:
#necessary for RSpec's own specs
end
def violated(message="")
raise Spec::Expectations::ExpectationNotMetError.new(message)
end
end
include InstanceMethods
end
end
end

Просмотреть файл

@ -0,0 +1,17 @@
module Kernel
def context(name, &block)
context = Spec::Runner::Context.new(name, &block)
context_runner.add_context(context)
end
private
def context_runner
# TODO: Figure out a better way to get this considered "covered" and keep this statement on multiple lines
unless $context_runner; \
$context_runner = ::Spec::Runner::OptionParser.new.create_context_runner(ARGV.dup, STDERR, STDOUT, false); \
at_exit { $context_runner.run(false) }; \
end
$context_runner
end
end

Просмотреть файл

@ -0,0 +1,32 @@
# The following copyright applies to Object#copy_instance_variables_from,
# which we borrowed from active_support.
#
# Copyright (c) 2004 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class Object
# From active_support
def copy_instance_variables_from(object, exclude = []) # :nodoc:
exclude += object.protected_instance_variables if object.respond_to? :protected_instance_variables
instance_variables = object.instance_variables - exclude.map { |name| name.to_s }
instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) }
end
end

Просмотреть файл

@ -0,0 +1,5 @@
require 'spec/runner/formatter/base_text_formatter'
require 'spec/runner/formatter/progress_bar_formatter'
require 'spec/runner/formatter/rdoc_formatter'
require 'spec/runner/formatter/specdoc_formatter'
require 'spec/runner/formatter/html_formatter'

Просмотреть файл

@ -0,0 +1,118 @@
module Spec
module Runner
module Formatter
# Baseclass for text-based formatters. Can in fact be used for
# non-text based ones too - just ignore the +output+ constructor
# argument.
class BaseTextFormatter
def initialize(output, dry_run=false, colour=false)
@output = output
@dry_run = dry_run
@colour = colour
begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end
end
# This method is invoked before any specs are run, right after
# they have all been collected. This can be useful for special
# formatters that need to provide progress on feedback (graphical ones)
#
# This method will only be invoked once, and the next one to be invoked
# is #add_context
def start(spec_count)
end
# This method is invoked at the beginning of the execution of each context.
# +name+ is the name of the context and +first+ is true if it is the
# first context - otherwise it's false.
#
# The next method to be invoked after this is #spec_started
def add_context(name, first)
end
# This method is invoked right before a spec is executed.
# The next method to be invoked after this one is one of #spec_failed
# or #spec_passed.
def spec_started(name)
end
# This method is invoked when a spec fails, i.e. an exception occurred
# inside it (such as a failed should or other exception). +name+ is the name
# of the specification. +counter+ is the sequence number of the failure
# (starting at 1) and +failure+ is the associated Failure object.
def spec_failed(name, counter, failure)
end
# This method is invoked when a spec passes. +name+ is the name of the
# specification.
def spec_passed(name)
end
# This method is invoked after all of the specs have executed. The next method
# to be invoked after this one is #dump_failure (once for each failed spec),
def start_dump
end
# Dumps detailed information about a spec failure.
# This method is invoked for each failed spec after all specs have run. +counter+ is the sequence number
# of the associated spec. +failure+ is a Failure object, which contains detailed
# information about the failure.
def dump_failure(counter, failure)
@output.puts
@output.puts "#{counter.to_s})"
if(failure.expectation_not_met?)
@output.puts red(failure.header)
@output.puts red(failure.exception.message)
else
@output.puts magenta(failure.header)
@output.puts magenta(failure.exception.message)
end
@output.puts format_backtrace(failure.exception.backtrace)
STDOUT.flush
end
# This method is invoked at the very end.
def dump_summary(duration, spec_count, failure_count)
return if @dry_run
@output.puts
@output.puts "Finished in #{duration} seconds"
@output.puts
summary = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
if failure_count == 0
@output.puts green(summary)
else
@output.puts red(summary)
end
end
def format_backtrace(backtrace)
return "" if backtrace.nil?
backtrace.map { |line| backtrace_line(line) }.join("\n")
end
protected
def backtrace_line(line)
line.sub(/\A([^:]+:\d+)$/, '\\1:')
end
def colour(text, colour_code)
return text unless @colour && output_to_tty?
"#{colour_code}#{text}\e[0m"
end
def output_to_tty?
begin
@output == Kernel || @output.tty?
rescue NoMethodError
false
end
end
def red(text); colour(text, "\e[31m"); end
def green(text); colour(text, "\e[32m"); end
def magenta(text); colour(text, "\e[35m"); end
end
end
end
end

Просмотреть файл

@ -0,0 +1,219 @@
module Spec
module Runner
module Formatter
class HtmlFormatter < BaseTextFormatter
attr_reader :current_spec_number, :current_context_number
def initialize(output, dry_run=false, colour=false)
super
@current_spec_number = 0
@current_context_number = 0
end
def start(spec_count)
@spec_count = spec_count
@output.puts HEADER_1
@output.puts extra_header_content unless extra_header_content.nil?
@output.puts HEADER_2
STDOUT.flush
end
def add_context(name, first)
@current_context_number += 1
unless first
@output.puts " </dl>"
@output.puts "</div>"
end
@output.puts "<div class=\"context\">"
@output.puts " <dl>"
@output.puts " <dt id=\"context_#{@current_context_number}\">#{name}</dt>"
STDOUT.flush
end
def start_dump
@output.puts " </dl>"
@output.puts "</div>"
STDOUT.flush
end
def spec_started(name)
@current_spec_number += 1
STDOUT.flush
end
def spec_passed(name)
move_progress
@output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{escape(name)}</span></dd>"
STDOUT.flush
end
def spec_failed(name, counter, failure)
@output.puts " <script type=\"text/javascript\">makeRed('header');</script>"
@output.puts " <script type=\"text/javascript\">makeRed('context_#{@current_context_number}');</script>"
move_progress
@output.puts " <dd class=\"spec failed\">"
@output.puts " <span class=\"failed_spec_name\">#{escape(name)}</span>"
@output.puts " <div class=\"failure\" id=\"failure_#{counter}\">"
@output.puts " <div class=\"message\"><pre>#{escape(failure.exception.message)}</pre></div>" unless failure.exception.nil?
@output.puts " <div class=\"backtrace\"><pre>#{format_backtrace(failure.exception.backtrace)}</pre></div>" unless failure.exception.nil?
@output.puts extra_failure_content unless extra_failure_content.nil?
@output.puts " </div>"
@output.puts " </dd>"
STDOUT.flush
end
# Override this method if you wish to output extra HTML in the header
#
def extra_header_content
end
# Override this method if you wish to output extra HTML for a failed spec. For example, you
# could output links to images or other files produced during the specs.
#
def extra_failure_content
end
def move_progress
percent_done = @spec_count == 0 ? 100.0 : (@current_spec_number.to_f / @spec_count.to_f * 1000).to_i / 10.0
@output.puts " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
end
def escape(string)
string.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
end
def dump_failure(counter, failure)
end
def dump_summary(duration, spec_count, failure_count)
if @dry_run
totals = "This was a dry-run"
else
totals = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
end
@output.puts "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{duration} seconds</strong>\";</script>"
@output.puts "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{totals}\";</script>"
@output.puts "</div>"
@output.puts "</body>"
@output.puts "</html>"
STDOUT.flush
end
HEADER_1 = <<-EOF
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>RSpec results</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Expires" content="-1" />
<meta http-equiv="Pragma" content="no-cache" />
EOF
HEADER_2 = <<-EOF
<script type="text/javascript">
function moveProgressBar(percentDone) {
document.getElementById("header").style.width = percentDone +"%";
}
function makeRed(element_id) {
document.getElementById(element_id).style.background = '#C40D0D';
}
</script>
<style type="text/css">
body {
margin: 0; padding: 0;
background: #fff;
}
#header {
background: #65C400; color: #fff;
}
h1 {
margin: 0 0 10px;
padding: 10px;
font: bold 18px "Lucida Grande", Helvetica, sans-serif;
}
#summary {
margin: 0; padding: 5px 10px;
font: bold 10px "Lucida Grande", Helvetica, sans-serif;
text-align: right;
position: absolute;
top: 0px;
right: 0px;
}
#summary p {
margin: 0 0 2px;
}
#summary #totals {
font-size: 14px;
}
.context {
margin: 0 10px 5px;
background: #fff;
}
dl {
margin: 0; padding: 0 0 5px;
font: normal 11px "Lucida Grande", Helvetica, sans-serif;
}
dt {
padding: 3px;
background: #65C400;
color: #fff;
font-weight: bold;
}
dd {
margin: 5px 0 5px 5px;
padding: 3px 3px 3px 18px;
}
dd.spec.passed {
border-left: 5px solid #65C400;
border-bottom: 1px solid #65C400;
background: #DBFFB4; color: #3D7700;
}
dd.spec.failed {
border-left: 5px solid #C20000;
border-bottom: 1px solid #C20000;
color: #C20000; background: #FFFBD3;
}
div.backtrace {
color: #000;
font-size: 12px;
}
a {
color: #BE5C00;
}
</style>
</head>
<body>
<div id="header">
<h1>RSpec Results</h1>
<div id="summary">
<p id="duration">&nbsp;</p>
<p id="totals">&nbsp;</p>
</div>
</div>
<div id="results">
EOF
end
end
end
end

Просмотреть файл

@ -0,0 +1,27 @@
module Spec
module Runner
module Formatter
class ProgressBarFormatter < BaseTextFormatter
def add_context(name, first)
@output.puts if first
STDOUT.flush
end
def spec_failed(name, counter, failure)
@output.print failure.expectation_not_met? ? red('F') : magenta('F')
STDOUT.flush
end
def spec_passed(name)
@output.print green('.')
STDOUT.flush
end
def start_dump
@output.puts
STDOUT.flush
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,22 @@
module Spec
module Runner
module Formatter
class RdocFormatter < BaseTextFormatter
def add_context(name, first)
@output.print "# #{name}\n"
STDOUT.flush
end
def spec_passed(name)
@output.print "# * #{name}\n"
STDOUT.flush
end
def spec_failed(name, counter, failure)
@output.print "# * #{name} [#{counter} - FAILED]\n"
STDOUT.flush
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,23 @@
module Spec
module Runner
module Formatter
class SpecdocFormatter < BaseTextFormatter
def add_context(name, first)
@output.puts
@output.puts name
STDOUT.flush
end
def spec_failed(name, counter, failure)
@output.puts failure.expectation_not_met? ? red("- #{name} (FAILED - #{counter})") : magenta("- #{name} (ERROR - #{counter})")
STDOUT.flush
end
def spec_passed(name)
@output.print green("- #{name}\n")
STDOUT.flush
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,71 @@
begin
require 'rubygems'
require 'heckle'
rescue LoadError ; raise "You must gem install heckle to use --heckle" ; end
module Spec
module Runner
# Creates a new Heckler configured to heckle all methods in the classes
# whose name matches +filter+
class HeckleRunner
def initialize(filter, heckle_class=Heckler)
@filter = filter
@heckle_class = heckle_class
end
# Runs all the contexts held by +context_runner+ once for each of the
# methods in the matched classes.
def heckle_with(context_runner)
if @filter =~ /(.*)[#\.](.*)/
heckle_method($1, $2)
else
heckle_class_or_module(@filter)
end
end
def heckle_method(class_name, method_name)
verify_constant(class_name)
heckle = @heckle_class.new(class_name, method_name, context_runner)
heckle.validate
end
def heckle_class_or_module(class_or_module_name)
verify_constant(class_or_module_name)
pattern = /^#{class_or_module_name}/
classes = []
ObjectSpace.each_object(Class) do |klass|
classes << klass if klass.name =~ pattern
end
classes.each do |klass|
klass.instance_methods(false).each do |method_name|
heckle = @heckle_class.new(klass.name, method_name, context_runner)
heckle.validate
end
end
end
def verify_constant(name)
begin
# This is defined in Heckle
name.to_class
rescue
raise "Heckling failed - \"#{name}\" is not a known class or module"
end
end
end
#Supports Heckle 1.2 and prior (earlier versions used Heckle::Base)
class Heckler < (Heckle.const_defined?(:Base) ? Heckle::Base : Heckle)
def initialize(klass_name, method_name, context_runner)
super(klass_name, method_name)
@context_runner = context_runner
end
def tests_pass?
failure_count = @context_runner.run(false)
failure_count == 0
end
end
end
end

Просмотреть файл

@ -0,0 +1,10 @@
module Spec
module Runner
# Dummy implementation for Windows that just fails (Heckle is not supported on Windows)
class HeckleRunner
def initialize(filter)
raise "Heckle not supported on Windows"
end
end
end
end

Просмотреть файл

@ -0,0 +1,224 @@
require 'ostruct'
require 'optparse'
require 'spec/runner/spec_parser'
require 'spec/runner/formatter'
require 'spec/runner/backtrace_tweaker'
require 'spec/runner/reporter'
require 'spec/runner/context_runner'
module Spec
module Runner
class OptionParser
def initialize
@spec_parser = SpecParser.new
@file_factory = File
end
def create_context_runner(args, err, out, warn_if_no_files)
options = parse(args, err, out, warn_if_no_files)
# Some exit points in parse (--generate-options, --drb) don't return the options,
# but hand over control. In that case we don't want to continue.
return nil unless options.is_a?(OpenStruct)
formatter = options.formatter_type.new(options.out, options.dry_run, options.colour)
options.reporter = Reporter.new(formatter, options.backtrace_tweaker)
# this doesn't really belong here.
# it should, but the way things are coupled, it doesn't
if options.differ_class
Spec::Expectations.differ = options.differ_class.new(options.diff_format, options.context_lines, options.colour)
end
unless options.generate
ContextRunner.new(options)
end
end
def parse(args, err, out, warn_if_no_files)
options_file = nil
args_copy = args.dup
options = OpenStruct.new
options.out = (out == STDOUT ? Kernel : out)
options.formatter_type = Formatter::ProgressBarFormatter
options.backtrace_tweaker = QuietBacktraceTweaker.new
options.spec_name = nil
opts = ::OptionParser.new do |opts|
opts.banner = "Usage: spec [options] (FILE|DIRECTORY|GLOB)+"
opts.separator ""
opts.on("-D", "--diff [FORMAT]", "Show diff of objects that are expected to be equal when they are not",
"Builtin formats: unified|u|context|c",
"You can also specify a custom differ class",
"(in which case you should also specify --require)") do |format|
# TODO make context_lines settable
options.context_lines = 3
case format
when 'context', 'c'
options.diff_format = :context
when 'unified', 'u', '', nil
options.diff_format = :unified
end
if [:context,:unified].include? options.diff_format
require 'spec/expectations/differs/default'
options.differ_class = Spec::Expectations::Differs::Default
else
begin
options.diff_format = :custom
options.differ_class = eval(format)
rescue NameError
err.puts "Couldn't find differ class #{format}"
err.puts "Make sure the --require option is specified *before* --diff"
exit if out == $stdout
end
end
end
opts.on("-c", "--colour", "--color", "Show coloured (red/green) output") do
options.colour = true
end
opts.on("-s", "--spec SPECIFICATION_NAME", "Execute context or specification with matching name") do |spec_name|
options.spec_name = spec_name
end
opts.on("-l", "--line LINE_NUMBER", Integer, "Execute context or specification at given line") do |line_number|
options.line_number = line_number.to_i
end
opts.on("-f", "--format FORMAT", "Builtin formats: specdoc|s|rdoc|r|html|h",
"You can also specify a custom formatter class",
"(in which case you should also specify --require)") do |format|
case format
when 'specdoc', 's'
options.formatter_type = Formatter::SpecdocFormatter
when 'html', 'h'
options.formatter_type = Formatter::HtmlFormatter
when 'rdoc', 'r'
options.formatter_type = Formatter::RdocFormatter
options.dry_run = true
else
begin
options.formatter_type = eval(format)
rescue NameError
err.puts "Couldn't find formatter class #{format}"
err.puts "Make sure the --require option is specified *before* --format"
exit if out == $stdout
end
end
end
opts.on("-r", "--require FILE", "Require FILE before running specs",
"Useful for loading custom formatters or other extensions",
"If this option is used it must come before the others") do |req|
req.split(",").each{|file| require file}
end
opts.on("-b", "--backtrace", "Output full backtrace") do
options.backtrace_tweaker = NoisyBacktraceTweaker.new
end
opts.on("-H", "--heckle CODE", "If all specs pass, this will run your specs many times, mutating",
"the specced code a little each time. The intent is that specs",
"*should* fail, and RSpec will tell you if they don't.",
"CODE should be either Some::Module, Some::Class or Some::Fabulous#method}") do |heckle|
heckle_runner = PLATFORM == 'i386-mswin32' ? 'spec/runner/heckle_runner_win' : 'spec/runner/heckle_runner'
require heckle_runner
options.heckle_runner = HeckleRunner.new(heckle)
end
opts.on("-d", "--dry-run", "Don't execute specs") do
options.dry_run = true
end
opts.on("-o", "--out OUTPUT_FILE", "Path to output file (defaults to STDOUT)") do |out_file|
options.out = File.new(out_file, 'w')
end
opts.on("-O", "--options PATH", "Read options from a file") do |options_file|
# Remove the --options option and the argument before writing to file
index = args_copy.index("-O") || args_copy.index("--options")
args_copy.delete_at(index)
args_copy.delete_at(index)
new_args = args_copy + IO.readlines(options_file).each {|s| s.chomp!}
return CommandLine.run(new_args, err, out, true, warn_if_no_files)
end
opts.on("-G", "--generate-options PATH", "Generate an options file for --options") do |options_file|
# Remove the --generate-options option and the argument before writing to file
index = args_copy.index("-G") || args_copy.index("--generate-options")
args_copy.delete_at(index)
args_copy.delete_at(index)
File.open(options_file, 'w') do |io|
io.puts args_copy.join("\n")
end
out.puts "\nOptions written to #{options_file}. You can now use these options with:"
out.puts "spec --options #{options_file}"
options.generate = true
end
opts.on("-X", "--drb", "Run specs via DRb. (For example against script/rails_spec_server)") do |options_file|
# Remove the --options option and the argument before writing to file
index = args_copy.index("-X") || args_copy.index("--drb")
args_copy.delete_at(index)
return DrbCommandLine.run(args_copy, err, out, true, warn_if_no_files)
end
opts.on("-v", "--version", "Show version") do
out.puts ::Spec::VERSION::DESCRIPTION
exit if out == $stdout
end
opts.on_tail("-h", "--help", "You're looking at it") do
out.puts opts
exit if out == $stdout
end
end
opts.parse!(args)
if args.empty? && warn_if_no_files
err.puts "No files specified."
err.puts opts
exit(6) if err == $stderr
end
if options.line_number
set_spec_from_line_number(options, args, err)
end
options
end
def set_spec_from_line_number(options, args, err)
unless options.spec_name
if args.length == 1
if @file_factory.file?(args[0])
source = @file_factory.open(args[0])
options.spec_name = @spec_parser.spec_name_for(source, options.line_number)
elsif @file_factory.directory?(args[0])
err.puts "You must specify one file, not a directory when using the --line option"
exit(1) if err == $stderr
else
err.puts "#{args[0]} does not exist"
exit(2) if err == $stderr
end
else
err.puts "Only one file can be specified when using the --line option: #{args.inspect}"
exit(3) if err == $stderr
end
else
err.puts "You cannot use both --line and --spec"
exit(4) if err == $stderr
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,105 @@
module Spec
module Runner
class Reporter
def initialize(formatter, backtrace_tweaker)
@formatter = formatter
@backtrace_tweaker = backtrace_tweaker
clear!
end
def add_context(name)
#TODO - @context_names.empty? tells the formatter whether this is the first context or not - that's a little slippery
@formatter.add_context(name, @context_names.empty?)
@context_names << name
end
def spec_started(name)
@spec_names << name
@formatter.spec_started(name)
end
def spec_finished(name, error=nil, failure_location=nil)
if error.nil?
spec_passed(name)
else
@backtrace_tweaker.tweak_backtrace(error, failure_location)
spec_failed(name, Failure.new(@context_names.last, name, error))
end
end
def start(number_of_specs)
clear!
@start_time = Time.new
@formatter.start(number_of_specs)
end
def end
@end_time = Time.new
end
# Dumps the summary and returns the total number of failures
def dump
@formatter.start_dump
dump_failures
@formatter.dump_summary(duration, @spec_names.length, @failures.length)
@failures.length
end
private
def clear!
@context_names = []
@failures = []
@spec_names = []
@start_time = nil
@end_time = nil
end
def dump_failures
return if @failures.empty?
@failures.inject(1) do |index, failure|
@formatter.dump_failure(index, failure)
index + 1
end
end
def duration
return @end_time - @start_time unless (@end_time.nil? or @start_time.nil?)
return "0.0"
end
def spec_passed(name)
@formatter.spec_passed(name)
end
def spec_failed(name, failure)
@failures << failure
@formatter.spec_failed(name, @failures.length, failure)
end
class Failure
attr_reader :exception
def initialize(context_name, spec_name, exception)
@context_name = context_name
@spec_name = spec_name
@exception = exception
end
def header
if expectation_not_met?
"'#{@context_name} #{@spec_name}' FAILED"
else
"#{@exception.class.name} in '#{@context_name} #{@spec_name}'"
end
end
def expectation_not_met?
@exception.is_a?(Spec::Expectations::ExpectationNotMetError)
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,25 @@
module Spec
module Runner
class SpecMatcher
attr_writer :spec_desc
def initialize(context_desc, spec_desc=nil)
@context_desc = context_desc
@spec_desc = spec_desc
end
def matches?(desc)
desc =~ /(^#{context_regexp} #{spec_regexp}$|^#{context_regexp}$|^#{spec_regexp}$)/
end
private
def context_regexp
Regexp.escape(@context_desc)
end
def spec_regexp
Regexp.escape(@spec_desc)
end
end
end
end

Просмотреть файл

@ -0,0 +1,41 @@
module Spec
module Runner
# Parses a spec file and finds the nearest spec for a given line number.
class SpecParser
def spec_name_for(io, line_number)
source = io.read
context = context_at_line(source, line_number)
spec = spec_at_line(source, line_number)
if context && spec
"#{context} #{spec}"
elsif context
context
else
nil
end
end
protected
def context_at_line(source, line_number)
find_above(source, line_number, /^\s*context\s+['|"](.*)['|"]/)
end
def spec_at_line(source, line_number)
find_above(source, line_number, /^\s*specify\s+['|"](.*)['|"]/)
end
def find_above(source, line_number, pattern)
lines_above_reversed(source, line_number).each do |line|
return $1 if line =~ pattern
end
nil
end
def lines_above_reversed(source, line_number)
lines = source.split("\n")
lines[0...line_number].reverse
end
end
end
end

Просмотреть файл

@ -0,0 +1,74 @@
module Spec
module Runner
class SpecShouldRaiseHandler
def initialize(file_and_line_number, opts)
@file_and_line_number = file_and_line_number
@options = opts
@expected_error_class = determine_error_class(opts)
@expected_error_message = determine_error_message(opts)
end
def determine_error_class(opts)
if candidate = opts[:should_raise]
if candidate.is_a?(Class)
return candidate
elsif candidate.is_a?(Array)
return candidate[0]
else
return Exception
end
end
end
def determine_error_message(opts)
if candidate = opts[:should_raise]
if candidate.is_a?(Array)
return candidate[1]
end
end
return nil
end
def build_message(exception=nil)
if @expected_error_message.nil?
message = "specify block expected #{@expected_error_class.to_s}"
else
message = "specify block expected #{@expected_error_class.new(@expected_error_message.to_s).inspect}"
end
message << " but raised #{exception.inspect}" if exception
message << " but nothing was raised" unless exception
message << "\n"
message << @file_and_line_number
end
def error_matches?(error)
return false unless error.kind_of?(@expected_error_class)
unless @expected_error_message.nil?
if @expected_error_message.is_a?(Regexp)
return false unless error.message =~ @expected_error_message
else
return false unless error.message == @expected_error_message
end
end
return true
end
def handle(errors)
if @expected_error_class
if errors.empty?
errors << Spec::Expectations::ExpectationNotMetError.new(build_message)
else
error_to_remove = errors.detect do |error|
error_matches?(error)
end
if error_to_remove.nil?
errors.insert(0,Spec::Expectations::ExpectationNotMetError.new(build_message(errors[0])))
else
errors.delete(error_to_remove)
end
end
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,114 @@
module Spec
module Runner
class Specification
class << self
attr_accessor :current, :generated_description
protected :current=
callback_events :before_setup, :after_teardown
end
attr_reader :spec_block
callback_events :before_setup, :after_teardown
def initialize(name, opts={}, &spec_block)
@from = caller(0)[3]
@description = name
@options = opts
@spec_block = spec_block
@description_generated_callback = lambda { |desc| @generated_description = desc }
end
def run(reporter, setup_block, teardown_block, dry_run, execution_context)
reporter.spec_started(name) if reporter
return reporter.spec_finished(name) if dry_run
errors = []
begin
set_current
setup_ok = setup_spec(execution_context, errors, &setup_block)
spec_ok = execute_spec(execution_context, errors) if setup_ok
teardown_ok = teardown_spec(execution_context, errors, &teardown_block)
ensure
clear_current
end
SpecShouldRaiseHandler.new(@from, @options).handle(errors)
reporter.spec_finished(name, errors.first, failure_location(setup_ok, spec_ok, teardown_ok)) if reporter
end
def matches?(matcher, description)
matcher.spec_desc = name
matcher.matches?(description)
end
private
def name
@description == :__generate_description ? generated_description : @description
end
def generated_description
@generated_description || "NAME NOT GENERATED"
end
def setup_spec(execution_context, errors, &setup_block)
notify_before_setup(errors)
execution_context.instance_eval(&setup_block) if setup_block
return errors.empty?
rescue => e
errors << e
return false
end
def execute_spec(execution_context, errors)
begin
execution_context.instance_eval(&spec_block)
return true
rescue Exception => e
errors << e
return false
end
end
def teardown_spec(execution_context, errors, &teardown_block)
execution_context.instance_eval(&teardown_block) if teardown_block
notify_after_teardown(errors)
return errors.empty?
rescue => e
errors << e
return false
end
def notify_before_setup(errors)
notify_class_callbacks(:before_setup, self, &append_errors(errors))
notify_callbacks(:before_setup, self, &append_errors(errors))
end
def notify_after_teardown(errors)
notify_callbacks(:after_teardown, self, &append_errors(errors))
notify_class_callbacks(:after_teardown, self, &append_errors(errors))
end
def append_errors(errors)
proc {|error| errors << error}
end
def set_current
Spec::Matchers.description_generated(&@description_generated_callback)
self.class.send(:current=, self)
end
def clear_current
Spec::Matchers.unregister_callback(:description_generated, @description_generated_callback)
self.class.send(:current=, nil)
end
def failure_location(setup_ok, spec_ok, teardown_ok)
return 'setup' unless setup_ok
return name unless spec_ok
return 'teardown' unless teardown_ok
end
end
end
end

Просмотреть файл

@ -0,0 +1,87 @@
require 'fileutils'
module Spec
class Translator
def translate_dir(from, to)
from = File.expand_path(from)
to = File.expand_path(to)
if File.directory?(from)
FileUtils.mkdir_p(to) unless File.directory?(to)
Dir["#{from}/*"].each do |sub_from|
path = sub_from[from.length+1..-1]
sub_to = File.join(to, path)
translate_dir(sub_from, sub_to)
end
else
translate_file(from, to)
end
end
def translate_file(from, to)
translation = ""
File.open(from) do |io|
io.each_line do |line|
translation << translate(line)
end
end
File.open(to, "w") do |io|
io.write(translation)
end
end
def translate(line)
return line if line =~ /(should_not|should)_receive/
if line =~ /(.*\.)(should_not|should)(?:_be)(?!_)(.*)/m
pre = $1
should = $2
post = $3
be_or_equal = post =~ /(<|>)/ ? "be" : "equal"
return "#{pre}#{should} #{be_or_equal}#{post}"
end
if line =~ /(.*\.)(should_not|should)_(?!not)(.*)/m
pre = $1
should = $2
post = $3
post.gsub!(/^raise/, 'raise_error')
post.gsub!(/^throw/, 'throw_symbol')
unless standard_matcher?(post)
post = "be_#{post}"
end
line = "#{pre}#{should} #{post}"
end
line
end
def standard_matcher?(matcher)
patterns = [
/^be/,
/^be_close/,
/^eql/,
/^equal/,
/^has/,
/^have/,
/^change/,
/^include/,
/^match/,
/^raise_error/,
/^respond_to/,
/^satisfy/,
/^throw_symbol/,
# Extra ones that we use in spec_helper
/^pass/,
/^fail/,
/^fail_with/,
]
matched = patterns.detect{ |p| matcher =~ p }
!matched.nil?
end
end
end

30
test/lib/spec/version.rb Normal file
Просмотреть файл

@ -0,0 +1,30 @@
module Spec
module VERSION
def self.build_tag
tag = "REL_" + [MAJOR, MINOR, TINY].join('_')
if defined?(RELEASE_CANDIDATE)
tag << "_" << RELEASE_CANDIDATE
end
tag
end
unless defined? MAJOR
MAJOR = 0
MINOR = 8
TINY = 2
# RELEASE_CANDIDATE = "RC1"
# RANDOM_TOKEN: 0.375509844656552
REV = "$LastChangedRevision$".match(/LastChangedRevision: (\d+)/)[1]
STRING = [MAJOR, MINOR, TINY].join('.')
FULL_VERSION = "#{STRING} (r#{REV})"
TAG = build_tag
NAME = "RSpec"
URL = "http://rspec.rubyforge.org/"
DESCRIPTION = "#{NAME}-#{FULL_VERSION} - BDD for Ruby\n#{URL}"
end
end
end