summaryrefslogtreecommitdiffstats
path: root/test/lib/spec/mocks
diff options
context:
space:
mode:
authorluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-03-17 02:48:41 +0000
committerluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-03-17 02:48:41 +0000
commitba23a5ac276e59fdda8186750c6d0fd2cfecdeac (patch)
tree1e14b25ade74ea52d8da2788ede9b12b507867e8 /test/lib/spec/mocks
parent8ea6adaeb1e3d0aa6348c2a2c3a385d185372d06 (diff)
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
Diffstat (limited to 'test/lib/spec/mocks')
-rw-r--r--test/lib/spec/mocks/argument_expectation.rb132
-rw-r--r--test/lib/spec/mocks/error_generator.rb85
-rw-r--r--test/lib/spec/mocks/errors.rb10
-rw-r--r--test/lib/spec/mocks/extensions/object.rb3
-rw-r--r--test/lib/spec/mocks/message_expectation.rb231
-rw-r--r--test/lib/spec/mocks/methods.rb40
-rw-r--r--test/lib/spec/mocks/mock.rb26
-rw-r--r--test/lib/spec/mocks/mock_handler.rb166
-rw-r--r--test/lib/spec/mocks/order_group.rb29
9 files changed, 722 insertions, 0 deletions
diff --git a/test/lib/spec/mocks/argument_expectation.rb b/test/lib/spec/mocks/argument_expectation.rb
new file mode 100644
index 000000000..a4870e767
--- /dev/null
+++ b/test/lib/spec/mocks/argument_expectation.rb
@@ -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
diff --git a/test/lib/spec/mocks/error_generator.rb b/test/lib/spec/mocks/error_generator.rb
new file mode 100644
index 000000000..950864a7a
--- /dev/null
+++ b/test/lib/spec/mocks/error_generator.rb
@@ -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 \ No newline at end of file
diff --git a/test/lib/spec/mocks/errors.rb b/test/lib/spec/mocks/errors.rb
new file mode 100644
index 000000000..68fdfe006
--- /dev/null
+++ b/test/lib/spec/mocks/errors.rb
@@ -0,0 +1,10 @@
+module Spec
+ module Mocks
+ class MockExpectationError < StandardError
+ end
+
+ class AmbiguousReturnError < StandardError
+ end
+ end
+end
+
diff --git a/test/lib/spec/mocks/extensions/object.rb b/test/lib/spec/mocks/extensions/object.rb
new file mode 100644
index 000000000..4b7531066
--- /dev/null
+++ b/test/lib/spec/mocks/extensions/object.rb
@@ -0,0 +1,3 @@
+class Object
+ include Spec::Mocks::Methods
+end
diff --git a/test/lib/spec/mocks/message_expectation.rb b/test/lib/spec/mocks/message_expectation.rb
new file mode 100644
index 000000000..152e65a47
--- /dev/null
+++ b/test/lib/spec/mocks/message_expectation.rb
@@ -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
diff --git a/test/lib/spec/mocks/methods.rb b/test/lib/spec/mocks/methods.rb
new file mode 100644
index 000000000..a5f102fcf
--- /dev/null
+++ b/test/lib/spec/mocks/methods.rb
@@ -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 \ No newline at end of file
diff --git a/test/lib/spec/mocks/mock.rb b/test/lib/spec/mocks/mock.rb
new file mode 100644
index 000000000..68de11ff4
--- /dev/null
+++ b/test/lib/spec/mocks/mock.rb
@@ -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 \ No newline at end of file
diff --git a/test/lib/spec/mocks/mock_handler.rb b/test/lib/spec/mocks/mock_handler.rb
new file mode 100644
index 000000000..ef6f97a1c
--- /dev/null
+++ b/test/lib/spec/mocks/mock_handler.rb
@@ -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 \ No newline at end of file
diff --git a/test/lib/spec/mocks/order_group.rb b/test/lib/spec/mocks/order_group.rb
new file mode 100644
index 000000000..9983207eb
--- /dev/null
+++ b/test/lib/spec/mocks/order_group.rb
@@ -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