Sanic Server WorkerManager refactor (#2499)

Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
This commit is contained in:
Adam Hopkins
2022-09-18 17:17:23 +03:00
committed by GitHub
parent d352a4155e
commit 4726cf1910
73 changed files with 3929 additions and 940 deletions

View 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
View 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")

View 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()

View 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)

View 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

View 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()

View 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

View 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()

View 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"})

View 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()