From b8705c59cd8ebf5e87dfc4d7be0c5973786051aa Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 18 Dec 2020 22:52:17 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20Write=20more=20tests,=20wh?= =?UTF-8?q?y=20do=20they=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- royalnet/engineer/blueprints/blueprint.py | 3 +- royalnet/engineer/sentry/filter.py | 44 +++---- royalnet/engineer/tests/test_sentry.py | 145 +++++++++++++++++++++- 3 files changed, 161 insertions(+), 31 deletions(-) diff --git a/royalnet/engineer/blueprints/blueprint.py b/royalnet/engineer/blueprints/blueprint.py index 70cffae4..70b47634 100644 --- a/royalnet/engineer/blueprints/blueprint.py +++ b/royalnet/engineer/blueprints/blueprint.py @@ -42,12 +42,11 @@ class Blueprint(metaclass=abc.ABCMeta): """ - @abc.abstractmethod def __init__(self): """ :return: The created object. """ - raise NotImplementedError() + pass @abc.abstractmethod def __hash__(self): diff --git a/royalnet/engineer/sentry/filter.py b/royalnet/engineer/sentry/filter.py index cb01cb61..173fb363 100644 --- a/royalnet/engineer/sentry/filter.py +++ b/royalnet/engineer/sentry/filter.py @@ -112,7 +112,7 @@ class Filter: return decorated return decorator - def map(self, c: Callable[[Any], bool]) -> Filter: + def map(self, c: Callable[[Any], object]) -> Filter: """ Apply the function ``c`` on all objects transiting through the queue: - If the function **returns**, its return value replaces the object in the queue; @@ -184,18 +184,6 @@ class Filter: """ return self.requires(blueprints.Message.text).map(lambda o: o.text()) - @staticmethod - def _deco_startswith(prefix: str): - def decorator(func): - @functools.wraps(func) - def decorated(obj): - result: str = func(obj) - if not result.startswith(prefix): - raise exc.Discard(result, f"Text didn't start with {prefix}") - return result - return decorated - return decorator - def startswith(self, prefix: str): """ Check if an object starts with the specified prefix and discard the objects that do not. @@ -203,20 +191,16 @@ class Filter: :param prefix: The prefix object should start with. :return: A new :class:`Filter` with the new requirements. """ - return self.__class__(self._deco_startswith(prefix)(self.func)) + return self.filter(lambda x: x.startswith(prefix), error=f"Text didn't start with {prefix}") - @staticmethod - def _deco_regex(pattern: Pattern): - def decorator(func): - @functools.wraps(func) - def decorated(obj): - result: str = func(obj) - if match := pattern.match(result): - return match - else: - raise exc.Discard(result, f"Text didn't match pattern {pattern}") - return decorated - return decorator + def endswith(self, suffix: str): + """ + Check if an object ends with the specified suffix and discard the objects that do not. + + :param suffix: The prefix object should start with. + :return: A new :class:`Filter` with the new requirements. + """ + return self.filter(lambda x: x.endswith(suffix), error=f"Text didn't end with {suffix}") def regex(self, pattern: Pattern): """ @@ -225,7 +209,13 @@ class Filter: :param pattern: The pattern that should be matched by the text. :return: A new :class:`Filter` with the new requirements. """ - return self.__class__(self._deco_regex(pattern)(self.func)) + def mapping(x): + if match := pattern.match(x): + return match + else: + raise exc.Discard(x, f"Text didn't match pattern {pattern}") + + return self.map(mapping) def choices(self, *choices): """ diff --git a/royalnet/engineer/tests/test_sentry.py b/royalnet/engineer/tests/test_sentry.py index e6038be1..a875cea0 100644 --- a/royalnet/engineer/tests/test_sentry.py +++ b/royalnet/engineer/tests/test_sentry.py @@ -1,7 +1,8 @@ import pytest import asyncio import async_timeout -from .. import sentry, exc +import re +from .. import sentry, exc, blueprints @pytest.fixture @@ -40,6 +41,14 @@ def discarding_filter() -> sentry.Filter: return sentry.Filter(discard) +class ErrorTest(Exception): + pass + + +def error_test(*_, **__): + raise ErrorTest("This was raised by error_raiser.") + + class TestFilter: def test_creation(self): f = sentry.Filter(lambda _: _) @@ -66,5 +75,137 @@ class TestFilter: @pytest.mark.asyncio async def test_timeout(self, s: sentry.Sentry): with pytest.raises(asyncio.TimeoutError): - async with async_timeout.timeout(0.05): + async with async_timeout.timeout(0.001): await s.f().get() + + @pytest.mark.asyncio + async def test_filter(self, s: sentry.Sentry): + await s.queue.put(None) + await s.queue.put(None) + + assert await s.f().filter(lambda x: x is None, "Is not None").get_single() is None + + with pytest.raises(exc.Discard): + await s.f().filter(lambda x: isinstance(x, type), error="Is not type").get_single() + + with pytest.raises(ErrorTest): + await s.f().filter(error_test, error="Is error").get_single() + + @pytest.mark.asyncio + async def test_map(self, s: sentry.Sentry): + await s.queue.put(None) + await s.queue.put(None) + + assert await s.f().map(lambda x: 1).get_single() == 1 + + with pytest.raises(ErrorTest): + await s.f().map(error_test).get_single() + + @pytest.mark.asyncio + async def test_type(self, s: sentry.Sentry): + await s.queue.put(1) + await s.queue.put("no") + + assert await s.f().type(int).get_single() == 1 + + with pytest.raises(exc.Discard): + await s.f().type(int).get_single() + + @pytest.mark.asyncio + async def test_msg(self, s: sentry.Sentry): + class ExampleMessage(blueprints.Message): + def __hash__(self): + return 1 + + msg = ExampleMessage() + await s.queue.put(msg) + await s.queue.put("no") + + assert await s.f().msg().get_single() is msg + + with pytest.raises(exc.Discard): + await s.f().msg().get_single() + + @pytest.mark.asyncio + async def test_requires(self, s: sentry.Sentry): + class AvailableMessage(blueprints.Message): + def __hash__(self): + return 1 + + def text(self) -> str: + return "1" + + class NotAvailableMessage(blueprints.Message): + def __hash__(self): + return 2 + + def text(self) -> str: + raise exc.NotAvailableError() + + class NeverAvailableMessage(blueprints.Message): + def __hash__(self): + return 3 + + avmsg = AvailableMessage() + namsg = NotAvailableMessage() + nvmsg = NeverAvailableMessage() + + await s.queue.put(avmsg) + await s.queue.put(namsg) + await s.queue.put(nvmsg) + await s.queue.put(namsg) + await s.queue.put(nvmsg) + + assert await s.f().requires(blueprints.Message.text).get_single() is avmsg + + with pytest.raises(exc.Discard): + await s.f().requires(blueprints.Message.text).get_single() + + with pytest.raises(exc.NeverAvailableError): + await s.f().requires(blueprints.Message.text).get_single() + + with pytest.raises(exc.NotAvailableError): + await s.f().requires(blueprints.Message.text, propagate_not_available=True).get_single() + + with pytest.raises(exc.Discard): + await s.f().requires(blueprints.Message.text, propagate_never_available=False).get_single() + + @pytest.mark.asyncio + async def test_startswith(self, s: sentry.Sentry): + await s.queue.put("yarrharr") + await s.queue.put("yohoho") + + assert await s.f().startswith("yarr").get_single() == "yarrharr" + + with pytest.raises(exc.Discard): + await s.f().startswith("yarr").get_single() + + @pytest.mark.asyncio + async def test_endswith(self, s: sentry.Sentry): + await s.queue.put("yarrharr") + await s.queue.put("yohoho") + + assert await s.f().endswith("harr").get_single() == "yarrharr" + + with pytest.raises(exc.Discard): + await s.f().endswith("harr").get_single() + + @pytest.mark.asyncio + async def test_regex(self, s: sentry.Sentry): + await s.queue.put("yarrharr") + await s.queue.put("yohoho") + + assert isinstance(await s.f().regex(re.compile(r"[yh]arr")).get_single(), re.Match) + + with pytest.raises(exc.Discard): + await s.f().regex(re.compile(r"[yh]arr")).get_single() + + @pytest.mark.asyncio + async def test_choices(self, s: sentry.Sentry): + await s.queue.put("yarrharr") + await s.queue.put("yohoho") + + assert await s.f().choices("yarrharr", "banana").get_single() == "yarrharr" + + with pytest.raises(exc.Discard): + await s.f().choices("yarrharr", "banana").get_single()