sanic/tests/test_signals.py

541 lines
12 KiB
Python

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",
(
"<foo>.bar.bax",
"foo.<bar>.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.<baz:int>")
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="<Blueprint bp> 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