Sanic Server WorkerManager refactor (#2499)
Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
This commit is contained in:
167
tests/worker/test_inspector.py
Normal file
167
tests/worker/test_inspector.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
from logging import ERROR, INFO
|
||||
from socket import AF_INET, SOCK_STREAM, timeout
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.log import Colors
|
||||
from sanic.worker.inspector import Inspector, inspect
|
||||
|
||||
|
||||
DATA = {
|
||||
"info": {
|
||||
"packages": ["foo"],
|
||||
},
|
||||
"extra": {
|
||||
"more": "data",
|
||||
},
|
||||
"workers": {"Worker-Name": {"some": "state"}},
|
||||
}
|
||||
SERIALIZED = json.dumps(DATA)
|
||||
|
||||
|
||||
def test_inspector_stop():
|
||||
inspector = Inspector(Mock(), {}, {}, "", 1)
|
||||
assert inspector.run is True
|
||||
inspector.stop()
|
||||
assert inspector.run is False
|
||||
|
||||
|
||||
@patch("sanic.worker.inspector.sys.stdout.write")
|
||||
@patch("sanic.worker.inspector.socket")
|
||||
@pytest.mark.parametrize("command", ("foo", "raw", "pretty"))
|
||||
def test_send_inspect(socket: Mock, write: Mock, command: str):
|
||||
socket.return_value = socket
|
||||
socket.__enter__.return_value = socket
|
||||
socket.recv.return_value = SERIALIZED.encode()
|
||||
inspect("localhost", 9999, command)
|
||||
|
||||
socket.sendall.assert_called_once_with(command.encode())
|
||||
socket.recv.assert_called_once_with(4096)
|
||||
socket.connect.assert_called_once_with(("localhost", 9999))
|
||||
socket.assert_called_once_with(AF_INET, SOCK_STREAM)
|
||||
|
||||
if command == "raw":
|
||||
write.assert_called_once_with(SERIALIZED)
|
||||
elif command == "pretty":
|
||||
write.assert_called()
|
||||
else:
|
||||
write.assert_not_called()
|
||||
|
||||
|
||||
@patch("sanic.worker.inspector.sys")
|
||||
@patch("sanic.worker.inspector.socket")
|
||||
def test_send_inspect_conn_refused(socket: Mock, sys: Mock, caplog):
|
||||
with caplog.at_level(INFO):
|
||||
socket.return_value = socket
|
||||
socket.__enter__.return_value = socket
|
||||
socket.connect.side_effect = ConnectionRefusedError()
|
||||
inspect("localhost", 9999, "foo")
|
||||
|
||||
socket.close.assert_called_once()
|
||||
sys.exit.assert_called_once_with(1)
|
||||
|
||||
message = (
|
||||
f"{Colors.RED}Could not connect to inspector at: "
|
||||
f"{Colors.YELLOW}('localhost', 9999){Colors.END}\n"
|
||||
"Either the application is not running, or it did not start "
|
||||
"an inspector instance."
|
||||
)
|
||||
assert ("sanic.error", ERROR, message) in caplog.record_tuples
|
||||
|
||||
|
||||
@patch("sanic.worker.inspector.configure_socket")
|
||||
@pytest.mark.parametrize("action", (b"reload", b"shutdown", b"foo"))
|
||||
def test_run_inspector(configure_socket: Mock, action: bytes):
|
||||
sock = Mock()
|
||||
conn = Mock()
|
||||
conn.recv.return_value = action
|
||||
configure_socket.return_value = sock
|
||||
inspector = Inspector(Mock(), {}, {}, "localhost", 9999)
|
||||
inspector.reload = Mock() # type: ignore
|
||||
inspector.shutdown = Mock() # type: ignore
|
||||
inspector.state_to_json = Mock(return_value="foo") # type: ignore
|
||||
|
||||
def accept():
|
||||
inspector.run = False
|
||||
return conn, ...
|
||||
|
||||
sock.accept = accept
|
||||
|
||||
inspector()
|
||||
|
||||
configure_socket.assert_called_once_with(
|
||||
{"host": "localhost", "port": 9999, "unix": None, "backlog": 1}
|
||||
)
|
||||
conn.recv.assert_called_with(64)
|
||||
|
||||
if action == b"reload":
|
||||
conn.send.assert_called_with(b"\n")
|
||||
inspector.reload.assert_called()
|
||||
inspector.shutdown.assert_not_called()
|
||||
inspector.state_to_json.assert_not_called()
|
||||
elif action == b"shutdown":
|
||||
conn.send.assert_called_with(b"\n")
|
||||
inspector.reload.assert_not_called()
|
||||
inspector.shutdown.assert_called()
|
||||
inspector.state_to_json.assert_not_called()
|
||||
else:
|
||||
conn.send.assert_called_with(b'"foo"')
|
||||
inspector.reload.assert_not_called()
|
||||
inspector.shutdown.assert_not_called()
|
||||
inspector.state_to_json.assert_called()
|
||||
|
||||
|
||||
@patch("sanic.worker.inspector.configure_socket")
|
||||
def test_accept_timeout(configure_socket: Mock):
|
||||
sock = Mock()
|
||||
configure_socket.return_value = sock
|
||||
inspector = Inspector(Mock(), {}, {}, "localhost", 9999)
|
||||
inspector.reload = Mock() # type: ignore
|
||||
inspector.shutdown = Mock() # type: ignore
|
||||
inspector.state_to_json = Mock(return_value="foo") # type: ignore
|
||||
|
||||
def accept():
|
||||
inspector.run = False
|
||||
raise timeout
|
||||
|
||||
sock.accept = accept
|
||||
|
||||
inspector()
|
||||
|
||||
inspector.reload.assert_not_called()
|
||||
inspector.shutdown.assert_not_called()
|
||||
inspector.state_to_json.assert_not_called()
|
||||
|
||||
|
||||
def test_state_to_json():
|
||||
now = datetime.now()
|
||||
now_iso = now.isoformat()
|
||||
app_info = {"app": "hello"}
|
||||
worker_state = {"Test": {"now": now, "nested": {"foo": now}}}
|
||||
inspector = Inspector(Mock(), app_info, worker_state, "", 0)
|
||||
state = inspector.state_to_json()
|
||||
|
||||
assert state == {
|
||||
"info": app_info,
|
||||
"workers": {"Test": {"now": now_iso, "nested": {"foo": now_iso}}},
|
||||
}
|
||||
|
||||
|
||||
def test_reload():
|
||||
publisher = Mock()
|
||||
inspector = Inspector(publisher, {}, {}, "", 0)
|
||||
inspector.reload()
|
||||
|
||||
publisher.send.assert_called_once_with("__ALL_PROCESSES__:")
|
||||
|
||||
|
||||
def test_shutdown():
|
||||
publisher = Mock()
|
||||
inspector = Inspector(publisher, {}, {}, "", 0)
|
||||
inspector.shutdown()
|
||||
|
||||
publisher.send.assert_called_once_with("__TERMINATE__")
|
||||
102
tests/worker/test_loader.py
Normal file
102
tests/worker/test_loader.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import sys
|
||||
|
||||
from os import getcwd
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.worker.loader import AppLoader, CertLoader
|
||||
|
||||
|
||||
STATIC = Path.cwd() / "tests" / "static"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"module_input", ("tests.fake.server:app", "tests.fake.server.app")
|
||||
)
|
||||
def test_load_app_instance(module_input):
|
||||
loader = AppLoader(module_input)
|
||||
app = loader.load()
|
||||
assert isinstance(app, Sanic)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"module_input",
|
||||
("tests.fake.server:create_app", "tests.fake.server:create_app()"),
|
||||
)
|
||||
def test_load_app_factory(module_input):
|
||||
loader = AppLoader(module_input, as_factory=True)
|
||||
app = loader.load()
|
||||
assert isinstance(app, Sanic)
|
||||
|
||||
|
||||
def test_load_app_simple():
|
||||
loader = AppLoader(str(STATIC), as_simple=True)
|
||||
app = loader.load()
|
||||
assert isinstance(app, Sanic)
|
||||
|
||||
|
||||
def test_create_with_factory():
|
||||
loader = AppLoader(factory=lambda: Sanic("Test"))
|
||||
app = loader.load()
|
||||
assert isinstance(app, Sanic)
|
||||
|
||||
|
||||
def test_cwd_in_path():
|
||||
AppLoader("tests.fake.server:app").load()
|
||||
assert getcwd() in sys.path
|
||||
|
||||
|
||||
def test_input_is_dir():
|
||||
loader = AppLoader(str(STATIC))
|
||||
message = (
|
||||
"App not found.\n Please use --simple if you are passing a "
|
||||
f"directory to sanic.\n eg. sanic {str(STATIC)} --simple"
|
||||
)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
loader.load()
|
||||
|
||||
|
||||
def test_input_is_factory():
|
||||
ns = SimpleNamespace(module="foo")
|
||||
loader = AppLoader("tests.fake.server:create_app", args=ns)
|
||||
message = (
|
||||
"Module is not a Sanic app, it is a function\n If this callable "
|
||||
"returns a Sanic instance try: \nsanic foo --factory"
|
||||
)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
loader.load()
|
||||
|
||||
|
||||
def test_input_is_module():
|
||||
ns = SimpleNamespace(module="foo")
|
||||
loader = AppLoader("tests.fake.server", args=ns)
|
||||
message = (
|
||||
"Module is not a Sanic app, it is a module\n "
|
||||
"Perhaps you meant foo:app?"
|
||||
)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
loader.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("creator", ("mkcert", "trustme"))
|
||||
@patch("sanic.worker.loader.TrustmeCreator")
|
||||
@patch("sanic.worker.loader.MkcertCreator")
|
||||
def test_cert_loader(MkcertCreator: Mock, TrustmeCreator: Mock, creator: str):
|
||||
MkcertCreator.return_value = MkcertCreator
|
||||
TrustmeCreator.return_value = TrustmeCreator
|
||||
data = {
|
||||
"creator": creator,
|
||||
"key": Path.cwd() / "tests" / "certs" / "localhost" / "privkey.pem",
|
||||
"cert": Path.cwd() / "tests" / "certs" / "localhost" / "fullchain.pem",
|
||||
"localhost": "localhost",
|
||||
}
|
||||
app = Sanic("Test")
|
||||
loader = CertLoader(data) # type: ignore
|
||||
loader.load(app)
|
||||
creator_class = MkcertCreator if creator == "mkcert" else TrustmeCreator
|
||||
creator_class.assert_called_once_with(app, data["key"], data["cert"])
|
||||
creator_class.generate_cert.assert_called_once_with("localhost")
|
||||
217
tests/worker/test_manager.py
Normal file
217
tests/worker/test_manager.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from signal import SIGINT, SIGKILL
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.worker.manager import WorkerManager
|
||||
|
||||
|
||||
def fake_serve():
|
||||
...
|
||||
|
||||
|
||||
def test_manager_no_workers():
|
||||
message = "Cannot serve with no workers"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
WorkerManager(
|
||||
0,
|
||||
fake_serve,
|
||||
{},
|
||||
Mock(),
|
||||
(Mock(), Mock()),
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@patch("sanic.worker.process.os")
|
||||
def test_terminate(os_mock: Mock):
|
||||
process = Mock()
|
||||
process.pid = 1234
|
||||
context = Mock()
|
||||
context.Process.return_value = process
|
||||
manager = WorkerManager(
|
||||
1,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), Mock()),
|
||||
{},
|
||||
)
|
||||
assert manager.terminated is False
|
||||
manager.terminate()
|
||||
assert manager.terminated is True
|
||||
os_mock.kill.assert_called_once_with(1234, SIGINT)
|
||||
|
||||
|
||||
@patch("sanic.worker.process.os")
|
||||
def test_shutown(os_mock: Mock):
|
||||
process = Mock()
|
||||
process.pid = 1234
|
||||
process.is_alive.return_value = True
|
||||
context = Mock()
|
||||
context.Process.return_value = process
|
||||
manager = WorkerManager(
|
||||
1,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), Mock()),
|
||||
{},
|
||||
)
|
||||
manager.shutdown()
|
||||
os_mock.kill.assert_called_once_with(1234, SIGINT)
|
||||
|
||||
|
||||
@patch("sanic.worker.manager.os")
|
||||
def test_kill(os_mock: Mock):
|
||||
process = Mock()
|
||||
process.pid = 1234
|
||||
context = Mock()
|
||||
context.Process.return_value = process
|
||||
manager = WorkerManager(
|
||||
1,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), Mock()),
|
||||
{},
|
||||
)
|
||||
manager.kill()
|
||||
os_mock.kill.assert_called_once_with(1234, SIGKILL)
|
||||
|
||||
|
||||
def test_restart_all():
|
||||
p1 = Mock()
|
||||
p2 = Mock()
|
||||
context = Mock()
|
||||
context.Process.side_effect = [p1, p2, p1, p2]
|
||||
manager = WorkerManager(
|
||||
2,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), Mock()),
|
||||
{},
|
||||
)
|
||||
assert len(list(manager.transient_processes))
|
||||
manager.restart()
|
||||
p1.terminate.assert_called_once()
|
||||
p2.terminate.assert_called_once()
|
||||
context.Process.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
name="Sanic-Server-0-0",
|
||||
target=fake_serve,
|
||||
kwargs={"config": {}},
|
||||
daemon=True,
|
||||
),
|
||||
call(
|
||||
name="Sanic-Server-1-0",
|
||||
target=fake_serve,
|
||||
kwargs={"config": {}},
|
||||
daemon=True,
|
||||
),
|
||||
call(
|
||||
name="Sanic-Server-0-0",
|
||||
target=fake_serve,
|
||||
kwargs={"config": {}},
|
||||
daemon=True,
|
||||
),
|
||||
call(
|
||||
name="Sanic-Server-1-0",
|
||||
target=fake_serve,
|
||||
kwargs={"config": {}},
|
||||
daemon=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_monitor_all():
|
||||
p1 = Mock()
|
||||
p2 = Mock()
|
||||
sub = Mock()
|
||||
sub.recv.side_effect = ["__ALL_PROCESSES__:", ""]
|
||||
context = Mock()
|
||||
context.Process.side_effect = [p1, p2]
|
||||
manager = WorkerManager(
|
||||
2,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), sub),
|
||||
{},
|
||||
)
|
||||
manager.restart = Mock() # type: ignore
|
||||
manager.wait_for_ack = Mock() # type: ignore
|
||||
manager.monitor()
|
||||
|
||||
manager.restart.assert_called_once_with(
|
||||
process_names=None, reloaded_files=""
|
||||
)
|
||||
|
||||
|
||||
def test_monitor_all_with_files():
|
||||
p1 = Mock()
|
||||
p2 = Mock()
|
||||
sub = Mock()
|
||||
sub.recv.side_effect = ["__ALL_PROCESSES__:foo,bar", ""]
|
||||
context = Mock()
|
||||
context.Process.side_effect = [p1, p2]
|
||||
manager = WorkerManager(
|
||||
2,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), sub),
|
||||
{},
|
||||
)
|
||||
manager.restart = Mock() # type: ignore
|
||||
manager.wait_for_ack = Mock() # type: ignore
|
||||
manager.monitor()
|
||||
|
||||
manager.restart.assert_called_once_with(
|
||||
process_names=None, reloaded_files="foo,bar"
|
||||
)
|
||||
|
||||
|
||||
def test_monitor_one_process():
|
||||
p1 = Mock()
|
||||
p1.name = "Testing"
|
||||
p2 = Mock()
|
||||
sub = Mock()
|
||||
sub.recv.side_effect = [f"{p1.name}:foo,bar", ""]
|
||||
context = Mock()
|
||||
context.Process.side_effect = [p1, p2]
|
||||
manager = WorkerManager(
|
||||
2,
|
||||
fake_serve,
|
||||
{},
|
||||
context,
|
||||
(Mock(), sub),
|
||||
{},
|
||||
)
|
||||
manager.restart = Mock() # type: ignore
|
||||
manager.wait_for_ack = Mock() # type: ignore
|
||||
manager.monitor()
|
||||
|
||||
manager.restart.assert_called_once_with(
|
||||
process_names=[p1.name], reloaded_files="foo,bar"
|
||||
)
|
||||
|
||||
|
||||
def test_shutdown_signal():
|
||||
pub = Mock()
|
||||
manager = WorkerManager(
|
||||
1,
|
||||
fake_serve,
|
||||
{},
|
||||
Mock(),
|
||||
(pub, Mock()),
|
||||
{},
|
||||
)
|
||||
manager.shutdown = Mock() # type: ignore
|
||||
|
||||
manager.shutdown_signal(SIGINT, None)
|
||||
pub.send.assert_called_with(None)
|
||||
manager.shutdown.assert_called_once_with()
|
||||
119
tests/worker/test_multiplexer.py
Normal file
119
tests/worker/test_multiplexer.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from multiprocessing import Event
|
||||
from os import environ, getpid
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.worker.multiplexer import WorkerMultiplexer
|
||||
from sanic.worker.state import WorkerState
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monitor_publisher():
|
||||
return Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def worker_state():
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def m(monitor_publisher, worker_state):
|
||||
environ["SANIC_WORKER_NAME"] = "Test"
|
||||
worker_state["Test"] = {}
|
||||
yield WorkerMultiplexer(monitor_publisher, worker_state)
|
||||
del environ["SANIC_WORKER_NAME"]
|
||||
|
||||
|
||||
def test_has_multiplexer_default(app: Sanic):
|
||||
event = Event()
|
||||
|
||||
@app.main_process_start
|
||||
async def setup(app, _):
|
||||
app.shared_ctx.event = event
|
||||
|
||||
@app.after_server_start
|
||||
def stop(app):
|
||||
if hasattr(app, "m") and isinstance(app.m, WorkerMultiplexer):
|
||||
app.shared_ctx.event.set()
|
||||
app.stop()
|
||||
|
||||
app.run()
|
||||
|
||||
assert event.is_set()
|
||||
|
||||
|
||||
def test_not_have_multiplexer_single(app: Sanic):
|
||||
event = Event()
|
||||
|
||||
@app.main_process_start
|
||||
async def setup(app, _):
|
||||
app.shared_ctx.event = event
|
||||
|
||||
@app.after_server_start
|
||||
def stop(app):
|
||||
if hasattr(app, "m") and isinstance(app.m, WorkerMultiplexer):
|
||||
app.shared_ctx.event.set()
|
||||
app.stop()
|
||||
|
||||
app.run(single_process=True)
|
||||
|
||||
assert not event.is_set()
|
||||
|
||||
|
||||
def test_not_have_multiplexer_legacy(app: Sanic):
|
||||
event = Event()
|
||||
|
||||
@app.main_process_start
|
||||
async def setup(app, _):
|
||||
app.shared_ctx.event = event
|
||||
|
||||
@app.after_server_start
|
||||
def stop(app):
|
||||
if hasattr(app, "m") and isinstance(app.m, WorkerMultiplexer):
|
||||
app.shared_ctx.event.set()
|
||||
app.stop()
|
||||
|
||||
app.run(legacy=True)
|
||||
|
||||
assert not event.is_set()
|
||||
|
||||
|
||||
def test_ack(worker_state: Dict[str, Any], m: WorkerMultiplexer):
|
||||
worker_state["Test"] = {"foo": "bar"}
|
||||
m.ack()
|
||||
assert worker_state["Test"] == {"foo": "bar", "state": "ACKED"}
|
||||
|
||||
|
||||
def test_restart_self(monitor_publisher: Mock, m: WorkerMultiplexer):
|
||||
m.restart()
|
||||
monitor_publisher.send.assert_called_once_with("Test")
|
||||
|
||||
|
||||
def test_restart_foo(monitor_publisher: Mock, m: WorkerMultiplexer):
|
||||
m.restart("foo")
|
||||
monitor_publisher.send.assert_called_once_with("foo")
|
||||
|
||||
|
||||
def test_reload_alias(monitor_publisher: Mock, m: WorkerMultiplexer):
|
||||
m.reload()
|
||||
monitor_publisher.send.assert_called_once_with("Test")
|
||||
|
||||
|
||||
def test_terminate(monitor_publisher: Mock, m: WorkerMultiplexer):
|
||||
m.terminate()
|
||||
monitor_publisher.send.assert_called_once_with("__TERMINATE__")
|
||||
|
||||
|
||||
def test_properties(
|
||||
monitor_publisher: Mock, worker_state: Dict[str, Any], m: WorkerMultiplexer
|
||||
):
|
||||
assert m.reload == m.restart
|
||||
assert m.pid == getpid()
|
||||
assert m.name == "Test"
|
||||
assert m.workers == worker_state
|
||||
assert m.state == worker_state["Test"]
|
||||
assert isinstance(m.state, WorkerState)
|
||||
156
tests/worker/test_reloader.py
Normal file
156
tests/worker/test_reloader.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import signal
|
||||
|
||||
from asyncio import Event
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.worker.loader import AppLoader
|
||||
from sanic.worker.reloader import Reloader
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reloader():
|
||||
...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = Sanic("Test")
|
||||
|
||||
@app.route("/")
|
||||
def handler(_):
|
||||
...
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_loader(app):
|
||||
return AppLoader(factory=lambda: app)
|
||||
|
||||
|
||||
def run_reloader(reloader):
|
||||
def stop(*_):
|
||||
reloader.stop()
|
||||
|
||||
signal.signal(signal.SIGALRM, stop)
|
||||
signal.alarm(1)
|
||||
reloader()
|
||||
|
||||
|
||||
def is_python_file(filename):
|
||||
return (isinstance(filename, Path) and (filename.suffix == "py")) or (
|
||||
isinstance(filename, str) and filename.endswith(".py")
|
||||
)
|
||||
|
||||
|
||||
def test_reload_send():
|
||||
publisher = Mock()
|
||||
reloader = Reloader(publisher, 0.1, set(), Mock())
|
||||
reloader.reload("foobar")
|
||||
publisher.send.assert_called_once_with("__ALL_PROCESSES__:foobar")
|
||||
|
||||
|
||||
def test_iter_files():
|
||||
reloader = Reloader(Mock(), 0.1, set(), Mock())
|
||||
len_python_files = len(list(reloader.files()))
|
||||
assert len_python_files > 0
|
||||
|
||||
static_dir = Path(__file__).parent.parent / "static"
|
||||
len_static_files = len(list(static_dir.glob("**/*")))
|
||||
reloader = Reloader(Mock(), 0.1, set({static_dir}), Mock())
|
||||
len_total_files = len(list(reloader.files()))
|
||||
assert len_static_files > 0
|
||||
assert len_total_files == len_python_files + len_static_files
|
||||
|
||||
|
||||
def test_reloader_triggers_start_stop_listeners(
|
||||
app: Sanic, app_loader: AppLoader
|
||||
):
|
||||
results = []
|
||||
|
||||
@app.reload_process_start
|
||||
def reload_process_start(_):
|
||||
results.append("reload_process_start")
|
||||
|
||||
@app.reload_process_stop
|
||||
def reload_process_stop(_):
|
||||
results.append("reload_process_stop")
|
||||
|
||||
reloader = Reloader(Mock(), 0.1, set(), app_loader)
|
||||
run_reloader(reloader)
|
||||
|
||||
assert results == ["reload_process_start", "reload_process_stop"]
|
||||
|
||||
|
||||
def test_not_triggered(app_loader):
|
||||
reload_dir = Path(__file__).parent.parent / "fake"
|
||||
publisher = Mock()
|
||||
reloader = Reloader(publisher, 0.1, {reload_dir}, app_loader)
|
||||
run_reloader(reloader)
|
||||
|
||||
publisher.send.assert_not_called()
|
||||
|
||||
|
||||
def test_triggered(app_loader):
|
||||
paths = set()
|
||||
|
||||
def check_file(filename, mtimes):
|
||||
if (isinstance(filename, Path) and (filename.name == "server.py")) or (
|
||||
isinstance(filename, str) and "sanic/app.py" in filename
|
||||
):
|
||||
paths.add(str(filename))
|
||||
return True
|
||||
return False
|
||||
|
||||
reload_dir = Path(__file__).parent.parent / "fake"
|
||||
publisher = Mock()
|
||||
reloader = Reloader(publisher, 0.1, {reload_dir}, app_loader)
|
||||
reloader.check_file = check_file # type: ignore
|
||||
run_reloader(reloader)
|
||||
|
||||
assert len(paths) == 2
|
||||
|
||||
publisher.send.assert_called()
|
||||
call_arg = publisher.send.call_args_list[0][0][0]
|
||||
assert call_arg.startswith("__ALL_PROCESSES__:")
|
||||
assert call_arg.count(",") == 1
|
||||
for path in paths:
|
||||
assert str(path) in call_arg
|
||||
|
||||
|
||||
def test_reloader_triggers_reload_listeners(app: Sanic, app_loader: AppLoader):
|
||||
before = Event()
|
||||
after = Event()
|
||||
|
||||
def check_file(filename, mtimes):
|
||||
return not after.is_set()
|
||||
|
||||
@app.before_reload_trigger
|
||||
async def before_reload_trigger(_):
|
||||
before.set()
|
||||
|
||||
@app.after_reload_trigger
|
||||
async def after_reload_trigger(_):
|
||||
after.set()
|
||||
|
||||
reloader = Reloader(Mock(), 0.1, set(), app_loader)
|
||||
reloader.check_file = check_file # type: ignore
|
||||
run_reloader(reloader)
|
||||
|
||||
assert before.is_set()
|
||||
assert after.is_set()
|
||||
|
||||
|
||||
def test_check_file(tmp_path):
|
||||
current = tmp_path / "testing.txt"
|
||||
current.touch()
|
||||
mtimes = {}
|
||||
assert Reloader.check_file(current, mtimes) is False
|
||||
assert len(mtimes) == 1
|
||||
assert Reloader.check_file(current, mtimes) is False
|
||||
mtimes[current] = mtimes[current] - 1
|
||||
assert Reloader.check_file(current, mtimes) is True
|
||||
53
tests/worker/test_runner.py
Normal file
53
tests/worker/test_runner.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.server.runners import _run_server_forever, serve
|
||||
|
||||
|
||||
@patch("sanic.server.runners._serve_http_1")
|
||||
@patch("sanic.server.runners._serve_http_3")
|
||||
def test_run_http_1(_serve_http_3: Mock, _serve_http_1: Mock, app: Sanic):
|
||||
serve("", 0, app)
|
||||
_serve_http_3.assert_not_called()
|
||||
_serve_http_1.assert_called_once()
|
||||
|
||||
|
||||
@patch("sanic.server.runners._serve_http_1")
|
||||
@patch("sanic.server.runners._serve_http_3")
|
||||
def test_run_http_3(_serve_http_3: Mock, _serve_http_1: Mock, app: Sanic):
|
||||
serve("", 0, app, version=HTTP.VERSION_3)
|
||||
_serve_http_1.assert_not_called()
|
||||
_serve_http_3.assert_called_once()
|
||||
|
||||
|
||||
@patch("sanic.server.runners.remove_unix_socket")
|
||||
@pytest.mark.parametrize("do_cleanup", (True, False))
|
||||
def test_run_server_forever(remove_unix_socket: Mock, do_cleanup: bool):
|
||||
loop = Mock()
|
||||
cleanup = Mock()
|
||||
loop.run_forever = Mock(side_effect=KeyboardInterrupt())
|
||||
before_stop = Mock()
|
||||
before_stop.return_value = Mock()
|
||||
after_stop = Mock()
|
||||
after_stop.return_value = Mock()
|
||||
unix = Mock()
|
||||
|
||||
_run_server_forever(
|
||||
loop, before_stop, after_stop, cleanup if do_cleanup else None, unix
|
||||
)
|
||||
|
||||
loop.run_forever.assert_called_once_with()
|
||||
loop.run_until_complete.assert_has_calls(
|
||||
[call(before_stop.return_value), call(after_stop.return_value)]
|
||||
)
|
||||
|
||||
if do_cleanup:
|
||||
cleanup.assert_called_once_with()
|
||||
else:
|
||||
cleanup.assert_not_called()
|
||||
|
||||
remove_unix_socket.assert_called_once_with(unix)
|
||||
loop.close.assert_called_once_with()
|
||||
82
tests/worker/test_shared_ctx.py
Normal file
82
tests/worker/test_shared_ctx.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# 18
|
||||
# 21-29
|
||||
# 26
|
||||
# 36-37
|
||||
# 42
|
||||
# 55
|
||||
# 38->
|
||||
|
||||
import logging
|
||||
|
||||
from ctypes import c_int32
|
||||
from multiprocessing import Pipe, Queue, Value
|
||||
from os import environ
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.types.shared_ctx import SharedContext
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"item,okay",
|
||||
(
|
||||
(Pipe(), True),
|
||||
(Value("i", 0), True),
|
||||
(Queue(), True),
|
||||
(c_int32(1), True),
|
||||
(1, False),
|
||||
("thing", False),
|
||||
(object(), False),
|
||||
),
|
||||
)
|
||||
def test_set_items(item: Any, okay: bool, caplog):
|
||||
ctx = SharedContext()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
ctx.item = item
|
||||
|
||||
assert ctx.is_locked is False
|
||||
assert len(caplog.record_tuples) == 0 if okay else 1
|
||||
if not okay:
|
||||
assert caplog.record_tuples[0][0] == "sanic.error"
|
||||
assert caplog.record_tuples[0][1] == logging.WARNING
|
||||
assert "Unsafe object" in caplog.record_tuples[0][2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"item",
|
||||
(
|
||||
Pipe(),
|
||||
Value("i", 0),
|
||||
Queue(),
|
||||
c_int32(1),
|
||||
1,
|
||||
"thing",
|
||||
object(),
|
||||
),
|
||||
)
|
||||
def test_set_items_in_worker(item: Any, caplog):
|
||||
ctx = SharedContext()
|
||||
|
||||
environ["SANIC_WORKER_NAME"] = "foo"
|
||||
with caplog.at_level(logging.INFO):
|
||||
ctx.item = item
|
||||
del environ["SANIC_WORKER_NAME"]
|
||||
|
||||
assert ctx.is_locked is False
|
||||
assert len(caplog.record_tuples) == 0
|
||||
|
||||
|
||||
def test_lock():
|
||||
ctx = SharedContext()
|
||||
|
||||
assert ctx.is_locked is False
|
||||
|
||||
ctx.lock()
|
||||
|
||||
assert ctx.is_locked is True
|
||||
|
||||
message = "Cannot set item on locked SharedContext object"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
ctx.item = 1
|
||||
27
tests/worker/test_socket.py
Normal file
27
tests/worker/test_socket.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sanic.server.socket import (
|
||||
bind_unix_socket,
|
||||
configure_socket,
|
||||
remove_unix_socket,
|
||||
)
|
||||
|
||||
|
||||
def test_setup_and_teardown_unix():
|
||||
socket_address = "./test.sock"
|
||||
path = Path.cwd() / socket_address
|
||||
assert not path.exists()
|
||||
bind_unix_socket(socket_address)
|
||||
assert path.exists()
|
||||
remove_unix_socket(socket_address)
|
||||
assert not path.exists()
|
||||
|
||||
|
||||
def test_configure_socket():
|
||||
socket_address = "./test.sock"
|
||||
path = Path.cwd() / socket_address
|
||||
assert not path.exists()
|
||||
configure_socket({"unix": socket_address, "backlog": 100})
|
||||
assert path.exists()
|
||||
remove_unix_socket(socket_address)
|
||||
assert not path.exists()
|
||||
91
tests/worker/test_state.py
Normal file
91
tests/worker/test_state.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import pytest
|
||||
|
||||
from sanic.worker.state import WorkerState
|
||||
|
||||
|
||||
def gen_state(**kwargs):
|
||||
return WorkerState({"foo": kwargs}, "foo")
|
||||
|
||||
|
||||
def test_set_get_state():
|
||||
state = gen_state()
|
||||
state["additional"] = 123
|
||||
assert state["additional"] == 123
|
||||
assert state.get("additional") == 123
|
||||
assert state._state == {"foo": {"additional": 123}}
|
||||
|
||||
|
||||
def test_del_state():
|
||||
state = gen_state(one=1)
|
||||
assert state["one"] == 1
|
||||
del state["one"]
|
||||
assert state._state == {"foo": {}}
|
||||
|
||||
|
||||
def test_iter_state():
|
||||
result = [item for item in gen_state(one=1, two=2)]
|
||||
assert result == ["one", "two"]
|
||||
|
||||
|
||||
def test_state_len():
|
||||
result = [item for item in gen_state(one=1, two=2)]
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_state_repr():
|
||||
assert repr(gen_state(one=1, two=2)) == repr({"one": 1, "two": 2})
|
||||
|
||||
|
||||
def test_state_eq():
|
||||
state = gen_state(one=1, two=2)
|
||||
assert state == {"one": 1, "two": 2}
|
||||
assert state != {"one": 1}
|
||||
|
||||
|
||||
def test_state_keys():
|
||||
assert list(gen_state(one=1, two=2).keys()) == list(
|
||||
{"one": 1, "two": 2}.keys()
|
||||
)
|
||||
|
||||
|
||||
def test_state_values():
|
||||
assert list(gen_state(one=1, two=2).values()) == list(
|
||||
{"one": 1, "two": 2}.values()
|
||||
)
|
||||
|
||||
|
||||
def test_state_items():
|
||||
assert list(gen_state(one=1, two=2).items()) == list(
|
||||
{"one": 1, "two": 2}.items()
|
||||
)
|
||||
|
||||
|
||||
def test_state_update():
|
||||
state = gen_state()
|
||||
assert len(state) == 0
|
||||
state.update({"nine": 9})
|
||||
assert len(state) == 1
|
||||
assert state["nine"] == 9
|
||||
|
||||
|
||||
def test_state_pop():
|
||||
state = gen_state(one=1)
|
||||
with pytest.raises(NotImplementedError):
|
||||
state.pop()
|
||||
|
||||
|
||||
def test_state_full():
|
||||
state = gen_state(one=1)
|
||||
assert state.full() == {"foo": {"one": 1}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", WorkerState.RESTRICTED)
|
||||
def test_state_restricted_operation(key):
|
||||
state = gen_state()
|
||||
message = f"Cannot set restricted key on WorkerState: {key}"
|
||||
with pytest.raises(LookupError, match=message):
|
||||
state[key] = "Nope"
|
||||
del state[key]
|
||||
|
||||
with pytest.raises(LookupError, match=message):
|
||||
state.update({"okay": True, key: "bad"})
|
||||
113
tests/worker/test_worker_serve.py
Normal file
113
tests/worker/test_worker_serve.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from os import environ
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.worker.loader import AppLoader
|
||||
from sanic.worker.multiplexer import WorkerMultiplexer
|
||||
from sanic.worker.serve import worker_serve
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app():
|
||||
app = Mock()
|
||||
server_info = Mock()
|
||||
server_info.settings = {"app": app}
|
||||
app.state.workers = 1
|
||||
app.listeners = {"main_process_ready": []}
|
||||
app.get_motd_data.return_value = ({"packages": ""}, {})
|
||||
app.state.server_info = [server_info]
|
||||
return app
|
||||
|
||||
|
||||
def args(app, **kwargs):
|
||||
params = {**kwargs}
|
||||
params.setdefault("host", "127.0.0.1")
|
||||
params.setdefault("port", 9999)
|
||||
params.setdefault("app_name", "test_config_app")
|
||||
params.setdefault("monitor_publisher", None)
|
||||
params.setdefault("app_loader", AppLoader(factory=lambda: app))
|
||||
return params
|
||||
|
||||
|
||||
def test_config_app(mock_app: Mock):
|
||||
with patch("sanic.worker.serve._serve_http_1"):
|
||||
worker_serve(**args(mock_app, config={"FOO": "BAR"}))
|
||||
mock_app.update_config.assert_called_once_with({"FOO": "BAR"})
|
||||
|
||||
|
||||
def test_bad_process(mock_app: Mock):
|
||||
environ["SANIC_WORKER_NAME"] = "FOO"
|
||||
|
||||
message = "No restart publisher found in worker process"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
worker_serve(**args(mock_app))
|
||||
|
||||
message = "No worker state found in worker process"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
worker_serve(**args(mock_app, monitor_publisher=Mock()))
|
||||
|
||||
del environ["SANIC_WORKER_NAME"]
|
||||
|
||||
|
||||
def test_has_multiplexer(app: Sanic):
|
||||
environ["SANIC_WORKER_NAME"] = "FOO"
|
||||
|
||||
Sanic.register_app(app)
|
||||
with patch("sanic.worker.serve._serve_http_1"):
|
||||
worker_serve(
|
||||
**args(app, monitor_publisher=Mock(), worker_state=Mock())
|
||||
)
|
||||
assert isinstance(app.multiplexer, WorkerMultiplexer)
|
||||
|
||||
del environ["SANIC_WORKER_NAME"]
|
||||
|
||||
|
||||
@patch("sanic.mixins.startup.WorkerManager")
|
||||
def test_serve_app_implicit(wm: Mock, app):
|
||||
app.prepare()
|
||||
Sanic.serve()
|
||||
wm.call_args[0] == app.state.workers
|
||||
|
||||
|
||||
@patch("sanic.mixins.startup.WorkerManager")
|
||||
def test_serve_app_explicit(wm: Mock, mock_app):
|
||||
Sanic.serve(mock_app)
|
||||
wm.call_args[0] == mock_app.state.workers
|
||||
|
||||
|
||||
@patch("sanic.mixins.startup.WorkerManager")
|
||||
def test_serve_app_loader(wm: Mock, mock_app):
|
||||
Sanic.serve(app_loader=AppLoader(factory=lambda: mock_app))
|
||||
wm.call_args[0] == mock_app.state.workers
|
||||
# Sanic.serve(factory=lambda: mock_app)
|
||||
|
||||
|
||||
@patch("sanic.mixins.startup.WorkerManager")
|
||||
def test_serve_app_factory(wm: Mock, mock_app):
|
||||
Sanic.serve(factory=lambda: mock_app)
|
||||
wm.call_args[0] == mock_app.state.workers
|
||||
|
||||
|
||||
@patch("sanic.mixins.startup.WorkerManager")
|
||||
@patch("sanic.mixins.startup.Inspector")
|
||||
@pytest.mark.parametrize("config", (True, False))
|
||||
def test_serve_with_inspector(
|
||||
Inspector: Mock, WorkerManager: Mock, mock_app: Mock, config: bool
|
||||
):
|
||||
mock_app.config.INSPECTOR = config
|
||||
inspector = Mock()
|
||||
Inspector.return_value = inspector
|
||||
WorkerManager.return_value = WorkerManager
|
||||
|
||||
Sanic.serve(mock_app)
|
||||
|
||||
if config:
|
||||
Inspector.assert_called_once()
|
||||
WorkerManager.manage.assert_called_once_with(
|
||||
"Inspector", inspector, {}, transient=False
|
||||
)
|
||||
else:
|
||||
Inspector.assert_not_called()
|
||||
WorkerManager.manage.assert_not_called()
|
||||
Reference in New Issue
Block a user