mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-14 09:10:39 +00:00
251 lines
9.1 KiB
Python
251 lines
9.1 KiB
Python
""" recording warnings during test function execution. """
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import inspect
|
|
import re
|
|
import sys
|
|
import warnings
|
|
|
|
import six
|
|
|
|
import _pytest._code
|
|
from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS
|
|
from _pytest.deprecated import WARNS_EXEC
|
|
from _pytest.fixtures import yield_fixture
|
|
from _pytest.outcomes import fail
|
|
|
|
|
|
@yield_fixture
|
|
def recwarn():
|
|
"""Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
|
|
|
See http://docs.python.org/library/warnings.html for information
|
|
on warning categories.
|
|
"""
|
|
wrec = WarningsRecorder()
|
|
with wrec:
|
|
warnings.simplefilter("default")
|
|
yield wrec
|
|
|
|
|
|
def deprecated_call(func=None, *args, **kwargs):
|
|
"""context manager that can be used to ensure a block of code triggers a
|
|
``DeprecationWarning`` or ``PendingDeprecationWarning``::
|
|
|
|
>>> import warnings
|
|
>>> def api_call_v2():
|
|
... warnings.warn('use v3 of this api', DeprecationWarning)
|
|
... return 200
|
|
|
|
>>> with deprecated_call():
|
|
... assert api_call_v2() == 200
|
|
|
|
``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``,
|
|
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
|
|
types above.
|
|
"""
|
|
__tracebackhide__ = True
|
|
if func is not None:
|
|
args = (func,) + args
|
|
return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
|
|
|
|
|
|
def warns(expected_warning, *args, **kwargs):
|
|
r"""Assert that code raises a particular class of warning.
|
|
|
|
Specifically, the parameter ``expected_warning`` can be a warning class or
|
|
sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or
|
|
classes.
|
|
|
|
This helper produces a list of :class:`warnings.WarningMessage` objects,
|
|
one for each warning raised.
|
|
|
|
This function can be used as a context manager, or any of the other ways
|
|
``pytest.raises`` can be used::
|
|
|
|
>>> with warns(RuntimeWarning):
|
|
... warnings.warn("my warning", RuntimeWarning)
|
|
|
|
In the context manager form you may use the keyword argument ``match`` to assert
|
|
that the exception matches a text or regex::
|
|
|
|
>>> with warns(UserWarning, match='must be 0 or None'):
|
|
... warnings.warn("value must be 0 or None", UserWarning)
|
|
|
|
>>> with warns(UserWarning, match=r'must be \d+$'):
|
|
... warnings.warn("value must be 42", UserWarning)
|
|
|
|
>>> with warns(UserWarning, match=r'must be \d+$'):
|
|
... warnings.warn("this is not here", UserWarning)
|
|
Traceback (most recent call last):
|
|
...
|
|
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
|
|
|
"""
|
|
__tracebackhide__ = True
|
|
if not args:
|
|
match_expr = kwargs.pop("match", None)
|
|
if kwargs:
|
|
warnings.warn(
|
|
PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2
|
|
)
|
|
return WarningsChecker(expected_warning, match_expr=match_expr)
|
|
elif isinstance(args[0], str):
|
|
warnings.warn(WARNS_EXEC, stacklevel=2)
|
|
code, = args
|
|
assert isinstance(code, str)
|
|
frame = sys._getframe(1)
|
|
loc = frame.f_locals.copy()
|
|
loc.update(kwargs)
|
|
|
|
with WarningsChecker(expected_warning):
|
|
code = _pytest._code.Source(code).compile()
|
|
six.exec_(code, frame.f_globals, loc)
|
|
else:
|
|
func = args[0]
|
|
with WarningsChecker(expected_warning):
|
|
return func(*args[1:], **kwargs)
|
|
|
|
|
|
class WarningsRecorder(warnings.catch_warnings):
|
|
"""A context manager to record raised warnings.
|
|
|
|
Adapted from `warnings.catch_warnings`.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super(WarningsRecorder, self).__init__(record=True)
|
|
self._entered = False
|
|
self._list = []
|
|
|
|
@property
|
|
def list(self):
|
|
"""The list of recorded warnings."""
|
|
return self._list
|
|
|
|
def __getitem__(self, i):
|
|
"""Get a recorded warning by index."""
|
|
return self._list[i]
|
|
|
|
def __iter__(self):
|
|
"""Iterate through the recorded warnings."""
|
|
return iter(self._list)
|
|
|
|
def __len__(self):
|
|
"""The number of recorded warnings."""
|
|
return len(self._list)
|
|
|
|
def pop(self, cls=Warning):
|
|
"""Pop the first recorded warning, raise exception if not exists."""
|
|
for i, w in enumerate(self._list):
|
|
if issubclass(w.category, cls):
|
|
return self._list.pop(i)
|
|
__tracebackhide__ = True
|
|
raise AssertionError("%r not found in warning list" % cls)
|
|
|
|
def clear(self):
|
|
"""Clear the list of recorded warnings."""
|
|
self._list[:] = []
|
|
|
|
def __enter__(self):
|
|
if self._entered:
|
|
__tracebackhide__ = True
|
|
raise RuntimeError("Cannot enter %r twice" % self)
|
|
self._list = super(WarningsRecorder, self).__enter__()
|
|
warnings.simplefilter("always")
|
|
# python3 keeps track of a "filter version", when the filters are
|
|
# updated previously seen warnings can be re-warned. python2 has no
|
|
# concept of this so we must reset the warnings registry manually.
|
|
# trivial patching of `warnings.warn` seems to be enough somehow?
|
|
if six.PY2:
|
|
|
|
def warn(message, category=None, stacklevel=1):
|
|
# duplicate the stdlib logic due to
|
|
# bad handing in the c version of warnings
|
|
if isinstance(message, Warning):
|
|
category = message.__class__
|
|
# Check category argument
|
|
if category is None:
|
|
category = UserWarning
|
|
assert issubclass(category, Warning)
|
|
|
|
# emulate resetting the warn registry
|
|
f_globals = sys._getframe(stacklevel).f_globals
|
|
if "__warningregistry__" in f_globals:
|
|
orig = f_globals["__warningregistry__"]
|
|
f_globals["__warningregistry__"] = None
|
|
try:
|
|
return self._saved_warn(message, category, stacklevel + 1)
|
|
finally:
|
|
f_globals["__warningregistry__"] = orig
|
|
else:
|
|
return self._saved_warn(message, category, stacklevel + 1)
|
|
|
|
warnings.warn, self._saved_warn = warn, warnings.warn
|
|
return self
|
|
|
|
def __exit__(self, *exc_info):
|
|
if not self._entered:
|
|
__tracebackhide__ = True
|
|
raise RuntimeError("Cannot exit %r without entering first" % self)
|
|
# see above where `self._saved_warn` is assigned
|
|
if six.PY2:
|
|
warnings.warn = self._saved_warn
|
|
super(WarningsRecorder, self).__exit__(*exc_info)
|
|
|
|
# Built-in catch_warnings does not reset entered state so we do it
|
|
# manually here for this context manager to become reusable.
|
|
self._entered = False
|
|
|
|
|
|
class WarningsChecker(WarningsRecorder):
|
|
def __init__(self, expected_warning=None, match_expr=None):
|
|
super(WarningsChecker, self).__init__()
|
|
|
|
msg = "exceptions must be old-style classes or derived from Warning, not %s"
|
|
if isinstance(expected_warning, tuple):
|
|
for exc in expected_warning:
|
|
if not inspect.isclass(exc):
|
|
raise TypeError(msg % type(exc))
|
|
elif inspect.isclass(expected_warning):
|
|
expected_warning = (expected_warning,)
|
|
elif expected_warning is not None:
|
|
raise TypeError(msg % type(expected_warning))
|
|
|
|
self.expected_warning = expected_warning
|
|
self.match_expr = match_expr
|
|
|
|
def __exit__(self, *exc_info):
|
|
super(WarningsChecker, self).__exit__(*exc_info)
|
|
|
|
__tracebackhide__ = True
|
|
|
|
# only check if we're not currently handling an exception
|
|
if all(a is None for a in exc_info):
|
|
if self.expected_warning is not None:
|
|
if not any(issubclass(r.category, self.expected_warning) for r in self):
|
|
__tracebackhide__ = True
|
|
fail(
|
|
"DID NOT WARN. No warnings of type {} was emitted. "
|
|
"The list of emitted warnings is: {}.".format(
|
|
self.expected_warning, [each.message for each in self]
|
|
)
|
|
)
|
|
elif self.match_expr is not None:
|
|
for r in self:
|
|
if issubclass(r.category, self.expected_warning):
|
|
if re.compile(self.match_expr).search(str(r.message)):
|
|
break
|
|
else:
|
|
fail(
|
|
"DID NOT WARN. No warnings of type {} matching"
|
|
" ('{}') was emitted. The list of emitted warnings"
|
|
" is: {}.".format(
|
|
self.expected_warning,
|
|
self.match_expr,
|
|
[each.message for each in self],
|
|
)
|
|
)
|