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