diff options
| author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-17 02:48:41 +0000 |
|---|---|---|
| committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-17 02:48:41 +0000 |
| commit | ba23a5ac276e59fdda8186750c6d0fd2cfecdeac (patch) | |
| tree | 1e14b25ade74ea52d8da2788ede9b12b507867e8 /test/lib/spec/mocks | |
| parent | 8ea6adaeb1e3d0aa6348c2a2c3a385d185372d06 (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.rb | 132 | ||||
| -rw-r--r-- | test/lib/spec/mocks/error_generator.rb | 85 | ||||
| -rw-r--r-- | test/lib/spec/mocks/errors.rb | 10 | ||||
| -rw-r--r-- | test/lib/spec/mocks/extensions/object.rb | 3 | ||||
| -rw-r--r-- | test/lib/spec/mocks/message_expectation.rb | 231 | ||||
| -rw-r--r-- | test/lib/spec/mocks/methods.rb | 40 | ||||
| -rw-r--r-- | test/lib/spec/mocks/mock.rb | 26 | ||||
| -rw-r--r-- | test/lib/spec/mocks/mock_handler.rb | 166 | ||||
| -rw-r--r-- | test/lib/spec/mocks/order_group.rb | 29 |
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 |
