Commit b1304938 authored by Nick Coghlan's avatar Nick Coghlan

Make test.test_support.catch_warnings more robust as discussed on python-dev....

Make test.test_support.catch_warnings more robust as discussed on python-dev. Also add explicit tests for it to test_warnings. (forward port of r64910 from trunk)
parent 628b1b36
...@@ -211,7 +211,7 @@ This module defines the following exceptions: ...@@ -211,7 +211,7 @@ This module defines the following exceptions:
Subclass of :exc:`TestSkipped`. Raised when a resource (such as a network Subclass of :exc:`TestSkipped`. Raised when a resource (such as a network
connection) is not available. Raised by the :func:`requires` function. connection) is not available. Raised by the :func:`requires` function.
The :mod:`test.test_support` module defines the following constants: The :mod:`test.support` module defines the following constants:
.. data:: verbose .. data:: verbose
...@@ -278,20 +278,34 @@ The :mod:`test.support` module defines the following functions: ...@@ -278,20 +278,34 @@ The :mod:`test.support` module defines the following functions:
This will run all tests defined in the named module. This will run all tests defined in the named module.
.. function:: catch_warning(record=True) .. function:: catch_warning(module=warnings, record=True)
Return a context manager that guards the warnings filter from being Return a context manager that guards the warnings filter from being
permanently changed and records the data of the last warning that has been permanently changed and optionally alters the :func:`showwarning`
issued. The ``record`` argument specifies whether any raised warnings are function to record the details of any warnings that are issued in the
captured by the object returned by :func:`warnings.catch_warning` or allowed managed context. Attributes of the most recent warning are saved
to propagate as normal. directly on the context manager, while details of previous warnings
can be retrieved from the ``warnings`` list.
The context manager is typically used like this:: The context manager is used like this::
with catch_warning() as w: with catch_warning() as w:
warnings.simplefilter("always")
warnings.warn("foo") warnings.warn("foo")
assert str(w.message) == "foo" assert str(w.message) == "foo"
warnings.warn("bar")
assert str(w.message) == "bar"
assert str(w.warnings[0].message) == "foo"
assert str(w.warnings[1].message) == "bar"
By default, the real :mod:`warnings` module is affected - the ability
to select a different module is provided for the benefit of the
:mod:`warnings` module's own unit tests.
The ``record`` argument specifies whether or not the :func:`showwarning`
function is replaced. Note that recording the warnings in this fashion
also prevents them from being written to sys.stderr. If set to ``False``,
the standard handling of warning messages is left in place (however, the
original handling is still restored at the end of the block).
.. function:: captured_stdout() .. function:: captured_stdout()
...@@ -331,3 +345,5 @@ The :mod:`test.support` module defines the following classes: ...@@ -331,3 +345,5 @@ The :mod:`test.support` module defines the following classes:
.. method:: EnvironmentVarGuard.unset(envvar) .. method:: EnvironmentVarGuard.unset(envvar)
Temporarily unset the environment variable ``envvar``. Temporarily unset the environment variable ``envvar``.
...@@ -368,36 +368,49 @@ def open_urlresource(url, *args, **kw): ...@@ -368,36 +368,49 @@ def open_urlresource(url, *args, **kw):
class WarningMessage(object): class WarningMessage(object):
"Holds the result of the latest showwarning() call" "Holds the result of a single showwarning() call"
_WARNING_DETAILS = "message category filename lineno line".split()
def __init__(self, message, category, filename, lineno, line=None):
for attr in self._WARNING_DETAILS:
setattr(self, attr, locals()[attr])
self._category_name = category.__name__ if category else None
def __str__(self):
return ("{message : %r, category : %r, filename : %r, lineno : %s, "
"line : %r}" % (self.message, self._category_name,
self.filename, self.lineno, self.line))
class WarningRecorder(object):
"Records the result of any showwarning calls"
def __init__(self): def __init__(self):
self.message = None self.warnings = []
self.category = None self._set_last(None)
self.filename = None
self.lineno = None def _showwarning(self, message, category, filename, lineno,
file=None, line=None):
def _showwarning(self, message, category, filename, lineno, file=None, wm = WarningMessage(message, category, filename, lineno, line)
line=None): self.warnings.append(wm)
self.message = message self._set_last(wm)
self.category = category
self.filename = filename def _set_last(self, last_warning):
self.lineno = lineno if last_warning is None:
self.line = line for attr in WarningMessage._WARNING_DETAILS:
setattr(self, attr, None)
else:
for attr in WarningMessage._WARNING_DETAILS:
setattr(self, attr, getattr(last_warning, attr))
def reset(self): def reset(self):
self._showwarning(*((None,)*6)) self.warnings = []
self._set_last(None)
def __str__(self): def __str__(self):
return ("{message : %r, category : %r, filename : %r, lineno : %s, " return '[%s]' % (', '.join(map(str, self.warnings)))
"line : %r}" % (self.message,
self.category.__name__ if self.category else None,
self.filename, self.lineno, self.line))
@contextlib.contextmanager @contextlib.contextmanager
def catch_warning(module=warnings, record=True): def catch_warning(module=warnings, record=True):
""" """Guard the warnings filter from being permanently changed and
Guard the warnings filter from being permanently changed and record the optionally record the details of any warnings that are issued.
data of the last warning that has been issued.
Use like this: Use like this:
...@@ -405,13 +418,17 @@ def catch_warning(module=warnings, record=True): ...@@ -405,13 +418,17 @@ def catch_warning(module=warnings, record=True):
warnings.warn("foo") warnings.warn("foo")
assert str(w.message) == "foo" assert str(w.message) == "foo"
""" """
original_filters = module.filters[:] original_filters = module.filters
original_showwarning = module.showwarning original_showwarning = module.showwarning
if record: if record:
warning_obj = WarningMessage() recorder = WarningRecorder()
module.showwarning = warning_obj._showwarning module.showwarning = recorder._showwarning
else:
recorder = None
try: try:
yield warning_obj if record else None # Replace the filters with a copy of the original
module.filters = module.filters[:]
yield recorder
finally: finally:
module.showwarning = original_showwarning module.showwarning = original_showwarning
module.filters = original_filters module.filters = original_filters
...@@ -421,7 +438,7 @@ class CleanImport(object): ...@@ -421,7 +438,7 @@ class CleanImport(object):
"""Context manager to force import to return a new module reference. """Context manager to force import to return a new module reference.
This is useful for testing module-level behaviours, such as This is useful for testing module-level behaviours, such as
the emission of a DepreciationWarning on import. the emission of a DeprecationWarning on import.
Use like this: Use like this:
......
...@@ -35,12 +35,9 @@ def with_warning_restore(func): ...@@ -35,12 +35,9 @@ def with_warning_restore(func):
@wraps(func) @wraps(func)
def decorator(*args, **kw): def decorator(*args, **kw):
with catch_warning(): with catch_warning():
# Grrr, we need this function to warn every time. Without removing # We need this function to warn every time, so stick an
# the warningregistry, running test_tarfile then test_struct would fail # unqualifed 'always' at the head of the filter list
# on 64-bit platforms. warnings.simplefilter("always")
globals = func.__globals__
if '__warningregistry__' in globals:
del globals['__warningregistry__']
warnings.filterwarnings("error", category=DeprecationWarning) warnings.filterwarnings("error", category=DeprecationWarning)
return func(*args, **kw) return func(*args, **kw)
return decorator return decorator
...@@ -53,7 +50,7 @@ def deprecated_err(func, *args): ...@@ -53,7 +50,7 @@ def deprecated_err(func, *args):
pass pass
except DeprecationWarning: except DeprecationWarning:
if not PY_STRUCT_OVERFLOW_MASKING: if not PY_STRUCT_OVERFLOW_MASKING:
raise TestFailed("%s%s expected to raise struct.error" % ( raise TestFailed("%s%s expected to raise DeprecationWarning" % (
func.__name__, args)) func.__name__, args))
else: else:
raise TestFailed("%s%s did not raise error" % ( raise TestFailed("%s%s did not raise error" % (
......
...@@ -487,6 +487,47 @@ class CWarningsDisplayTests(BaseTest, WarningsDisplayTests): ...@@ -487,6 +487,47 @@ class CWarningsDisplayTests(BaseTest, WarningsDisplayTests):
class PyWarningsDisplayTests(BaseTest, WarningsDisplayTests): class PyWarningsDisplayTests(BaseTest, WarningsDisplayTests):
module = py_warnings module = py_warnings
class WarningsSupportTests(object):
"""Test the warning tools from test support module"""
def test_catch_warning_restore(self):
wmod = self.module
orig_filters = wmod.filters
orig_showwarning = wmod.showwarning
with support.catch_warning(wmod):
wmod.filters = wmod.showwarning = object()
self.assert_(wmod.filters is orig_filters)
self.assert_(wmod.showwarning is orig_showwarning)
with support.catch_warning(wmod, record=False):
wmod.filters = wmod.showwarning = object()
self.assert_(wmod.filters is orig_filters)
self.assert_(wmod.showwarning is orig_showwarning)
def test_catch_warning_recording(self):
wmod = self.module
with support.catch_warning(wmod) as w:
self.assertEqual(w.warnings, [])
wmod.simplefilter("always")
wmod.warn("foo")
self.assertEqual(str(w.message), "foo")
wmod.warn("bar")
self.assertEqual(str(w.message), "bar")
self.assertEqual(str(w.warnings[0].message), "foo")
self.assertEqual(str(w.warnings[1].message), "bar")
w.reset()
self.assertEqual(w.warnings, [])
orig_showwarning = wmod.showwarning
with support.catch_warning(wmod, record=False) as w:
self.assert_(w is None)
self.assert_(wmod.showwarning is orig_showwarning)
class CWarningsSupportTests(BaseTest, WarningsSupportTests):
module = c_warnings
class PyWarningsSupportTests(BaseTest, WarningsSupportTests):
module = py_warnings
def test_main(): def test_main():
py_warnings.onceregistry.clear() py_warnings.onceregistry.clear()
...@@ -498,6 +539,7 @@ def test_main(): ...@@ -498,6 +539,7 @@ def test_main():
CWCmdLineTests, PyWCmdLineTests, CWCmdLineTests, PyWCmdLineTests,
_WarningsTests, _WarningsTests,
CWarningsDisplayTests, PyWarningsDisplayTests, CWarningsDisplayTests, PyWarningsDisplayTests,
CWarningsSupportTests, PyWarningsSupportTests,
) )
......
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