-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add matcher for exceptions in asyncio future
Example of use ``` assert_that( await resolved(raise_exception()), future_raising(AssertionError)) ) ``` The resolved helper is used to create resolved future objects in async code. It takes a "future like" object and waits for it to complete. Ref #155
- Loading branch information
Showing
3 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import sys | ||
import re | ||
import asyncio | ||
from typing import ( | ||
Optional, | ||
Type, | ||
TypeVar, | ||
Union, | ||
Awaitable, | ||
) | ||
|
||
from hamcrest.core.base_matcher import BaseMatcher | ||
from hamcrest.core.description import Description | ||
from hamcrest.core.matcher import Matcher | ||
|
||
__author__ = "David Keijser" | ||
__copyright__ = "Copyright 2021 hamcrest.org" | ||
__license__ = "BSD, see License.txt" | ||
|
||
T = TypeVar("T") | ||
|
||
if sys.version_info > (3, 9): | ||
# Same as used for asyncio.ensure_future | ||
FutureT = Union[asyncio.Future[T], Awaitable[T]] | ||
else: | ||
# Future is not a parametrised type in earlier version of python | ||
FutureT = Union[asyncio.Future, Awaitable] | ||
|
||
|
||
class FutureRaising(BaseMatcher[asyncio.Future]): | ||
def __init__( | ||
self, | ||
expected: Type[Exception], | ||
pattern: Optional[str] = None, | ||
matching: Optional[Matcher] = None, | ||
) -> None: | ||
self.pattern = pattern | ||
self.matcher = matching | ||
self.expected = expected | ||
|
||
def _matches(self, future: asyncio.Future) -> bool: | ||
if not asyncio.isfuture(future): | ||
return False | ||
|
||
if not future.done(): | ||
return False | ||
|
||
if future.cancelled(): | ||
return False | ||
|
||
exc = future.exception() | ||
if exc is None: | ||
return False | ||
|
||
if isinstance(exc, self.expected): | ||
if self.pattern is not None: | ||
if re.search(self.pattern, str(exc)) is None: | ||
return False | ||
if self.matcher is not None: | ||
if not self.matcher.matches(exc): | ||
return False | ||
return True | ||
|
||
return False | ||
|
||
def describe_to(self, description: Description) -> None: | ||
description.append_text("Expected a completed future with exception %s" % self.expected) | ||
|
||
def describe_mismatch(self, future: asyncio.Future, description: Description) -> None: | ||
if not asyncio.isfuture(future): | ||
description.append_text("%s is not a future" % future) | ||
return | ||
|
||
if not future.done(): | ||
description.append_text("%s is not completed yet" % future) | ||
return | ||
|
||
if future.cancelled(): | ||
description.append_text("%s is cancelled" % future) | ||
return | ||
|
||
exc = future.exception() | ||
if exc is None: | ||
description.append_text("No exception raised.") | ||
elif isinstance(exc, self.expected): | ||
if self.pattern is not None or self.matcher is not None: | ||
description.append_text("Correct assertion type raised, but ") | ||
if self.pattern is not None: | ||
description.append_text('the expected pattern ("%s") ' % self.pattern) | ||
if self.pattern is not None and self.matcher is not None: | ||
description.append_text("and ") | ||
if self.matcher is not None: | ||
description.append_description_of(self.matcher) | ||
description.append_text(" ") | ||
description.append_text('not found. Exception message was: "%s"' % str(exc)) | ||
else: | ||
description.append_text("%r of type %s was raised instead" % (exc, type(exc))) | ||
|
||
def describe_match(self, future: asyncio.Future, match_description: Description) -> None: | ||
exc = future.exception() | ||
match_description.append_text("%r of type %s was raised." % (exc, type(exc))) | ||
|
||
|
||
def future_raising( | ||
exception: Type[Exception], pattern=None, matching=None | ||
) -> Matcher[asyncio.Future]: | ||
"""Matches a future with the expected exception. | ||
:param exception: The class of the expected exception | ||
:param pattern: Optional regular expression to match exception message. | ||
:param matching: Optional Hamcrest matchers to apply to the exception. | ||
Expects the actual to be an already resolved future. The :py:func:`~hamcrest:core.core.future.resolved` helper can be used to wait for a future to resolve. | ||
Optional argument pattern should be a string containing a regular expression. If provided, | ||
the string representation of the actual exception - e.g. `str(actual)` - must match pattern. | ||
Examples:: | ||
assert_that(somefuture, future_exception(ValueError)) | ||
assert_that( | ||
await resolved(async_http_get()), | ||
future_exception(HTTPError, matching=has_properties(status_code=500) | ||
) | ||
""" | ||
return FutureRaising(exception, pattern, matching) | ||
|
||
|
||
async def resolved(obj: FutureT) -> asyncio.Future: | ||
"""Wait for an async operation to finish and return a resolved future object with the result. | ||
:param obj: A future like object or an awaitable object. | ||
""" | ||
fut = asyncio.ensure_future(obj) | ||
await asyncio.wait([fut]) | ||
return fut |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import sys | ||
|
||
import pytest | ||
import asyncio | ||
from hamcrest import has_properties | ||
from hamcrest.core.core.future import resolved, future_raising | ||
from hamcrest_unit_test.matcher_test import MatcherTest | ||
|
||
if __name__ == "__main__": | ||
sys.path.insert(0, "..") | ||
sys.path.insert(0, "../..") | ||
|
||
|
||
__author__ = "David Keijser" | ||
__copyright__ = "Copyright 2023 hamcrest.org" | ||
__license__ = "BSD, see License.txt" | ||
|
||
|
||
async def no_exception(*args, **kwargs): | ||
return | ||
|
||
|
||
async def raise_exception(*args, **kwargs): | ||
raise AssertionError(str(args) + str(kwargs)) | ||
|
||
|
||
async def raise_exception_with_properties(**kwargs): | ||
err = AssertionError("boom") | ||
for k, v in kwargs.items(): | ||
setattr(err, k, v) | ||
raise err | ||
|
||
|
||
# From python 3.8 this could be simplified by using unittest.IsolatedAsyncioTestCase | ||
class FutureExceptionTest(MatcherTest): | ||
def testMatchesIfFutureHasTheExactExceptionExpected(self): | ||
async def test(): | ||
self.assert_matches( | ||
"Right exception", | ||
future_raising(AssertionError), | ||
await resolved(raise_exception()), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testDoesNotMatchIfActualIsNotAFuture(self): | ||
async def test(): | ||
self.assert_does_not_match("Not a future", future_raising(TypeError), 23) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testDoesNotMatchIfFutureIsNotDone(self): | ||
future = asyncio.Future() | ||
self.assert_does_not_match("Unresolved future", future_raising(TypeError), future) | ||
|
||
def testDoesNotMatchIfFutureIsCancelled(self): | ||
future = asyncio.Future() | ||
future.cancel() | ||
self.assert_does_not_match("Cancelled future", future_raising(TypeError), future) | ||
|
||
@pytest.mark.skipif( | ||
not (3, 0) <= sys.version_info < (3, 7), reason="Message differs between Python versions" | ||
) | ||
def testDoesNotMatchIfFutureHasTheWrongExceptionTypePy3(self): | ||
async def test(): | ||
self.assert_does_not_match( | ||
"Wrong exception", future_raising(IOError), await resolved(raise_exception()) | ||
) | ||
expected_message = ( | ||
"AssertionError('(){}',) of type <class 'AssertionError'> was raised instead" | ||
) | ||
self.assert_mismatch_description( | ||
expected_message, future_raising(TypeError), await resolved(raise_exception()) | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Message differs between Python versions") | ||
def testDoesNotMatchIfFutureHasTheWrongExceptionTypePy37(self): | ||
async def test(): | ||
self.assert_does_not_match( | ||
"Wrong exception", future_raising(IOError), await resolved(raise_exception()) | ||
) | ||
expected_message = ( | ||
"AssertionError('(){}') of type <class 'AssertionError'> was raised instead" | ||
) | ||
self.assert_mismatch_description( | ||
expected_message, future_raising(TypeError), await resolved(raise_exception()) | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testMatchesIfFutureHasASubclassOfTheExpectedException(self): | ||
async def test(): | ||
self.assert_matches( | ||
"Subclassed Exception", | ||
future_raising(Exception), | ||
await resolved(raise_exception()), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testDoesNotMatchIfFutureDoesNotHaveException(self): | ||
async def test(): | ||
self.assert_does_not_match( | ||
"No exception", future_raising(ValueError), await resolved(no_exception()) | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testDoesNotMatchExceptionIfRegularExpressionDoesNotMatch(self): | ||
async def test(): | ||
self.assert_does_not_match( | ||
"Bad regex", | ||
future_raising(AssertionError, "Phrase not found"), | ||
await resolved(raise_exception()), | ||
) | ||
self.assert_mismatch_description( | ||
'''Correct assertion type raised, but the expected pattern ("Phrase not found") not found. Exception message was: "(){}"''', | ||
future_raising(AssertionError, "Phrase not found"), | ||
await resolved(raise_exception()), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testMatchesRegularExpressionToStringifiedException(self): | ||
async def test(): | ||
self.assert_matches( | ||
"Regex", | ||
future_raising(AssertionError, "(3, 1, 4)"), | ||
await resolved(raise_exception(3, 1, 4)), | ||
) | ||
|
||
self.assert_matches( | ||
"Regex", | ||
future_raising(AssertionError, r"([\d, ]+)"), | ||
await resolved(raise_exception(3, 1, 4)), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testMachesIfExceptionMatchesAdditionalMatchers(self): | ||
async def test(): | ||
self.assert_matches( | ||
"Properties", | ||
future_raising(AssertionError, matching=has_properties(prip="prop")), | ||
await resolved(raise_exception_with_properties(prip="prop")), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testDoesNotMatchIfAdditionalMatchersDoesNotMatch(self): | ||
async def test(): | ||
self.assert_does_not_match( | ||
"Bad properties", | ||
future_raising(AssertionError, matching=has_properties(prop="prip")), | ||
await resolved(raise_exception_with_properties(prip="prop")), | ||
) | ||
self.assert_mismatch_description( | ||
'''Correct assertion type raised, but an object with a property 'prop' matching 'prip' not found. Exception message was: "boom"''', | ||
future_raising(AssertionError, matching=has_properties(prop="prip")), | ||
await resolved(raise_exception_with_properties(prip="prop")), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) | ||
|
||
def testDoesNotMatchIfNeitherPatternOrMatcherMatch(self): | ||
async def test(): | ||
self.assert_does_not_match( | ||
"Bad pattern and properties", | ||
future_raising( | ||
AssertionError, pattern="asdf", matching=has_properties(prop="prip") | ||
), | ||
await resolved(raise_exception_with_properties(prip="prop")), | ||
) | ||
self.assert_mismatch_description( | ||
'''Correct assertion type raised, but the expected pattern ("asdf") and an object with a property 'prop' matching 'prip' not found. Exception message was: "boom"''', | ||
future_raising( | ||
AssertionError, pattern="asdf", matching=has_properties(prop="prip") | ||
), | ||
await resolved(raise_exception_with_properties(prip="prop")), | ||
) | ||
|
||
asyncio.get_event_loop().run_until_complete(test()) |