Sanic multi-application server (#2347)
This commit is contained in:
@@ -175,6 +175,21 @@ def run_startup(caplog):
|
||||
return run
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_multi(caplog):
|
||||
def run(app, level=logging.DEBUG):
|
||||
@app.after_server_start
|
||||
async def stop(app, _):
|
||||
app.stop()
|
||||
|
||||
with caplog.at_level(level):
|
||||
Sanic.serve()
|
||||
|
||||
return caplog.record_tuples
|
||||
|
||||
return run
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def message_in_records():
|
||||
def msg_in_log(records: List[LogRecord], msg: str):
|
||||
|
||||
@@ -197,7 +197,7 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
|
||||
assert app.websocket_enabled == True
|
||||
|
||||
|
||||
@patch("sanic.app.WebSocketProtocol")
|
||||
@patch("sanic.mixins.runner.WebSocketProtocol")
|
||||
def test_app_websocket_parameters(websocket_protocol_mock, app):
|
||||
app.config.WEBSOCKET_MAX_SIZE = 44
|
||||
app.config.WEBSOCKET_PING_TIMEOUT = 48
|
||||
@@ -473,13 +473,14 @@ def test_custom_context():
|
||||
assert app.ctx == ctx
|
||||
|
||||
|
||||
def test_uvloop_config(app, monkeypatch):
|
||||
@pytest.mark.parametrize("use", (False, True))
|
||||
def test_uvloop_config(app, monkeypatch, use):
|
||||
@app.get("/test")
|
||||
def handler(request):
|
||||
return text("ok")
|
||||
|
||||
try_use_uvloop = Mock()
|
||||
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
|
||||
monkeypatch.setattr(sanic.mixins.runner, "try_use_uvloop", try_use_uvloop)
|
||||
|
||||
# Default config
|
||||
app.test_client.get("/test")
|
||||
@@ -489,14 +490,13 @@ def test_uvloop_config(app, monkeypatch):
|
||||
try_use_uvloop.assert_called_once()
|
||||
|
||||
try_use_uvloop.reset_mock()
|
||||
app.config["USE_UVLOOP"] = False
|
||||
app.config["USE_UVLOOP"] = use
|
||||
app.test_client.get("/test")
|
||||
try_use_uvloop.assert_not_called()
|
||||
|
||||
try_use_uvloop.reset_mock()
|
||||
app.config["USE_UVLOOP"] = True
|
||||
app.test_client.get("/test")
|
||||
try_use_uvloop.assert_called_once()
|
||||
if use:
|
||||
try_use_uvloop.assert_called_once()
|
||||
else:
|
||||
try_use_uvloop.assert_not_called()
|
||||
|
||||
|
||||
def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
|
||||
@@ -506,7 +506,7 @@ def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
|
||||
apps[2].config.USE_UVLOOP = True
|
||||
|
||||
try_use_uvloop = Mock()
|
||||
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
|
||||
monkeypatch.setattr(sanic.mixins.runner, "try_use_uvloop", try_use_uvloop)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
@@ -569,3 +569,8 @@ def test_cannot_run_fast_and_workers(app):
|
||||
message = "You cannot use both fast=True and workers=X"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
app.run(fast=True, workers=4)
|
||||
|
||||
|
||||
def test_no_workers(app):
|
||||
with pytest.raises(RuntimeError, match="Cannot serve with no workers"):
|
||||
app.run(workers=0)
|
||||
|
||||
@@ -118,10 +118,10 @@ def test_host_port_localhost(cmd):
|
||||
command = ["sanic", "fake.server.app", *cmd]
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
expected = b"Goin' Fast @ http://localhost:9999"
|
||||
|
||||
assert exitcode != 1
|
||||
assert firstline == b"Goin' Fast @ http://localhost:9999"
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -135,10 +135,10 @@ def test_host_port_ipv4(cmd):
|
||||
command = ["sanic", "fake.server.app", *cmd]
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
expected = b"Goin' Fast @ http://127.0.0.127:9999"
|
||||
|
||||
assert exitcode != 1
|
||||
assert firstline == b"Goin' Fast @ http://127.0.0.127:9999"
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -152,10 +152,10 @@ def test_host_port_ipv6_any(cmd):
|
||||
command = ["sanic", "fake.server.app", *cmd]
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
expected = b"Goin' Fast @ http://[::]:9999"
|
||||
|
||||
assert exitcode != 1
|
||||
assert firstline == b"Goin' Fast @ http://[::]:9999"
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -169,10 +169,10 @@ def test_host_port_ipv6_loopback(cmd):
|
||||
command = ["sanic", "fake.server.app", *cmd]
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
firstline = lines[starting_line(lines) + 1]
|
||||
expected = b"Goin' Fast @ http://[::1]:9999"
|
||||
|
||||
assert exitcode != 1
|
||||
assert firstline == b"Goin' Fast @ http://[::1]:9999"
|
||||
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -191,13 +191,13 @@ def test_num_workers(num, cmd):
|
||||
out, err, exitcode = capture(command)
|
||||
lines = out.split(b"\n")
|
||||
|
||||
worker_lines = [
|
||||
line
|
||||
for line in lines
|
||||
if b"Starting worker" in line or b"Stopping worker" in line
|
||||
]
|
||||
if num == 1:
|
||||
expected = b"mode: production, single worker"
|
||||
else:
|
||||
expected = (f"mode: production, w/ {num} workers").encode()
|
||||
|
||||
assert exitcode != 1
|
||||
assert len(worker_lines) == num * 2, f"Lines found: {lines}"
|
||||
assert expected in lines, f"Expected {expected}\nLines found: {lines}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--debug",))
|
||||
@@ -207,9 +207,11 @@ def test_debug(cmd):
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
|
||||
assert info["debug"] is True
|
||||
assert info["auto_reload"] is False
|
||||
assert "dev" not in info
|
||||
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
|
||||
assert (
|
||||
info["auto_reload"] is False
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--dev", "-d"))
|
||||
@@ -219,8 +221,10 @@ def test_dev(cmd):
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
|
||||
assert info["debug"] is True
|
||||
assert info["auto_reload"] is True
|
||||
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
|
||||
assert (
|
||||
info["auto_reload"] is True
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
|
||||
@@ -230,9 +234,11 @@ def test_auto_reload(cmd):
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
|
||||
assert info["debug"] is False
|
||||
assert info["auto_reload"] is True
|
||||
assert "dev" not in info
|
||||
assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}"
|
||||
assert (
|
||||
info["auto_reload"] is True
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -244,7 +250,9 @@ def test_access_logs(cmd, expected):
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
|
||||
assert info["access_log"] is expected
|
||||
assert (
|
||||
info["access_log"] is expected
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", ("--version", "-v"))
|
||||
@@ -269,4 +277,6 @@ def test_noisy_exceptions(cmd, expected):
|
||||
lines = out.split(b"\n")
|
||||
info = read_app_info(lines)
|
||||
|
||||
assert info["noisy_exceptions"] is expected
|
||||
assert (
|
||||
info["noisy_exceptions"] is expected
|
||||
), f"Lines found: {lines}\nErr output: {err}"
|
||||
|
||||
@@ -301,6 +301,9 @@ def test_config_access_log_passing_in_run(app: Sanic):
|
||||
app.run(port=1340, access_log=False)
|
||||
assert app.config.ACCESS_LOG is False
|
||||
|
||||
app.router.reset()
|
||||
app.signal_router.reset()
|
||||
|
||||
app.run(port=1340, access_log=True)
|
||||
assert app.config.ACCESS_LOG is True
|
||||
|
||||
@@ -420,3 +423,15 @@ def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
|
||||
|
||||
app.config.update_config({"FOO": 10})
|
||||
post_set.assert_called_once_with("FOO", 10)
|
||||
|
||||
|
||||
def test_negative_proxy_count(app: Sanic):
|
||||
app.config.PROXIES_COUNT = -1
|
||||
|
||||
message = (
|
||||
"PROXIES_COUNT cannot be negative. "
|
||||
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
|
||||
"#proxy-configuration"
|
||||
)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
app.prepare()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from sanic import __version__
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic, __version__
|
||||
from sanic.application.logo import BASE_LOGO
|
||||
from sanic.application.motd import MOTDTTY
|
||||
from sanic.application.motd import MOTD, MOTDTTY
|
||||
|
||||
|
||||
def test_logo_base(app, run_startup):
|
||||
@@ -83,3 +87,25 @@ def test_motd_display(caplog):
|
||||
└───────────────────────┴────────┘
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not on 3.7")
|
||||
def test_reload_dirs(app):
|
||||
app.config.LOGO = None
|
||||
app.config.AUTO_RELOAD = True
|
||||
app.prepare(reload_dir="./", auto_reload=True, motd_display={"foo": "bar"})
|
||||
|
||||
existing = MOTD.output
|
||||
MOTD.output = Mock()
|
||||
|
||||
app.motd("foo")
|
||||
|
||||
MOTD.output.assert_called_once()
|
||||
assert (
|
||||
MOTD.output.call_args.args[2]["auto-reload"]
|
||||
== f"enabled, {os.getcwd()}"
|
||||
)
|
||||
assert MOTD.output.call_args.args[3] == {"foo": "bar"}
|
||||
|
||||
MOTD.output = existing
|
||||
Sanic._app_registry = {}
|
||||
|
||||
207
tests/test_multi_serve.py
Normal file
207
tests/test_multi_serve.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import logging
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
from sanic.signals import Event
|
||||
from sanic.touchup.schemes.ode import OptionalDispatchEvent
|
||||
|
||||
|
||||
try:
|
||||
from unittest.mock import AsyncMock
|
||||
except ImportError:
|
||||
from asyncmock import AsyncMock # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_one():
|
||||
app = Sanic("One")
|
||||
|
||||
@app.get("/one")
|
||||
async def one(request):
|
||||
return text("one")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_two():
|
||||
app = Sanic("Two")
|
||||
|
||||
@app.get("/two")
|
||||
async def two(request):
|
||||
return text("two")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean():
|
||||
Sanic._app_registry = {}
|
||||
yield
|
||||
|
||||
|
||||
def test_serve_same_app_multiple_tuples(app_one, run_multi):
|
||||
app_one.prepare(port=23456)
|
||||
app_one.prepare(port=23457)
|
||||
|
||||
logs = run_multi(app_one)
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.INFO,
|
||||
"Goin' Fast @ http://127.0.0.1:23456",
|
||||
) in logs
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.INFO,
|
||||
"Goin' Fast @ http://127.0.0.1:23457",
|
||||
) in logs
|
||||
|
||||
|
||||
def test_serve_multiple_apps(app_one, app_two, run_multi):
|
||||
app_one.prepare(port=23456)
|
||||
app_two.prepare(port=23457)
|
||||
|
||||
logs = run_multi(app_one)
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.INFO,
|
||||
"Goin' Fast @ http://127.0.0.1:23456",
|
||||
) in logs
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.INFO,
|
||||
"Goin' Fast @ http://127.0.0.1:23457",
|
||||
) in logs
|
||||
|
||||
|
||||
def test_listeners_on_secondary_app(app_one, app_two, run_multi):
|
||||
app_one.prepare(port=23456)
|
||||
app_two.prepare(port=23457)
|
||||
|
||||
before_start = AsyncMock()
|
||||
after_start = AsyncMock()
|
||||
before_stop = AsyncMock()
|
||||
after_stop = AsyncMock()
|
||||
|
||||
app_two.before_server_start(before_start)
|
||||
app_two.after_server_start(after_start)
|
||||
app_two.before_server_stop(before_stop)
|
||||
app_two.after_server_stop(after_stop)
|
||||
|
||||
run_multi(app_one)
|
||||
|
||||
before_start.assert_awaited_once()
|
||||
after_start.assert_awaited_once()
|
||||
before_stop.assert_awaited_once()
|
||||
after_stop.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"events",
|
||||
(
|
||||
(Event.HTTP_LIFECYCLE_BEGIN,),
|
||||
(Event.HTTP_LIFECYCLE_BEGIN, Event.HTTP_LIFECYCLE_COMPLETE),
|
||||
(
|
||||
Event.HTTP_LIFECYCLE_BEGIN,
|
||||
Event.HTTP_LIFECYCLE_COMPLETE,
|
||||
Event.HTTP_LIFECYCLE_REQUEST,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_signal_synchronization(app_one, app_two, run_multi, events):
|
||||
app_one.prepare(port=23456)
|
||||
app_two.prepare(port=23457)
|
||||
|
||||
for event in events:
|
||||
app_one.signal(event)(AsyncMock())
|
||||
|
||||
run_multi(app_one)
|
||||
|
||||
assert len(app_two.signal_router.routes) == len(events) + 1
|
||||
|
||||
signal_handlers = {
|
||||
signal.handler
|
||||
for signal in app_two.signal_router.routes
|
||||
if signal.name.startswith("http")
|
||||
}
|
||||
|
||||
assert len(signal_handlers) == 1
|
||||
assert list(signal_handlers)[0] is OptionalDispatchEvent.noop
|
||||
|
||||
|
||||
def test_warning_main_process_listeners_on_secondary(
|
||||
app_one, app_two, run_multi
|
||||
):
|
||||
app_two.main_process_start(AsyncMock())
|
||||
app_two.main_process_stop(AsyncMock())
|
||||
app_one.prepare(port=23456)
|
||||
app_two.prepare(port=23457)
|
||||
|
||||
log = run_multi(app_one)
|
||||
|
||||
message = (
|
||||
f"Sanic found 2 listener(s) on "
|
||||
"secondary applications attached to the main "
|
||||
"process. These will be ignored since main "
|
||||
"process listeners can only be attached to your "
|
||||
"primary application: "
|
||||
f"{repr(app_one)}"
|
||||
)
|
||||
|
||||
assert ("sanic.error", logging.WARNING, message) in log
|
||||
|
||||
|
||||
def test_no_applications():
|
||||
Sanic._app_registry = {}
|
||||
message = "Did not find any applications."
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
Sanic.serve()
|
||||
|
||||
|
||||
def test_oserror_warning(app_one, app_two, run_multi, capfd):
|
||||
orig = AsyncioServer.__await__
|
||||
AsyncioServer.__await__ = Mock(side_effect=OSError("foo"))
|
||||
app_one.prepare(port=23456, workers=2)
|
||||
app_two.prepare(port=23457, workers=2)
|
||||
|
||||
run_multi(app_one)
|
||||
|
||||
captured = capfd.readouterr()
|
||||
assert (
|
||||
"An OSError was detected on startup. The encountered error was: foo"
|
||||
) in captured.err
|
||||
|
||||
AsyncioServer.__await__ = orig
|
||||
|
||||
|
||||
def test_running_multiple_offset_warning(app_one, app_two, run_multi, capfd):
|
||||
app_one.prepare(port=23456, workers=2)
|
||||
app_two.prepare(port=23457)
|
||||
|
||||
run_multi(app_one)
|
||||
|
||||
captured = capfd.readouterr()
|
||||
assert (
|
||||
f"The primary application {repr(app_one)} is running "
|
||||
"with 2 worker(s). All "
|
||||
"application instances will run with the same number. "
|
||||
f"You requested {repr(app_two)} to run with "
|
||||
"1 worker(s), which will be ignored "
|
||||
"in favor of the primary application."
|
||||
) in captured.err
|
||||
|
||||
|
||||
def test_running_multiple_secondary(app_one, app_two, run_multi, capfd):
|
||||
app_one.prepare(port=23456, workers=2)
|
||||
app_two.prepare(port=23457)
|
||||
|
||||
before_start = AsyncMock()
|
||||
app_two.before_server_start(before_start)
|
||||
run_multi(app_one)
|
||||
|
||||
before_start.await_count == 2
|
||||
@@ -132,11 +132,11 @@ def test_main_process_event(app, caplog):
|
||||
logger.info("main_process_stop")
|
||||
|
||||
@app.main_process_start
|
||||
def main_process_start(app, loop):
|
||||
def main_process_start2(app, loop):
|
||||
logger.info("main_process_start")
|
||||
|
||||
@app.main_process_stop
|
||||
def main_process_stop(app, loop):
|
||||
def main_process_stop2(app, loop):
|
||||
logger.info("main_process_stop")
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
|
||||
71
tests/test_prepare.py
Normal file
71
tests/test_prepare.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.application.state import ApplicationServerInfo
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_skip():
|
||||
should_auto_reload = Sanic.should_auto_reload
|
||||
Sanic.should_auto_reload = Mock(return_value=False)
|
||||
yield
|
||||
Sanic._app_registry = {}
|
||||
Sanic.should_auto_reload = should_auto_reload
|
||||
|
||||
|
||||
def get_primary(app: Sanic) -> ApplicationServerInfo:
|
||||
return app.state.server_info[0]
|
||||
|
||||
|
||||
def test_dev(app: Sanic):
|
||||
app.prepare(dev=True)
|
||||
|
||||
assert app.state.is_debug
|
||||
assert app.state.auto_reload
|
||||
|
||||
|
||||
def test_motd_display(app: Sanic):
|
||||
app.prepare(motd_display={"foo": "bar"})
|
||||
|
||||
assert app.config.MOTD_DISPLAY["foo"] == "bar"
|
||||
del app.config.MOTD_DISPLAY["foo"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dirs", ("./foo", ("./foo", "./bar")))
|
||||
def test_reload_dir(app: Sanic, dirs, caplog):
|
||||
messages = []
|
||||
with caplog.at_level(logging.WARNING):
|
||||
app.prepare(reload_dir=dirs)
|
||||
|
||||
if isinstance(dirs, str):
|
||||
dirs = (dirs,)
|
||||
for d in dirs:
|
||||
assert Path(d) in app.state.reload_dirs
|
||||
messages.append(
|
||||
f"Directory {d} could not be located",
|
||||
)
|
||||
|
||||
for message in messages:
|
||||
assert ("sanic.root", logging.WARNING, message) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_fast(app: Sanic, run_multi):
|
||||
app.prepare(fast=True)
|
||||
try:
|
||||
workers = len(os.sched_getaffinity(0))
|
||||
except AttributeError:
|
||||
workers = os.cpu_count() or 1
|
||||
|
||||
assert app.state.fast
|
||||
assert app.state.workers == workers
|
||||
|
||||
logs = run_multi(app, logging.INFO)
|
||||
|
||||
messages = [m[2] for m in logs]
|
||||
assert f"mode: production, goin' fast w/ {workers} workers" in messages
|
||||
@@ -35,7 +35,7 @@ def create_listener(listener_name, in_list):
|
||||
|
||||
def start_stop_app(random_name_app, **run_kwargs):
|
||||
def stop_on_alarm(signum, frame):
|
||||
raise KeyboardInterrupt("SIGINT for sanic to stop gracefully")
|
||||
random_name_app.stop()
|
||||
|
||||
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||
signal.alarm(1)
|
||||
@@ -130,6 +130,9 @@ async def test_trigger_before_events_create_server_missing_event(app):
|
||||
def test_create_server_trigger_events(app):
|
||||
"""Test if create_server can trigger server events"""
|
||||
|
||||
def stop_on_alarm(signum, frame):
|
||||
raise KeyboardInterrupt("...")
|
||||
|
||||
flag1 = False
|
||||
flag2 = False
|
||||
flag3 = False
|
||||
@@ -137,8 +140,7 @@ def test_create_server_trigger_events(app):
|
||||
async def stop(app, loop):
|
||||
nonlocal flag1
|
||||
flag1 = True
|
||||
await asyncio.sleep(0.1)
|
||||
app.stop()
|
||||
signal.alarm(1)
|
||||
|
||||
async def before_stop(app, loop):
|
||||
nonlocal flag2
|
||||
@@ -155,6 +157,8 @@ def test_create_server_trigger_events(app):
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Use random port for tests
|
||||
|
||||
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||
with closing(socket()) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from asyncio.tasks import Task
|
||||
from unittest.mock import Mock, call
|
||||
@@ -7,9 +6,15 @@ from unittest.mock import Mock, call
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.application.state import ApplicationServerInfo, ServerStage
|
||||
from sanic.response import empty
|
||||
|
||||
|
||||
try:
|
||||
from unittest.mock import AsyncMock
|
||||
except ImportError:
|
||||
from asyncmock import AsyncMock # type: ignore
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@@ -20,11 +25,14 @@ async def dummy(n=0):
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mark_app_running(app):
|
||||
app.is_running = True
|
||||
def mark_app_running(app: Sanic):
|
||||
app.state.server_info.append(
|
||||
ApplicationServerInfo(
|
||||
stage=ServerStage.SERVING, settings={}, server=AsyncMock()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
||||
async def test_add_task_returns_task(app: Sanic):
|
||||
task = app.add_task(dummy())
|
||||
|
||||
@@ -32,7 +40,6 @@ async def test_add_task_returns_task(app: Sanic):
|
||||
assert len(app._task_registry) == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
||||
async def test_add_task_with_name(app: Sanic):
|
||||
task = app.add_task(dummy(), name="dummy")
|
||||
|
||||
@@ -44,7 +51,6 @@ async def test_add_task_with_name(app: Sanic):
|
||||
assert task in app._task_registry.values()
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
||||
async def test_cancel_task(app: Sanic):
|
||||
task = app.add_task(dummy(3), name="dummy")
|
||||
|
||||
@@ -62,7 +68,6 @@ async def test_cancel_task(app: Sanic):
|
||||
assert task.cancelled()
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
||||
async def test_purge_tasks(app: Sanic):
|
||||
app.add_task(dummy(3), name="dummy")
|
||||
|
||||
@@ -75,7 +80,6 @@ async def test_purge_tasks(app: Sanic):
|
||||
assert len(app._task_registry) == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
||||
def test_shutdown_tasks_on_app_stop():
|
||||
class TestSanic(Sanic):
|
||||
shutdown_tasks = Mock()
|
||||
|
||||
@@ -72,14 +72,12 @@ def test_unix_socket_creation(caplog):
|
||||
assert not os.path.exists(SOCKPATH)
|
||||
|
||||
|
||||
def test_invalid_paths():
|
||||
@pytest.mark.parametrize("path", (".", "no-such-directory/sanictest.sock"))
|
||||
def test_invalid_paths(path):
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
with pytest.raises(FileExistsError):
|
||||
app.run(unix=".")
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
app.run(unix="no-such-directory/sanictest.sock")
|
||||
with pytest.raises((FileExistsError, FileNotFoundError)):
|
||||
app.run(unix=path)
|
||||
|
||||
|
||||
def test_dont_replace_file():
|
||||
@@ -201,7 +199,7 @@ async def test_zero_downtime():
|
||||
for _ in range(40):
|
||||
async with httpx.AsyncClient(transport=transport) as client:
|
||||
r = await client.get("http://localhost/sleep/0.1")
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 200, r.content
|
||||
assert r.text == "Slept 0.1 seconds.\n"
|
||||
|
||||
def spawn():
|
||||
@@ -209,6 +207,7 @@ async def test_zero_downtime():
|
||||
sys.executable,
|
||||
"-m",
|
||||
"sanic",
|
||||
"--debug",
|
||||
"--unix",
|
||||
SOCKPATH,
|
||||
"examples.delayed_response.app",
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_testing.testing import ASGI_PORT as PORT
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.worker import GunicornWorker
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gunicorn_worker():
|
||||
command = (
|
||||
"gunicorn "
|
||||
f"--bind 127.0.0.1:{PORT} "
|
||||
"--worker-class sanic.worker.GunicornWorker "
|
||||
"examples.hello_world:app"
|
||||
)
|
||||
worker = subprocess.Popen(shlex.split(command))
|
||||
time.sleep(2)
|
||||
yield
|
||||
worker.kill()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gunicorn_worker_with_access_logs():
|
||||
command = (
|
||||
"gunicorn "
|
||||
f"--bind 127.0.0.1:{PORT + 1} "
|
||||
"--worker-class sanic.worker.GunicornWorker "
|
||||
"examples.hello_world:app"
|
||||
)
|
||||
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
||||
time.sleep(2)
|
||||
return worker
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gunicorn_worker_with_env_var():
|
||||
command = (
|
||||
'env SANIC_ACCESS_LOG="False" '
|
||||
"gunicorn "
|
||||
f"--bind 127.0.0.1:{PORT + 2} "
|
||||
"--worker-class sanic.worker.GunicornWorker "
|
||||
"--log-level info "
|
||||
"examples.hello_world:app"
|
||||
)
|
||||
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
||||
time.sleep(2)
|
||||
return worker
|
||||
|
||||
|
||||
def test_gunicorn_worker(gunicorn_worker):
|
||||
with urllib.request.urlopen(f"http://localhost:{PORT}/") as f:
|
||||
res = json.loads(f.read(100).decode())
|
||||
assert res["test"]
|
||||
|
||||
|
||||
def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var):
|
||||
"""
|
||||
if SANIC_ACCESS_LOG was set to False do not show access logs
|
||||
"""
|
||||
with urllib.request.urlopen(f"http://localhost:{PORT + 2}/") as _:
|
||||
gunicorn_worker_with_env_var.kill()
|
||||
logs = list(
|
||||
filter(
|
||||
lambda x: b"sanic.access" in x,
|
||||
gunicorn_worker_with_env_var.stdout.read().split(b"\n"),
|
||||
)
|
||||
)
|
||||
assert len(logs) == 0
|
||||
|
||||
|
||||
def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs):
|
||||
"""
|
||||
default - show access logs
|
||||
"""
|
||||
with urllib.request.urlopen(f"http://localhost:{PORT + 1}/") as _:
|
||||
gunicorn_worker_with_access_logs.kill()
|
||||
assert (
|
||||
b"(sanic.access)[INFO][127.0.0.1"
|
||||
in gunicorn_worker_with_access_logs.stdout.read()
|
||||
)
|
||||
|
||||
|
||||
class GunicornTestWorker(GunicornWorker):
|
||||
def __init__(self):
|
||||
self.app = mock.Mock()
|
||||
self.app.callable = Sanic("test_gunicorn_worker")
|
||||
self.servers = {}
|
||||
self.exit_code = 0
|
||||
self.cfg = mock.Mock()
|
||||
self.notify = mock.Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def worker():
|
||||
return GunicornTestWorker()
|
||||
|
||||
|
||||
def test_worker_init_process(worker):
|
||||
with mock.patch("sanic.worker.asyncio") as mock_asyncio:
|
||||
try:
|
||||
worker.init_process()
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
assert mock_asyncio.get_event_loop.return_value.close.called
|
||||
assert mock_asyncio.new_event_loop.called
|
||||
assert mock_asyncio.set_event_loop.called
|
||||
|
||||
|
||||
def test_worker_init_signals(worker):
|
||||
worker.loop = mock.Mock()
|
||||
worker.init_signals()
|
||||
assert worker.loop.add_signal_handler.called
|
||||
|
||||
|
||||
def test_handle_abort(worker):
|
||||
with mock.patch("sanic.worker.sys") as mock_sys:
|
||||
worker.handle_abort(object(), object())
|
||||
assert not worker.alive
|
||||
assert worker.exit_code == 1
|
||||
mock_sys.exit.assert_called_with(1)
|
||||
|
||||
|
||||
def test_handle_quit(worker):
|
||||
worker.handle_quit(object(), object())
|
||||
assert not worker.alive
|
||||
assert worker.exit_code == 0
|
||||
|
||||
|
||||
async def _a_noop(*a, **kw):
|
||||
pass
|
||||
|
||||
|
||||
def test_run_max_requests_exceeded(worker):
|
||||
loop = asyncio.new_event_loop()
|
||||
worker.ppid = 1
|
||||
worker.alive = True
|
||||
sock = mock.Mock()
|
||||
sock.cfg_addr = ("localhost", 8080)
|
||||
worker.sockets = [sock]
|
||||
worker.wsgi = mock.Mock()
|
||||
worker.connections = set()
|
||||
worker.log = mock.Mock()
|
||||
worker.loop = loop
|
||||
worker.servers = {
|
||||
"server1": {"requests_count": 14},
|
||||
"server2": {"requests_count": 15},
|
||||
}
|
||||
worker.max_requests = 10
|
||||
worker._run = mock.Mock(wraps=_a_noop)
|
||||
|
||||
# exceeding request count
|
||||
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
|
||||
loop.run_until_complete(_runner)
|
||||
|
||||
assert not worker.alive
|
||||
worker.notify.assert_called_with()
|
||||
worker.log.info.assert_called_with(
|
||||
"Max requests exceeded, shutting " "down: %s", worker
|
||||
)
|
||||
|
||||
|
||||
def test_worker_close(worker):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.sleep = mock.Mock(wraps=_a_noop)
|
||||
worker.ppid = 1
|
||||
worker.pid = 2
|
||||
worker.cfg.graceful_timeout = 1.0
|
||||
worker.signal = mock.Mock()
|
||||
worker.signal.stopped = False
|
||||
worker.wsgi = mock.Mock()
|
||||
conn = mock.Mock()
|
||||
conn.websocket = mock.Mock()
|
||||
conn.websocket.fail_connection = mock.Mock(wraps=_a_noop)
|
||||
worker.connections = set([conn])
|
||||
worker.log = mock.Mock()
|
||||
worker.loop = loop
|
||||
server = mock.Mock()
|
||||
server.close = mock.Mock(wraps=lambda *a, **kw: None)
|
||||
server.wait_closed = mock.Mock(wraps=_a_noop)
|
||||
worker.servers = {server: {"requests_count": 14}}
|
||||
worker.max_requests = 10
|
||||
|
||||
# close worker
|
||||
_close = asyncio.ensure_future(worker.close(), loop=loop)
|
||||
loop.run_until_complete(_close)
|
||||
|
||||
assert worker.signal.stopped
|
||||
assert conn.websocket.fail_connection.called
|
||||
assert len(worker.servers) == 0
|
||||
Reference in New Issue
Block a user