lib/muack/mock.rb in muack-1.4.0 vs lib/muack/mock.rb in muack-1.5.0

- old
+ new

@@ -4,12 +4,10 @@ require 'muack/failure' require 'muack/block' require 'muack/error' module Muack - EmptyBlock = proc{} - class Mock < BasicObject attr_reader :object def initialize object @object = object @__mock_injected = {} @@ -22,12 +20,12 @@ def inspect "Muack::API.#{__mock_class.name[/\w+$/].downcase}(#{object.inspect})" end # Public API: Define mocked method - def method_missing msg, *args, &block - defi = Definition.new(msg, args, block) + def method_missing msg, *args, &returns + defi = Definition.new(msg, args, returns) if injected = __mock_injected[defi.msg] defi.original_method = injected.original_method else __mock_inject_method(defi) end @@ -49,57 +47,57 @@ def __mock_class (class << self; self; end).superclass end # used for mocked object to dispatch mocked method - def __mock_dispatch msg, actual_args - if defi = __mock_defis[msg].shift + def __mock_dispatch actual_call + if defi = __mock_defis[actual_call.msg].shift __mock_disps_push(defi) - if __mock_check_args(defi.args, actual_args) + if __mock_check_args(defi, actual_call) defi else Mock.__send__(:raise, # Wrong argument - Unexpected.new(object, [defi], msg, actual_args)) + Unexpected.new(object, [defi], actual_call)) end else - __mock_failed(msg, actual_args) + __mock_failed(actual_call) end end # used for mocked object to dispatch mocked method - def __mock_dispatch_call context, disp, actual_args, actual_block, &_yield - args = if disp.peek_args - __mock_block_call(context, disp.peek_args, - actual_args, actual_block, true) - else - actual_args - end + def __mock_dispatch_call context, disp, actual_call, &proxy_super + # resolving arguments + call = + if disp.peek_args + args = __mock_block_call(context, disp.peek_args, actual_call) + ActualCall.new(actual_call.msg, args, actual_call.block) + else + actual_call + end - ret = if disp.returns - __mock_block_call(context, disp.returns, - args, actual_block, true) - elsif disp.original_method # proxies for singleton methods - context.__send__(disp.original_method, *args, &actual_block) - else # proxies for instance methods - # need the original context for calling `super` - # ruby: can't pass a block to yield, so we name it _yield - _yield.call(args, &actual_block) - end + # retrieve actual return + ret = + if disp.returns + __mock_block_call(context, disp.returns, call) + else + __mock_proxy_call(context, disp, call, proxy_super) + end + # resolving return if disp.peek_return - __mock_block_call(context, disp.peek_return, ret, EmptyBlock, false) + __mock_block_call(context, disp.peek_return, ret, true) else ret end end # used for Muack::Session#verify def __mock_verify __mock_defis.values.all?(&:empty?) || begin - msg, defis_with_same_msg = __mock_defis.find{ |_, v| v.size > 0 } + msg, defis_with_same_msg = __mock_defis.find{ |_, v| v.any? } args, defis = defis_with_same_msg.group_by(&:args).first - dsize = __mock_disps[msg].select{ |d| d.args == args }.size + dsize = __mock_disps[msg].count{ |d| d.args == args } Mock.__send__(:raise, # Too little times Expected.new(object, defis.first, defis.size + dsize, dsize)) end end @@ -112,40 +110,50 @@ attr_accessor :__mock_defis, :__mock_disps, :__mock_injected private def __mock_inject_method defi __mock_injected[defi.msg] = defi - target = object.singleton_class # would be the class in AnyInstanceOf - privilege = Mock.store_original_method(target, defi) - __mock_inject_mock_method(target, defi, privilege) + # a) ancestors.first is the first module in the method chain. + # it's just the singleton_class when nothing was prepended, + # otherwise the last prepended module. + # b) would be the class in AnyInstanceOf. + target = object.singleton_class.ancestors.first + Mock.store_original_method(target, defi) + __mock_inject_mock_method(target, defi) end def __mock_reset_method defi - object.singleton_class.module_eval do + object.singleton_class.ancestors.first.module_eval do remove_method(defi.msg) # restore original method - if instance_methods(false).include?(defi.original_method) || + if public_instance_methods(false).include?(defi.original_method) || + protected_instance_methods(false).include?(defi.original_method) || private_instance_methods(false).include?(defi.original_method) alias_method(defi.msg, defi.original_method) + __send__(defi.visibility, defi.msg) remove_method(defi.original_method) end end end def self.store_original_method klass, defi - privilege = if klass.instance_methods(false).include?(defi.msg) - :public # TODO: forget about protected methods? + visibility = if klass.public_instance_methods(false).include?(defi.msg) + :public + elsif klass.protected_instance_methods(false).include?(defi.msg) + :protected elsif klass.private_instance_methods(false).include?(defi.msg) :private end - return :public unless privilege - # store original method - original_method = find_new_name(klass, defi.msg) - klass.__send__(:alias_method, original_method, defi.msg) - defi.original_method = original_method - privilege + if visibility # store original method + original_method = find_new_name(klass, defi.msg) + klass.__send__(:alias_method, original_method, defi.msg) + defi.original_method = original_method + defi.visibility = visibility + else + defi.visibility = :public + end end def self.find_new_name klass, message, level=0 if level >= (::ENV['MUACK_RECURSION_LEVEL'] || 9).to_i raise CannotFindInjectionName.new(level+1, message) @@ -157,53 +165,103 @@ else new_name end end - def __mock_inject_mock_method target, defi, privilege=:public + def __mock_inject_mock_method target, defi mock = self # remember the context target.__send__(:define_method, defi.msg){ |*actual_args, &actual_block| - disp = mock.__mock_dispatch(defi.msg, actual_args) - mock.__mock_dispatch_call(self, disp, actual_args, - actual_block) do |args, &block| - super(*args, &block) + actual_call = ActualCall.new(defi.msg, actual_args, actual_block) + disp = mock.__mock_dispatch(actual_call) + mock.__mock_dispatch_call(self, disp, actual_call) do |call, has_kargs| + # need the original context for calling `super` + if has_kargs && kargs = call.args.last + super(*call.args[0...-1], **kargs, &call.block) + else + super(*call.args, &call.block) + end end } - target.__send__(privilege, defi.msg) + target.__send__(defi.visibility, defi.msg) end # used for __mock_dispatch - def __mock_failed msg, actual_args, disps=__mock_disps[msg] - if expected = __mock_find_checked_difi(disps, actual_args) + def __mock_failed actual_call, disps=__mock_disps[actual_call.msg] + if expected = __mock_find_checked_difi(disps, actual_call) Mock.__send__(:raise, # Too many times Expected.new(object, expected, disps.size, disps.size+1)) else Mock.__send__(:raise, # Wrong argument - Unexpected.new(object, disps, msg, actual_args)) + Unexpected.new(object, disps, actual_call)) end end # used for __mock_dispatch_call - def __mock_block_call context, block, actual_args, actual_block, splat - return unless block + def __mock_block_call context, block, actual_call, peek_return=false # for AnyInstanceOf, we don't have the actual context at the time # we're defining it, so we update it here - block.context = context if block.kind_of?(Block) - if splat - block.call(*actual_args, &actual_block) - else # peek_return doesn't need splat - block.call(actual_args, &actual_block) + if block.kind_of?(Block) + block.context = context + instance_exec_block = block.block end + + if peek_return # actual_call is the actual return in this case + block.call(actual_call, &instance_exec_block) + else + actual_block = actual_call.block || instance_exec_block + if __mock_block_with_kargs?(instance_exec_block || block) && + kargs = actual_call.args.last + block.call(*actual_call.args[0...-1], **kargs, &actual_block) + else + block.call(*actual_call.args, &actual_block) + end + end end - def __mock_find_checked_difi defis, actual_args, meth=:find - defis.public_send(meth){ |d| __mock_check_args(d.args, actual_args) } + # used for __mock_dispatch_call + def __mock_proxy_call context, disp, call, proxy_super + if disp.original_method # proxies for singleton methods with __send__ + if __mock_method_with_kargs?(context, disp.original_method) && + kargs = call.args.last + context.__send__( + disp.original_method, *call.args[0...-1], **kargs, &call.block) + else + context.__send__(disp.original_method, *call.args, &call.block) + end + else # proxies for instance methods with super + proxy_super.call(call, __mock_super_with_kargs?(context, call.msg)) + end end - def __mock_check_args expected_args, actual_args - if expected_args == [WithAnyArgs] - true - elsif expected_args.none?{ |arg| arg.kind_of?(Satisfying) } + def __mock_find_checked_difi defis, actual_call, meth=:find + defis.public_send(meth){ |d| __mock_check_args(d, actual_call) } + end + + def __mock_method_with_kargs? object, method_name + __mock_block_with_kargs?( + ::Kernel.instance_method(:method).bind(object).call(method_name)) + end + + def __mock_super_with_kargs? object, method_name + super_method = + ::Kernel.instance_method(:method).bind(object).call(method_name). + super_method + + super_method && __mock_block_with_kargs?(super_method) + end + + def __mock_block_with_kargs? block + # there's no Symbol#start_with? in older Ruby + block.parameters.dig(-1, 0).to_s.start_with?('key') + end + + def __mock_check_args defi, actual_call + return true if defi.args.size == 1 && defi.args.first == WithAnyArgs + + expected_args = defi.args + actual_args = actual_call.args + + if expected_args.none?{ |arg| arg.kind_of?(Satisfying) } expected_args == actual_args elsif expected_args.size == actual_args.size expected_args.zip(actual_args).all?{ |(e, a)| if e.kind_of?(Satisfying) then e.match(a) else e == a end