diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 1d52555..74c90b2 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -467,6 +467,44 @@ def assert_wrapper( raise e +def assert_has_calls_wrapper( + __wrapped_mock_method__: Callable[..., Any], *args: Any, **kwargs: Any +) -> None: + __tracebackhide__ = True + try: + __wrapped_mock_method__(*args, **kwargs) + return + except AssertionError as e: + any_order = kwargs.get("any_order", False) + if getattr(e, "_mock_introspection_applied", 0) or any_order: + msg = str(e) + else: + __mock_self = args[0] + msg = str(e) + if __mock_self.call_args_list is not None: + actual_calls = list(__mock_self.call_args_list) + expect_calls = args[1] + introspection = "" + from itertools import zip_longest + + for actual_call, expect_call in zip_longest(actual_calls, expect_calls): + actual_args, actual_kwargs = actual_call + _, expect_args, expect_kwargs = expect_call + try: + assert actual_args == expect_args + except AssertionError as e_args: + introspection += "\nArgs:\n" + str(e_args) + try: + assert actual_kwargs == expect_kwargs + except AssertionError as e_kwargs: + introspection += "\nKwargs:\n" + str(e_kwargs) + if introspection: + msg += "\n\npytest introspection follows:\n" + introspection + e = AssertionError(msg) + e._mock_introspection_applied = True # type:ignore[attr-defined] + raise e + + def wrap_assert_not_called(*args: Any, **kwargs: Any) -> None: __tracebackhide__ = True assert_wrapper(_mock_module_originals["assert_not_called"], *args, **kwargs) @@ -489,7 +527,9 @@ def wrap_assert_called_once_with(*args: Any, **kwargs: Any) -> None: def wrap_assert_has_calls(*args: Any, **kwargs: Any) -> None: __tracebackhide__ = True - assert_wrapper(_mock_module_originals["assert_has_calls"], *args, **kwargs) + assert_has_calls_wrapper( + _mock_module_originals["assert_has_calls"], *args, **kwargs + ) def wrap_assert_any_call(*args: Any, **kwargs: Any) -> None: