Add matchers for detecting emitted warnings.

This commit is contained in:
Jonathan Jacobs
2016-03-12 23:39:04 +02:00
committed by Thomi Richards
parent 45a0a15924
commit 8a69c2c536
8 changed files with 375 additions and 0 deletions

View File

@@ -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
View File

@@ -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
~~~~~

View File

@@ -92,3 +92,4 @@ Thanks
* Julia Varlamova
* ClusterHQ Ltd
* Tristan Seligmann
* Jonathan Jacobs

View File

@@ -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
-------------------------

View File

@@ -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.

View 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)]))

View File

@@ -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)

View 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__)