mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-14 01:00:39 +00:00
337 lines
11 KiB
Python
337 lines
11 KiB
Python
from __future__ import unicode_literals
|
|
|
|
import inspect
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from _pytest_mock_version import version
|
|
|
|
__version__ = version
|
|
|
|
# pseudo-six; if this starts to require more than this, depend on six already
|
|
if sys.version_info[0] == 2: # pragma: no cover
|
|
text_type = unicode # noqa
|
|
else:
|
|
text_type = str
|
|
|
|
|
|
def _get_mock_module(config):
|
|
"""
|
|
Import and return the actual "mock" module. By default this is "mock" for Python 2 and
|
|
"unittest.mock" for Python 3, but the user can force to always use "mock" on Python 3 using
|
|
the mock_use_standalone_module ini option.
|
|
"""
|
|
if not hasattr(_get_mock_module, "_module"):
|
|
use_standalone_module = parse_ini_boolean(
|
|
config.getini("mock_use_standalone_module")
|
|
)
|
|
if sys.version_info[0] == 2 or use_standalone_module:
|
|
import mock
|
|
|
|
_get_mock_module._module = mock
|
|
else:
|
|
import unittest.mock
|
|
|
|
_get_mock_module._module = unittest.mock
|
|
|
|
return _get_mock_module._module
|
|
|
|
|
|
class MockFixture(object):
|
|
"""
|
|
Fixture that provides the same interface to functions in the mock module,
|
|
ensuring that they are uninstalled at the end of each test.
|
|
"""
|
|
|
|
def __init__(self, config):
|
|
self._patches = [] # list of mock._patch objects
|
|
self._mocks = [] # list of MagicMock objects
|
|
self.mock_module = mock_module = _get_mock_module(config)
|
|
self.patch = self._Patcher(self._patches, self._mocks, mock_module)
|
|
# aliases for convenience
|
|
self.Mock = mock_module.Mock
|
|
self.MagicMock = mock_module.MagicMock
|
|
self.NonCallableMock = mock_module.NonCallableMock
|
|
self.PropertyMock = mock_module.PropertyMock
|
|
self.call = mock_module.call
|
|
self.ANY = mock_module.ANY
|
|
self.DEFAULT = mock_module.DEFAULT
|
|
self.create_autospec = mock_module.create_autospec
|
|
self.sentinel = mock_module.sentinel
|
|
self.mock_open = mock_module.mock_open
|
|
|
|
def resetall(self):
|
|
"""
|
|
Call reset_mock() on all patchers started by this fixture.
|
|
"""
|
|
for m in self._mocks:
|
|
m.reset_mock()
|
|
|
|
def stopall(self):
|
|
"""
|
|
Stop all patchers started by this fixture. Can be safely called multiple
|
|
times.
|
|
"""
|
|
for p in reversed(self._patches):
|
|
p.stop()
|
|
self._patches[:] = []
|
|
self._mocks[:] = []
|
|
|
|
def spy(self, obj, name):
|
|
"""
|
|
Creates a spy of method. It will run method normally, but it is now
|
|
possible to use `mock` call features with it, like call count.
|
|
|
|
:param object obj: An object.
|
|
:param unicode name: A method in object.
|
|
:rtype: mock.MagicMock
|
|
:return: Spy object.
|
|
"""
|
|
method = getattr(obj, name)
|
|
|
|
autospec = inspect.ismethod(method) or inspect.isfunction(method)
|
|
# Can't use autospec classmethod or staticmethod objects
|
|
# see: https://bugs.python.org/issue23078
|
|
if inspect.isclass(obj):
|
|
# Bypass class descriptor:
|
|
# http://stackoverflow.com/questions/14187973/python3-check-if-method-is-static
|
|
try:
|
|
value = obj.__getattribute__(obj, name)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if isinstance(value, (classmethod, staticmethod)):
|
|
autospec = False
|
|
|
|
result = self.patch.object(obj, name, side_effect=method, autospec=autospec)
|
|
return result
|
|
|
|
def stub(self, name=None):
|
|
"""
|
|
Creates a stub method. It accepts any arguments. Ideal to register to
|
|
callbacks in tests.
|
|
|
|
:param name: the constructed stub's name as used in repr
|
|
:rtype: mock.MagicMock
|
|
:return: Stub object.
|
|
"""
|
|
return self.mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name)
|
|
|
|
class _Patcher(object):
|
|
"""
|
|
Object to provide the same interface as mock.patch, mock.patch.object,
|
|
etc. We need this indirection to keep the same API of the mock package.
|
|
"""
|
|
|
|
def __init__(self, patches, mocks, mock_module):
|
|
self._patches = patches
|
|
self._mocks = mocks
|
|
self.mock_module = mock_module
|
|
|
|
def _start_patch(self, mock_func, *args, **kwargs):
|
|
"""Patches something by calling the given function from the mock
|
|
module, registering the patch to stop it later and returns the
|
|
mock object resulting from the mock call.
|
|
"""
|
|
p = mock_func(*args, **kwargs)
|
|
mocked = p.start()
|
|
self._patches.append(p)
|
|
if hasattr(mocked, "reset_mock"):
|
|
self._mocks.append(mocked)
|
|
return mocked
|
|
|
|
def object(self, *args, **kwargs):
|
|
"""API to mock.patch.object"""
|
|
return self._start_patch(self.mock_module.patch.object, *args, **kwargs)
|
|
|
|
def multiple(self, *args, **kwargs):
|
|
"""API to mock.patch.multiple"""
|
|
return self._start_patch(self.mock_module.patch.multiple, *args, **kwargs)
|
|
|
|
def dict(self, *args, **kwargs):
|
|
"""API to mock.patch.dict"""
|
|
return self._start_patch(self.mock_module.patch.dict, *args, **kwargs)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
"""API to mock.patch"""
|
|
return self._start_patch(self.mock_module.patch, *args, **kwargs)
|
|
|
|
|
|
@pytest.yield_fixture
|
|
def mocker(pytestconfig):
|
|
"""
|
|
return an object that has the same interface to the `mock` module, but
|
|
takes care of automatically undoing all patches after each test method.
|
|
"""
|
|
result = MockFixture(pytestconfig)
|
|
yield result
|
|
result.stopall()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock(mocker):
|
|
"""
|
|
Same as "mocker", but kept only for backward compatibility.
|
|
"""
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
'"mock" fixture has been deprecated, use "mocker" instead', DeprecationWarning
|
|
)
|
|
return mocker
|
|
|
|
|
|
_mock_module_patches = []
|
|
_mock_module_originals = {}
|
|
|
|
|
|
def assert_wrapper(__wrapped_mock_method__, *args, **kwargs):
|
|
__tracebackhide__ = True
|
|
try:
|
|
__wrapped_mock_method__(*args, **kwargs)
|
|
return
|
|
except AssertionError as e:
|
|
if getattr(e, "_mock_introspection_applied", 0):
|
|
msg = text_type(e)
|
|
else:
|
|
__mock_self = args[0]
|
|
msg = text_type(e)
|
|
if __mock_self.call_args is not None:
|
|
actual_args, actual_kwargs = __mock_self.call_args
|
|
msg += "\n\npytest introspection follows:\n"
|
|
try:
|
|
assert actual_args == args[1:]
|
|
except AssertionError as e:
|
|
msg += "\nArgs:\n" + text_type(e)
|
|
try:
|
|
assert actual_kwargs == kwargs
|
|
except AssertionError as e:
|
|
msg += "\nKwargs:\n" + text_type(e)
|
|
e = AssertionError(msg)
|
|
e._mock_introspection_applied = True
|
|
raise e
|
|
|
|
|
|
def wrap_assert_not_called(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_not_called"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_called_with(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_called_with"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_called_once(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_called_once"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_called_once_with(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_called_once_with"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_has_calls(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_has_calls"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_any_call(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_any_call"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_called(*args, **kwargs):
|
|
__tracebackhide__ = True
|
|
assert_wrapper(_mock_module_originals["assert_called"], *args, **kwargs)
|
|
|
|
|
|
def wrap_assert_methods(config):
|
|
"""
|
|
Wrap assert methods of mock module so we can hide their traceback and
|
|
add introspection information to specified argument asserts.
|
|
"""
|
|
# Make sure we only do this once
|
|
if _mock_module_originals:
|
|
return
|
|
|
|
mock_module = _get_mock_module(config)
|
|
|
|
wrappers = {
|
|
"assert_called": wrap_assert_called,
|
|
"assert_called_once": wrap_assert_called_once,
|
|
"assert_called_with": wrap_assert_called_with,
|
|
"assert_called_once_with": wrap_assert_called_once_with,
|
|
"assert_any_call": wrap_assert_any_call,
|
|
"assert_has_calls": wrap_assert_has_calls,
|
|
"assert_not_called": wrap_assert_not_called,
|
|
}
|
|
for method, wrapper in wrappers.items():
|
|
try:
|
|
original = getattr(mock_module.NonCallableMock, method)
|
|
except AttributeError: # pragma: no cover
|
|
continue
|
|
_mock_module_originals[method] = original
|
|
patcher = mock_module.patch.object(mock_module.NonCallableMock, method, wrapper)
|
|
patcher.start()
|
|
_mock_module_patches.append(patcher)
|
|
|
|
if hasattr(config, "add_cleanup"):
|
|
add_cleanup = config.add_cleanup
|
|
else:
|
|
# pytest 2.7 compatibility
|
|
add_cleanup = config._cleanup.append
|
|
add_cleanup(unwrap_assert_methods)
|
|
|
|
|
|
def unwrap_assert_methods():
|
|
for patcher in _mock_module_patches:
|
|
try:
|
|
patcher.stop()
|
|
except RuntimeError as e:
|
|
# a patcher might have been stopped by user code (#137)
|
|
# so we need to catch this error here and ignore it;
|
|
# unfortunately there's no public API to check if a patch
|
|
# has been started, so catching the error it is
|
|
if text_type(e) == "stop called on unstarted patcher":
|
|
pass
|
|
else:
|
|
raise
|
|
_mock_module_patches[:] = []
|
|
_mock_module_originals.clear()
|
|
|
|
|
|
def pytest_addoption(parser):
|
|
parser.addini(
|
|
"mock_traceback_monkeypatch",
|
|
"Monkeypatch the mock library to improve reporting of the "
|
|
"assert_called_... methods",
|
|
default=True,
|
|
)
|
|
parser.addini(
|
|
"mock_use_standalone_module",
|
|
'Use standalone "mock" (from PyPI) instead of builtin "unittest.mock" '
|
|
"on Python 3",
|
|
default=False,
|
|
)
|
|
|
|
|
|
def parse_ini_boolean(value):
|
|
if value in (True, False):
|
|
return value
|
|
try:
|
|
return {"true": True, "false": False}[value.lower()]
|
|
except KeyError:
|
|
raise ValueError("unknown string for bool: %r" % value)
|
|
|
|
|
|
def pytest_configure(config):
|
|
tb = config.getoption("--tb")
|
|
if (
|
|
parse_ini_boolean(config.getini("mock_traceback_monkeypatch"))
|
|
and tb != "native"
|
|
):
|
|
wrap_assert_methods(config)
|