Commit 8ca0fa9d authored by Chris Withers's avatar Chris Withers Committed by GitHub

bpo-35226: Fix equality for nested unittest.mock.call objects. (#10555)

Also refactor the call recording imolementation and add some notes
about its limitations.
parent 3bc0ebab
...@@ -166,6 +166,15 @@ You use the :data:`call` object to construct lists for comparing with ...@@ -166,6 +166,15 @@ You use the :data:`call` object to construct lists for comparing with
>>> mock.mock_calls == expected >>> mock.mock_calls == expected
True True
However, parameters to calls that return mocks are not recorded, which means it is not
possible to track nested calls where the parameters used to create ancestors are important:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
Setting Return Values and Attributes Setting Return Values and Attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
......
...@@ -702,6 +702,19 @@ the *new_callable* argument to :func:`patch`. ...@@ -702,6 +702,19 @@ the *new_callable* argument to :func:`patch`.
unpacked as tuples to get at the individual arguments. See unpacked as tuples to get at the individual arguments. See
:ref:`calls as tuples <calls-as-tuples>`. :ref:`calls as tuples <calls-as-tuples>`.
.. note::
The way :attr:`mock_calls` are recorded means that where nested
calls are made, the parameters of ancestor calls are not recorded
and so will always compare equal:
>>> mock = MagicMock()
>>> mock.top(a=3).bottom()
<MagicMock name='mock.top().bottom()' id='...'>
>>> mock.mock_calls
[call.top(a=3), call.top().bottom()]
>>> mock.mock_calls[-1] == call.top(a=-1).bottom()
True
.. attribute:: __class__ .. attribute:: __class__
......
...@@ -977,46 +977,51 @@ class CallableMixin(Base): ...@@ -977,46 +977,51 @@ class CallableMixin(Base):
self = _mock_self self = _mock_self
self.called = True self.called = True
self.call_count += 1 self.call_count += 1
_new_name = self._mock_new_name
_new_parent = self._mock_new_parent
# handle call_args
_call = _Call((args, kwargs), two=True) _call = _Call((args, kwargs), two=True)
self.call_args = _call self.call_args = _call
self.call_args_list.append(_call) self.call_args_list.append(_call)
self.mock_calls.append(_Call(('', args, kwargs)))
seen = set() seen = set()
skip_next_dot = _new_name == '()'
# initial stuff for method_calls:
do_method_calls = self._mock_parent is not None do_method_calls = self._mock_parent is not None
name = self._mock_name method_call_name = self._mock_name
while _new_parent is not None:
this_mock_call = _Call((_new_name, args, kwargs))
if _new_parent._mock_new_name:
dot = '.'
if skip_next_dot:
dot = ''
skip_next_dot = False # initial stuff for mock_calls:
if _new_parent._mock_new_name == '()': mock_call_name = self._mock_new_name
skip_next_dot = True is_a_call = mock_call_name == '()'
self.mock_calls.append(_Call(('', args, kwargs)))
_new_name = _new_parent._mock_new_name + dot + _new_name # follow up the chain of mocks:
_new_parent = self._mock_new_parent
while _new_parent is not None:
# handle method_calls:
if do_method_calls: if do_method_calls:
if _new_name == name: _new_parent.method_calls.append(_Call((method_call_name, args, kwargs)))
this_method_call = this_mock_call
else:
this_method_call = _Call((name, args, kwargs))
_new_parent.method_calls.append(this_method_call)
do_method_calls = _new_parent._mock_parent is not None do_method_calls = _new_parent._mock_parent is not None
if do_method_calls: if do_method_calls:
name = _new_parent._mock_name + '.' + name method_call_name = _new_parent._mock_name + '.' + method_call_name
# handle mock_calls:
this_mock_call = _Call((mock_call_name, args, kwargs))
_new_parent.mock_calls.append(this_mock_call) _new_parent.mock_calls.append(this_mock_call)
if _new_parent._mock_new_name:
if is_a_call:
dot = ''
else:
dot = '.'
is_a_call = _new_parent._mock_new_name == '()'
mock_call_name = _new_parent._mock_new_name + dot + mock_call_name
# follow the parental chain:
_new_parent = _new_parent._mock_new_parent _new_parent = _new_parent._mock_new_parent
# use ids here so as not to call __hash__ on the mocks # check we're not in an infinite loop:
# ( use ids here so as not to call __hash__ on the mocks)
_new_parent_id = id(_new_parent) _new_parent_id = id(_new_parent)
if _new_parent_id in seen: if _new_parent_id in seen:
break break
...@@ -2054,6 +2059,10 @@ class _Call(tuple): ...@@ -2054,6 +2059,10 @@ class _Call(tuple):
else: else:
self_name, self_args, self_kwargs = self self_name, self_args, self_kwargs = self
if (getattr(self, 'parent', None) and getattr(other, 'parent', None)
and self.parent != other.parent):
return False
other_name = '' other_name = ''
if len_other == 0: if len_other == 0:
other_args, other_kwargs = (), {} other_args, other_kwargs = (), {}
......
...@@ -270,6 +270,22 @@ class CallTest(unittest.TestCase): ...@@ -270,6 +270,22 @@ class CallTest(unittest.TestCase):
self.assertEqual(mock.mock_calls, last_call.call_list()) self.assertEqual(mock.mock_calls, last_call.call_list())
def test_extended_not_equal(self):
a = call(x=1).foo
b = call(x=2).foo
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertNotEqual(a, b)
def test_nested_calls_not_equal(self):
a = call(x=1).foo().bar
b = call(x=2).foo().bar
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertNotEqual(a, b)
def test_call_list(self): def test_call_list(self):
mock = MagicMock() mock = MagicMock()
mock(1) mock(1)
......
...@@ -925,6 +925,57 @@ class MockTest(unittest.TestCase): ...@@ -925,6 +925,57 @@ class MockTest(unittest.TestCase):
call().__int__().call_list()) call().__int__().call_list())
def test_child_mock_call_equal(self):
m = Mock()
result = m()
result.wibble()
# parent looks like this:
self.assertEqual(m.mock_calls, [call(), call().wibble()])
# but child should look like this:
self.assertEqual(result.mock_calls, [call.wibble()])
def test_mock_call_not_equal_leaf(self):
m = Mock()
m.foo().something()
self.assertNotEqual(m.mock_calls[1], call.foo().different())
self.assertEqual(m.mock_calls[0], call.foo())
def test_mock_call_not_equal_non_leaf(self):
m = Mock()
m.foo().bar()
self.assertNotEqual(m.mock_calls[1], call.baz().bar())
self.assertNotEqual(m.mock_calls[0], call.baz())
def test_mock_call_not_equal_non_leaf_params_different(self):
m = Mock()
m.foo(x=1).bar()
# This isn't ideal, but there's no way to fix it without breaking backwards compatibility:
self.assertEqual(m.mock_calls[1], call.foo(x=2).bar())
def test_mock_call_not_equal_non_leaf_attr(self):
m = Mock()
m.foo.bar()
self.assertNotEqual(m.mock_calls[0], call.baz.bar())
def test_mock_call_not_equal_non_leaf_call_versus_attr(self):
m = Mock()
m.foo.bar()
self.assertNotEqual(m.mock_calls[0], call.foo().bar())
def test_mock_call_repr(self):
m = Mock()
m.foo().bar().baz.bob()
self.assertEqual(repr(m.mock_calls[0]), 'call.foo()')
self.assertEqual(repr(m.mock_calls[1]), 'call.foo().bar()')
self.assertEqual(repr(m.mock_calls[2]), 'call.foo().bar().baz.bob()')
def test_subclassing(self): def test_subclassing(self):
class Subclass(Mock): class Subclass(Mock):
pass pass
......
Recursively check arguments when testing for equality of
:class:`unittest.mock.call` objects and add note that tracking of parameters
used to create ancestors of mocks in ``mock_calls`` is not possible.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment