From f8770adb29be14845b407494cc0e3e1b98ce75e3 Mon Sep 17 00:00:00 2001 From: smiddlek Date: Wed, 7 Jul 2010 17:43:04 +0000 Subject: Adding a "fix" (cough, hack!) for method signature validation when calling an unbound method with the instance as the first arg. By Steve Middlekauff git-svn-id: http://pymox.googlecode.com/svn/trunk@52 b1010a0a-674b-0410-b734-77272b80c875 --- mox.py | 31 ++++++++++++++++++++++++------- mox_test.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/mox.py b/mox.py index ba73bd3..948973a 100755 --- a/mox.py +++ b/mox.py @@ -819,7 +819,7 @@ class _MockObjectFactory(MockObject): super(_MockObjectFactory, self)._Verify() -class MethodCallChecker(object): +class MethodSignatureChecker(object): """Ensures that methods are called correctly.""" _NEEDED, _DEFAULT, _GIVEN = range(3) @@ -843,6 +843,7 @@ class MethodCallChecker(object): if inspect.ismethod(method): self._args = self._args[1:] # Skip 'self'. self._method = method + self._instance = None # May contain the instance this is bound to. self._has_varargs = varargs is not None self._has_varkw = varkw is not None @@ -865,9 +866,9 @@ class MethodCallChecker(object): Raises: AttributeError: arg_name is already marked as _GIVEN. """ - if arg_status.get(arg_name, None) == MethodCallChecker._GIVEN: + if arg_status.get(arg_name, None) == MethodSignatureChecker._GIVEN: raise AttributeError('%s provided more than once' % (arg_name,)) - arg_status[arg_name] = MethodCallChecker._GIVEN + arg_status[arg_name] = MethodSignatureChecker._GIVEN def Check(self, params, named_params): """Ensures that the parameters used while recording a call are valid. @@ -881,10 +882,26 @@ class MethodCallChecker(object): Raises: AttributeError: the given parameters don't work with the given method. """ - arg_status = dict((a, MethodCallChecker._NEEDED) + arg_status = dict((a, MethodSignatureChecker._NEEDED) for a in self._required_args) for arg in self._default_args: - arg_status[arg] = MethodCallChecker._DEFAULT + arg_status[arg] = MethodSignatureChecker._DEFAULT + + # WARNING: Suspect hack ahead. + # + # Check to see if this is an unbound method, where the instance + # should be bound as the first argument. We try to determine if + # the first argument (param[0]) is an instance of the class, or it + # is equivalent to the class (used to account for Comparators). + # + # NOTE: If a Func() comparator is used, and the signature is not + # correct, this will cause extra executions of the function. + if inspect.ismethod(self._method): + # The extra param accounts for the bound instance. + if len(params) == len(self._args) + 1: + clazz = getattr(self._method, 'im_class', None) + if isinstance(params[0], clazz) or params[0] == clazz: + params = params[1:] # Check that each positional param is valid. for i in range(len(params)): @@ -906,7 +923,7 @@ class MethodCallChecker(object): # Ensure all the required arguments have been given. still_needed = [k for k, v in arg_status.iteritems() - if v == MethodCallChecker._NEEDED] + if v == MethodSignatureChecker._NEEDED] if still_needed: raise AttributeError('No values given for arguments: %s' % (' '.join(sorted(still_needed)))) @@ -956,7 +973,7 @@ class MockMethod(object): self._side_effects = None try: - self._checker = MethodCallChecker(method_to_mock) + self._checker = MethodSignatureChecker(method_to_mock) except ValueError: self._checker = None diff --git a/mox_test.py b/mox_test.py index 2e429e6..005455a 100755 --- a/mox_test.py +++ b/mox_test.py @@ -1496,6 +1496,59 @@ class MoxTest(unittest.TestCase): self.assertEquals('foo', actual) self.assertTrue(type(test_obj.OtherValidCall) is method_type) + def testStubOutMethod_CalledAsUnboundMethod_Comparator(self): + instance = TestClass() + self.mox.StubOutWithMock(TestClass, 'OtherValidCall') + + TestClass.OtherValidCall(mox.IgnoreArg()).AndReturn('foo') + self.mox.ReplayAll() + + actual = TestClass.OtherValidCall(instance) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + self.assertEquals('foo', actual) + + def testStubOutMethod_CalledAsUnboundMethod_ActualInstance(self): + instance = TestClass() + self.mox.StubOutWithMock(TestClass, 'OtherValidCall') + + TestClass.OtherValidCall(instance).AndReturn('foo') + self.mox.ReplayAll() + + actual = TestClass.OtherValidCall(instance) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + self.assertEquals('foo', actual) + + def testStubOutMethod_CalledAsUnboundMethod_DifferentInstance(self): + instance = TestClass() + self.mox.StubOutWithMock(TestClass, 'OtherValidCall') + + TestClass.OtherValidCall(instance).AndReturn('foo') + self.mox.ReplayAll() + + # This should fail, since the instances are different + self.assertRaises(mox.UnexpectedMethodCallError, + TestClass.OtherValidCall, "wrong self") + + + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def testStubOutMethod_CalledAsBoundMethod(self): + t = self.mox.CreateMock(TestClass) + + t.MethodWithArgs(mox.IgnoreArg(), mox.IgnoreArg()).AndReturn('foo') + self.mox.ReplayAll() + + actual = t.MethodWithArgs(None, None); + + self.mox.VerifyAll() + self.mox.UnsetStubs() + self.assertEquals('foo', actual) + def testStubOutClass_OldStyle(self): """Test a mocked class whose __init__ returns a Mock.""" self.mox.StubOutWithMock(mox_test_helper, 'TestClassFromAnotherModule') -- cgit v1.2.1