import asyncio from enum import Enum from inspect import isawaitable from itertools import count import pytest from sanic_routing.exceptions import NotFound from sanic import Blueprint, Sanic, empty from sanic.exceptions import InvalidSignal, SanicException from sanic.signals import Event def test_add_signal(app): def sync_signal(*_): ... app.add_signal(sync_signal, "foo.bar.baz") assert len(app.signal_router.routes) == 1 def test_add_signal_method_handler(app): counter = 0 class TestSanic(Sanic): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_signal(self.after_routing_signal_handler, "http.routing.after") def after_routing_signal_handler(self, *args, **kwargs): nonlocal counter counter += 1 app = TestSanic("Test") assert len(app.signal_router.routes) == 1 @app.route("/") async def handler(_): return empty() app.test_client.get("/") assert counter == 1 def test_add_signal_decorator(app): @app.signal("foo.bar.baz") def sync_signal(*_): ... @app.signal("foo.bar.baz") async def async_signal(*_): ... assert len(app.signal_router.routes) == 2 assert len(app.signal_router.dynamic_routes) == 1 @pytest.mark.parametrize( "signal", ( ".bar.bax", "foo..baz", "foo", "foo.bar", "foo.bar.baz.qux", ), ) def test_invalid_signal(app, signal): with pytest.raises(InvalidSignal, match=f"Invalid signal event: {signal}"): @app.signal(signal) def handler(): ... @pytest.mark.asyncio async def test_dispatch_signal_with_enum_event(app): counter = 0 class FooEnum(Enum): FOO_BAR_BAZ = "foo.bar.baz" @app.signal(FooEnum.FOO_BAR_BAZ) def sync_signal(*_): nonlocal counter counter += 1 app.signal_router.finalize() await app.dispatch("foo.bar.baz") assert counter == 1 @pytest.mark.asyncio async def test_dispatch_signal_triggers_multiple_handlers(app): counter = 0 @app.signal("foo.bar.baz") def sync_signal(*_): nonlocal counter counter += 1 @app.signal("foo.bar.baz") async def async_signal(*_): nonlocal counter counter += 1 app.signal_router.finalize() assert len(app.signal_router.routes) == 3 await app.dispatch("foo.bar.baz") assert counter == 2 @pytest.mark.asyncio async def test_dispatch_signal_triggers_triggers_event(app): counter = 0 @app.signal("foo.bar.baz") def sync_signal(*args): nonlocal app nonlocal counter group, *_ = app.signal_router.get("foo.bar.baz") for signal in group: counter += signal.ctx.event.is_set() app.signal_router.finalize() await app.dispatch("foo.bar.baz") assert counter == 1 @pytest.mark.asyncio async def test_dispatch_signal_triggers_dynamic_route(app): counter = 0 @app.signal("foo.bar.") def sync_signal(baz): nonlocal counter counter += baz app.signal_router.finalize() await app.dispatch("foo.bar.9") assert counter == 9 @pytest.mark.asyncio async def test_dispatch_signal_triggers_with_requirements(app): counter = 0 @app.signal("foo.bar.baz", condition={"one": "two"}) def sync_signal(*_): nonlocal counter counter += 1 app.signal_router.finalize() await app.dispatch("foo.bar.baz") assert counter == 0 await app.dispatch("foo.bar.baz", condition={"one": "two"}) assert counter == 1 @pytest.mark.asyncio async def test_dispatch_signal_triggers_with_requirements_exclusive(app): counter = 0 @app.signal("foo.bar.baz", condition={"one": "two"}, exclusive=False) def sync_signal(*_): nonlocal counter counter += 1 app.signal_router.finalize() await app.dispatch("foo.bar.baz") assert counter == 1 await app.dispatch("foo.bar.baz", condition={"one": "two"}) assert counter == 2 @pytest.mark.asyncio async def test_dispatch_signal_triggers_with_context(app): counter = 0 @app.signal("foo.bar.baz") def sync_signal(amount): nonlocal counter counter += amount app.signal_router.finalize() await app.dispatch("foo.bar.baz", context={"amount": 9}) assert counter == 9 @pytest.mark.asyncio async def test_dispatch_signal_triggers_with_context_fail(app): counter = 0 @app.signal("foo.bar.baz") def sync_signal(amount): nonlocal counter counter += amount app.signal_router.finalize() with pytest.raises(TypeError): await app.dispatch("foo.bar.baz", {"amount": 9}) @pytest.mark.asyncio async def test_dispatch_signal_triggers_on_bp(app): bp = Blueprint("bp") app_counter = 0 bp_counter = 0 @app.signal("foo.bar.baz") def app_signal(): nonlocal app_counter app_counter += 1 @bp.signal("foo.bar.baz") def bp_signal(): nonlocal bp_counter bp_counter += 1 app.blueprint(bp) app.signal_router.finalize() await app.dispatch("foo.bar.baz") assert app_counter == 1 assert bp_counter == 1 await bp.dispatch("foo.bar.baz") assert app_counter == 1 assert bp_counter == 2 @pytest.mark.asyncio async def test_dispatch_signal_triggers_on_bp_alone(app): bp = Blueprint("bp") bp_counter = 0 @bp.signal("foo.bar.baz") def bp_signal(): nonlocal bp_counter bp_counter += 1 app.blueprint(bp) app.signal_router.finalize() await app.dispatch("foo.bar.baz") await bp.dispatch("foo.bar.baz") assert bp_counter == 2 @pytest.mark.asyncio async def test_dispatch_signal_triggers_event(app): app_counter = 0 @app.signal("foo.bar.baz") def app_signal(): ... async def do_wait(): nonlocal app_counter await app.event("foo.bar.baz") app_counter += 1 app.signal_router.finalize() await app.dispatch("foo.bar.baz") waiter = app.event("foo.bar.baz") assert isawaitable(waiter) fut = asyncio.ensure_future(do_wait()) await app.dispatch("foo.bar.baz") await fut assert app_counter == 1 @pytest.mark.asyncio async def test_dispatch_signal_triggers_event_on_bp(app): bp = Blueprint("bp") bp_counter = 0 @bp.signal("foo.bar.baz") def bp_signal(): ... async def do_wait(): nonlocal bp_counter await bp.event("foo.bar.baz") bp_counter += 1 app.blueprint(bp) app.signal_router.finalize() signal_group, *_ = app.signal_router.get( "foo.bar.baz", condition={"blueprint": "bp"} ) await bp.dispatch("foo.bar.baz") waiter = bp.event("foo.bar.baz") assert isawaitable(waiter) fut = do_wait() for signal in signal_group: signal.ctx.event.set() await asyncio.gather(fut) assert bp_counter == 1 def test_bad_finalize(app): counter = 0 @app.signal("foo.bar.baz") def sync_signal(amount): nonlocal counter counter += amount with pytest.raises( RuntimeError, match="Cannot finalize signals outside of event loop" ): app.signal_router.finalize() assert counter == 0 @pytest.mark.asyncio async def test_event_not_exist(app): with pytest.raises(NotFound, match="Could not find signal does.not.exist"): await app.event("does.not.exist") @pytest.mark.asyncio async def test_event_not_exist_on_bp(app): bp = Blueprint("bp") app.blueprint(bp) with pytest.raises(NotFound, match="Could not find signal does.not.exist"): await bp.event("does.not.exist") @pytest.mark.asyncio async def test_event_not_exist_with_autoregister(app): app.config.EVENT_AUTOREGISTER = True try: await app.event("does.not.exist", timeout=0.1) except asyncio.TimeoutError: ... @pytest.mark.asyncio async def test_dispatch_signal_triggers_non_exist_event_with_autoregister(app): @app.signal("some.stand.in") async def signal_handler(): ... app.config.EVENT_AUTOREGISTER = True app_counter = 0 app.signal_router.finalize() async def do_wait(): nonlocal app_counter await app.event("foo.bar.baz") app_counter += 1 fut = asyncio.ensure_future(do_wait()) await app.dispatch("foo.bar.baz") await fut assert app_counter == 1 @pytest.mark.asyncio async def test_dispatch_not_exist(app): @app.signal("do.something.start") async def signal_handler(): ... app.signal_router.finalize() await app.dispatch("does.not.exist") def test_event_on_bp_not_registered(): bp = Blueprint("bp") @bp.signal("foo.bar.baz") def bp_signal(): ... with pytest.raises( SanicException, match=" has not yet been registered to an app", ): bp.event("foo.bar.baz") @pytest.mark.parametrize( "event,expected", ( ("foo.bar.baz", True), ("server.init.before", True), ("server.init.somethingelse", False), ("http.request.start", False), ("sanic.notice.anything", True), ), ) def test_signal_reservation(app, event, expected): if not expected: with pytest.raises( InvalidSignal, match=f"Cannot declare reserved signal event: {event}", ): app.signal(event)(lambda: ...) else: app.signal(event)(lambda: ...) @pytest.mark.asyncio async def test_report_exception(app: Sanic): @app.report_exception async def catch_any_exception(app: Sanic, exception: Exception): ... @app.route("/") async def handler(request): 1 / 0 app.signal_router.finalize() registered_signal_handlers = [ handler for handler, *_ in app.signal_router.get(Event.SERVER_EXCEPTION_REPORT.value) ] assert catch_any_exception in registered_signal_handlers def test_report_exception_runs(app: Sanic): event = asyncio.Event() @app.report_exception async def catch_any_exception(app: Sanic, exception: Exception): event.set() @app.route("/") async def handler(request): 1 / 0 app.test_client.get("/") assert event.is_set() def test_report_exception_runs_once_inline(app: Sanic): event = asyncio.Event() c = count() @app.report_exception async def catch_any_exception(app: Sanic, exception: Exception): event.set() next(c) @app.route("/") async def handler(request): ... @app.signal(Event.HTTP_ROUTING_AFTER.value) async def after_routing(**_): 1 / 0 app.test_client.get("/") assert event.is_set() assert next(c) == 1 def test_report_exception_runs_once_custom(app: Sanic): event = asyncio.Event() c = count() @app.report_exception async def catch_any_exception(app: Sanic, exception: Exception): event.set() next(c) @app.route("/") async def handler(request): await app.dispatch("one.two.three") return empty() @app.signal("one.two.three") async def one_two_three(**_): 1 / 0 app.test_client.get("/") assert event.is_set() assert next(c) == 1 def test_report_exception_runs_task(app: Sanic): c = count() async def task_1(): next(c) async def task_2(app): next(c) @app.report_exception async def catch_any_exception(app: Sanic, exception: Exception): next(c) @app.route("/") async def handler(request): app.add_task(task_1) app.add_task(task_1()) app.add_task(task_2) app.add_task(task_2(app)) return empty() app.test_client.get("/") assert next(c) == 4