Add matchers for detecting emitted warnings.
This commit is contained in:
committed by
Thomi Richards
parent
45a0a15924
commit
8a69c2c536
1
LICENSE
1
LICENSE
@@ -20,6 +20,7 @@ The testtools authors are:
|
||||
* Nikola Đipanov
|
||||
* Tristan Seligmann
|
||||
* Julian Edwards
|
||||
* Jonathan Jacobs
|
||||
|
||||
and are collectively referred to as "testtools developers".
|
||||
|
||||
|
||||
3
NEWS
3
NEWS
@@ -30,6 +30,9 @@ Improvements
|
||||
This had the side effect of not clearing up fixtures nor gathering details
|
||||
properly. This is now fixed. (Julian Edwards, #1469759)
|
||||
|
||||
* New ``Warnings`` matcher, and ``WarningMessage`` and ``IsDeprecated``
|
||||
functions for matching emitted warnings. (Jonathan Jacobs, Github #223)
|
||||
|
||||
2.0.0
|
||||
~~~~~
|
||||
|
||||
|
||||
@@ -92,3 +92,4 @@ Thanks
|
||||
* Julia Varlamova
|
||||
* ClusterHQ Ltd
|
||||
* Tristan Seligmann
|
||||
* Jonathan Jacobs
|
||||
|
||||
@@ -392,6 +392,25 @@ This is actually a convenience function that combines two other matchers:
|
||||
Raises_ and MatchesException_.
|
||||
|
||||
|
||||
The IsDeprecated helper
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Matches if a callable produces a warning whose message matches the specified
|
||||
matcher. For example::
|
||||
|
||||
def old_func(x):
|
||||
warnings.warn('old_func is deprecated use new_func instead')
|
||||
return new_func(x)
|
||||
|
||||
def test_warning_example(self):
|
||||
self.assertThat(
|
||||
lambda: old_func(42),
|
||||
IsDeprecated(Contains('old_func is deprecated')))
|
||||
|
||||
This is just a convenience function that combines Warnings_ and
|
||||
`WarningMessage`_.
|
||||
|
||||
|
||||
DocTestMatches
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -469,6 +488,13 @@ example::
|
||||
Most of the time, you will want to uses `The raises helper`_ instead.
|
||||
|
||||
|
||||
WarningMessage
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Match against various attributes (category, message and filename to name a few) of
|
||||
a `warning.WarningMessage`, which can be captured by `Warnings`_.
|
||||
|
||||
|
||||
NotEquals
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -911,6 +937,28 @@ Although note that this could also be written as::
|
||||
See also MatchesException_ and `the raises helper`_
|
||||
|
||||
|
||||
Warnings
|
||||
~~~~~~~~
|
||||
|
||||
Captures all warnings produced by a callable as a list of
|
||||
`warning.WarningMessage` and matches against it. For example, if you want to
|
||||
assert that a callable emits exactly one warning::
|
||||
|
||||
def soon_to_be_old_func():
|
||||
warnings.warn('soon_to_be_old_func will be deprecated next version',
|
||||
PendingDeprecationWarning, 2)
|
||||
|
||||
def test_warnings_example(self):
|
||||
self.assertThat(
|
||||
soon_to_be_old_func,
|
||||
Warnings(HasLength(1)))
|
||||
|
||||
Deprecating something and making an assertion about the deprecation message is a
|
||||
very common test with which `the IsDeprecated helper`_ can assist. For making
|
||||
more specific matches against warnings `WarningMessage`_ can construct a
|
||||
matcher that can be combined with `Warnings`_.
|
||||
|
||||
|
||||
Writing your own matchers
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ __all__ = [
|
||||
'HasLength',
|
||||
'HasPermissions',
|
||||
'Is',
|
||||
'IsDeprecated',
|
||||
'IsInstance',
|
||||
'KeysEqual',
|
||||
'LessThan',
|
||||
@@ -55,6 +56,8 @@ __all__ = [
|
||||
'SamePath',
|
||||
'StartsWith',
|
||||
'TarballContains',
|
||||
'Warnings',
|
||||
'WarningMessage'
|
||||
]
|
||||
|
||||
from ._basic import (
|
||||
@@ -115,6 +118,11 @@ from ._higherorder import (
|
||||
MatchesPredicateWithParams,
|
||||
Not,
|
||||
)
|
||||
from ._warnings import (
|
||||
IsDeprecated,
|
||||
WarningMessage,
|
||||
Warnings,
|
||||
)
|
||||
|
||||
# XXX: These are not explicitly included in __all__. It's unclear how much of
|
||||
# the public interface they really are.
|
||||
|
||||
109
testtools/matchers/_warnings.py
Normal file
109
testtools/matchers/_warnings.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2009-2016 testtools developers. See LICENSE for details.
|
||||
|
||||
__all__ = [
|
||||
'Warnings',
|
||||
'WarningMessage',
|
||||
'IsDeprecated']
|
||||
|
||||
import warnings
|
||||
|
||||
from ._basic import Is
|
||||
from ._const import Always
|
||||
from ._datastructures import MatchesListwise, MatchesStructure
|
||||
from ._higherorder import (
|
||||
AfterPreprocessing,
|
||||
Annotate,
|
||||
MatchesAll,
|
||||
Not,
|
||||
)
|
||||
from ._impl import Mismatch
|
||||
|
||||
|
||||
def WarningMessage(category_type, message=None, filename=None, lineno=None,
|
||||
line=None):
|
||||
"""
|
||||
Create a matcher that will match `warnings.WarningMessage`\s.
|
||||
|
||||
For example, to match captured `DeprecationWarning`\s with a message about
|
||||
some ``foo`` being replaced with ``bar``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
WarningMessage(DeprecationWarning,
|
||||
message=MatchesAll(
|
||||
Contains('foo is deprecated'),
|
||||
Contains('use bar instead')))
|
||||
|
||||
:param type category_type: A warning type, for example
|
||||
`DeprecationWarning`.
|
||||
:param message_matcher: A matcher object that will be evaluated against
|
||||
warning's message.
|
||||
:param filename_matcher: A matcher object that will be evaluated against
|
||||
the warning's filename.
|
||||
:param lineno_matcher: A matcher object that will be evaluated against the
|
||||
warning's line number.
|
||||
:param line_matcher: A matcher object that will be evaluated against the
|
||||
warning's line of source code.
|
||||
"""
|
||||
category_matcher = Is(category_type)
|
||||
message_matcher = message or Always()
|
||||
filename_matcher = filename or Always()
|
||||
lineno_matcher = lineno or Always()
|
||||
line_matcher = line or Always()
|
||||
return MatchesStructure(
|
||||
category=Annotate(
|
||||
"Warning's category type does not match",
|
||||
category_matcher),
|
||||
message=Annotate(
|
||||
"Warning's message does not match",
|
||||
AfterPreprocessing(str, message_matcher)),
|
||||
filename=Annotate(
|
||||
"Warning's filname does not match",
|
||||
filename_matcher),
|
||||
lineno=Annotate(
|
||||
"Warning's line number does not match",
|
||||
lineno_matcher),
|
||||
line=Annotate(
|
||||
"Warning's source line does not match",
|
||||
line_matcher))
|
||||
|
||||
|
||||
class Warnings(object):
|
||||
"""
|
||||
Match if the matchee produces warnings.
|
||||
"""
|
||||
def __init__(self, warnings_matcher=None):
|
||||
"""
|
||||
Create a Warnings matcher.
|
||||
|
||||
:param warnings_matcher: Optional validator for the warnings emitted by
|
||||
matchee. If no warnings_matcher is supplied then the simple fact that
|
||||
at least one warning is emitted is considered enough to match on.
|
||||
"""
|
||||
self.warnings_matcher = warnings_matcher
|
||||
|
||||
def match(self, matchee):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter('always')
|
||||
matchee()
|
||||
if self.warnings_matcher is not None:
|
||||
return self.warnings_matcher.match(w)
|
||||
elif not w:
|
||||
return Mismatch('Expected at least one warning, got none')
|
||||
|
||||
def __str__(self):
|
||||
return 'Warnings({!s})'.format(self.warnings_matcher)
|
||||
|
||||
|
||||
def IsDeprecated(message):
|
||||
"""
|
||||
Make a matcher that checks that a callable produces exactly one
|
||||
`DeprecationWarning`.
|
||||
|
||||
:param message: Matcher for the warning message.
|
||||
"""
|
||||
return Warnings(
|
||||
MatchesListwise([
|
||||
WarningMessage(
|
||||
category_type=DeprecationWarning,
|
||||
message=message)]))
|
||||
@@ -15,6 +15,7 @@ def test_suite():
|
||||
test_filesystem,
|
||||
test_higherorder,
|
||||
test_impl,
|
||||
test_warnings
|
||||
)
|
||||
modules = [
|
||||
test_basic,
|
||||
@@ -26,6 +27,7 @@ def test_suite():
|
||||
test_filesystem,
|
||||
test_higherorder,
|
||||
test_impl,
|
||||
test_warnings
|
||||
]
|
||||
suites = map(lambda x: x.test_suite(), modules)
|
||||
return TestSuite(suites)
|
||||
|
||||
203
testtools/tests/matchers/test_warnings.py
Normal file
203
testtools/tests/matchers/test_warnings.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Copyright (c) 2008-2016 testtools developers. See LICENSE for details.
|
||||
|
||||
import warnings
|
||||
|
||||
from testtools import TestCase
|
||||
from testtools.matchers import (
|
||||
AfterPreprocessing,
|
||||
Equals,
|
||||
MatchesStructure,
|
||||
MatchesListwise,
|
||||
Contains,
|
||||
HasLength,
|
||||
)
|
||||
from testtools.matchers._warnings import Warnings, IsDeprecated, WarningMessage
|
||||
from testtools.tests.helpers import FullStackRunTest
|
||||
from testtools.tests.matchers.helpers import TestMatchersInterface
|
||||
|
||||
|
||||
def make_warning(warning_type, message):
|
||||
warnings.warn(message, warning_type, 2)
|
||||
|
||||
|
||||
def make_warning_message(message, category, filename=None, lineno=None, line=None):
|
||||
return warnings.WarningMessage(
|
||||
message=message,
|
||||
category=category,
|
||||
filename=filename,
|
||||
lineno=lineno,
|
||||
line=line)
|
||||
|
||||
|
||||
class TestWarningMessageCategoryTypeInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.WarningMessage`.
|
||||
|
||||
In particular matching the ``category_type``.
|
||||
"""
|
||||
matches_matcher = WarningMessage(category_type=DeprecationWarning)
|
||||
warning_foo = make_warning_message('foo', DeprecationWarning)
|
||||
warning_bar = make_warning_message('bar', SyntaxWarning)
|
||||
warning_base = make_warning_message('base', Warning)
|
||||
matches_matches = [warning_foo]
|
||||
matches_mismatches = [warning_bar, warning_base]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningMessageMessageInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.WarningMessage`.
|
||||
|
||||
In particular matching the ``message``.
|
||||
"""
|
||||
matches_matcher = WarningMessage(category_type=DeprecationWarning,
|
||||
message=Equals('foo'))
|
||||
warning_foo = make_warning_message('foo', DeprecationWarning)
|
||||
warning_bar = make_warning_message('bar', DeprecationWarning)
|
||||
matches_matches = [warning_foo]
|
||||
matches_mismatches = [warning_bar]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningMessageFilenameInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.WarningMessage`.
|
||||
|
||||
In particular matching the ``filename``.
|
||||
"""
|
||||
matches_matcher = WarningMessage(category_type=DeprecationWarning,
|
||||
filename=Equals('a'))
|
||||
warning_foo = make_warning_message('foo', DeprecationWarning, filename='a')
|
||||
warning_bar = make_warning_message('bar', DeprecationWarning, filename='b')
|
||||
matches_matches = [warning_foo]
|
||||
matches_mismatches = [warning_bar]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningMessageLineNumberInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.WarningMessage`.
|
||||
|
||||
In particular matching the ``lineno``.
|
||||
"""
|
||||
matches_matcher = WarningMessage(category_type=DeprecationWarning,
|
||||
lineno=Equals(42))
|
||||
warning_foo = make_warning_message('foo', DeprecationWarning, lineno=42)
|
||||
warning_bar = make_warning_message('bar', DeprecationWarning, lineno=21)
|
||||
matches_matches = [warning_foo]
|
||||
matches_mismatches = [warning_bar]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningMessageLineInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.WarningMessage`.
|
||||
|
||||
In particular matching the ``line``.
|
||||
"""
|
||||
matches_matcher = WarningMessage(category_type=DeprecationWarning,
|
||||
line=Equals('x'))
|
||||
warning_foo = make_warning_message('foo', DeprecationWarning, line='x')
|
||||
warning_bar = make_warning_message('bar', DeprecationWarning, line='y')
|
||||
matches_matches = [warning_foo]
|
||||
matches_mismatches = [warning_bar]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningsInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.Warnings`.
|
||||
|
||||
Specifically without the optional argument.
|
||||
"""
|
||||
matches_matcher = Warnings()
|
||||
def old_func():
|
||||
warnings.warn('old_func is deprecated', DeprecationWarning, 2)
|
||||
matches_matches = [old_func]
|
||||
matches_mismatches = [lambda: None]
|
||||
|
||||
# Tricky to get function objects to render constantly, and the interfaces
|
||||
# helper uses assertEqual rather than (for instance) DocTestMatches.
|
||||
str_examples = []
|
||||
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningsMatcherInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.Warnings`.
|
||||
|
||||
Specifically with the optional matcher argument.
|
||||
"""
|
||||
matches_matcher = Warnings(
|
||||
warnings_matcher=MatchesListwise([
|
||||
MatchesStructure(
|
||||
message=AfterPreprocessing(
|
||||
str, Contains('old_func')))]))
|
||||
def old_func():
|
||||
warnings.warn('old_func is deprecated', DeprecationWarning, 2)
|
||||
def older_func():
|
||||
warnings.warn('older_func is deprecated', DeprecationWarning, 2)
|
||||
matches_matches = [old_func]
|
||||
matches_mismatches = [lambda:None, older_func]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningsMatcherNoWarningsInterface(TestCase, TestMatchersInterface):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.Warnings`.
|
||||
|
||||
Specifically with the optional matcher argument matching that there were no
|
||||
warnings.
|
||||
"""
|
||||
matches_matcher = Warnings(warnings_matcher=HasLength(0))
|
||||
def nowarning_func():
|
||||
pass
|
||||
def warning_func():
|
||||
warnings.warn('warning_func is deprecated', DeprecationWarning, 2)
|
||||
matches_matches = [nowarning_func]
|
||||
matches_mismatches = [warning_func]
|
||||
|
||||
str_examples = []
|
||||
describe_examples = []
|
||||
|
||||
|
||||
class TestWarningMessage(TestCase):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.WarningMessage`.
|
||||
"""
|
||||
run_tests_with = FullStackRunTest
|
||||
|
||||
def test_category(self):
|
||||
def old_func():
|
||||
warnings.warn('old_func is deprecated', DeprecationWarning, 2)
|
||||
self.assertThat(old_func, IsDeprecated(Contains('old_func')))
|
||||
|
||||
|
||||
class TestIsDeprecated(TestCase):
|
||||
"""
|
||||
Tests for `testtools.matchers._warnings.IsDeprecated`.
|
||||
"""
|
||||
run_tests_with = FullStackRunTest
|
||||
|
||||
def test_warning(self):
|
||||
def old_func():
|
||||
warnings.warn('old_func is deprecated', DeprecationWarning, 2)
|
||||
self.assertThat(old_func, IsDeprecated(Contains('old_func')))
|
||||
|
||||
|
||||
def test_suite():
|
||||
from unittest import TestLoader
|
||||
return TestLoader().loadTestsFromName(__name__)
|
||||
Reference in New Issue
Block a user