From 21deebb78ae15bf528723f378cf4f0155a2c5484 Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Sun, 15 Aug 2021 14:55:04 -0400 Subject: [PATCH] Add 100% cover to uvicorn/main.py - Mostly test main as a cli client - Test main functions directly when it's impossible to do is as a cli client (i.e. app non string case) - Add no cover to the module entrypoint main call Related to: #102 --- tests/test_cli.py | 92 +++++++++++++++++++++++++++++++++++++++++++++- tests/test_main.py | 15 ++++++++ uvicorn/main.py | 2 +- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 706ed13a9..2f5d16a3b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,15 +1,40 @@ import importlib +import platform +import sys +from pathlib import Path from unittest import mock +import pytest from click.testing import CliRunner +import uvicorn +from uvicorn.config import Config from uvicorn.main import main as cli +from uvicorn.server import Server +from uvicorn.supervisors import ChangeReload, Multiprocess HEADERS = "Content-Security-Policy:default-src 'self'; script-src https://example.com" main = importlib.import_module("uvicorn.main") -def test_cli_headers(): +def test_cli_print_version() -> None: + runner = CliRunner() + + result = runner.invoke(cli, ["--version"]) + + assert result.exit_code == 0 + assert ( + "Running uvicorn %s with %s %s on %s" + % ( + uvicorn.__version__, + platform.python_implementation(), + platform.python_version(), + platform.system(), + ) + ) in result.output + + +def test_cli_headers() -> None: runner = CliRunner() with mock.patch.object(main, "run") as mock_run: @@ -26,5 +51,70 @@ def test_cli_headers(): ] +def test_cli_call_server_run() -> None: + runner = CliRunner() + + with mock.patch.object(Server, "run") as mock_run: + result = runner.invoke(cli, ["tests.test_cli:App"]) + + assert result.exit_code == 0 + mock_run.assert_called_once() + + +def test_cli_call_change_reload_run() -> None: + runner = CliRunner() + + with mock.patch.object(Config, "bind_socket") as mock_bind_socket: + with mock.patch.object(ChangeReload, "run") as mock_run: + result = runner.invoke(cli, ["tests.test_cli:App", "--reload"]) + + assert result.exit_code == 0 + mock_bind_socket.assert_called_once() + mock_run.assert_called_once() + + +def test_cli_call_multiprocess_run() -> None: + runner = CliRunner() + + with mock.patch.object(Config, "bind_socket") as mock_bind_socket: + with mock.patch.object(Multiprocess, "run") as mock_run: + result = runner.invoke(cli, ["tests.test_cli:App", "--workers=2"]) + + assert result.exit_code == 0 + mock_bind_socket.assert_called_once() + mock_run.assert_called_once() + + +@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system") +def test_cli_uds(tmp_path: Path) -> None: + runner = CliRunner() + uds_file = tmp_path / "uvicorn.sock" + uds_file.touch(exist_ok=True) + + with mock.patch.object(Config, "bind_socket") as mock_bind_socket: + with mock.patch.object(Multiprocess, "run") as mock_run: + result = runner.invoke( + cli, ["tests.test_cli:App", "--workers=2", "--uds", str(uds_file)] + ) + + assert result.exit_code == 0 + assert result.output == "" + mock_bind_socket.assert_called_once() + mock_run.assert_called_once() + assert not uds_file.exists() + + +def test_cli_incomplete_app_parameter() -> None: + runner = CliRunner() + + result = runner.invoke(cli, ["tests.test_cli"]) + + assert ( + 'Error loading ASGI app. Import string "tests.test_cli" ' + 'must be in format ":".' + ) in result.output + assert result.exit_code == 1 + + class App: pass diff --git a/tests/test_main.py b/tests/test_main.py index a4c5349ec..9f9662062 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,8 +1,11 @@ +from logging import WARNING + import httpx import pytest from tests.utils import run_server from uvicorn.config import Config +from uvicorn.main import run async def app(scope, receive, send): @@ -59,3 +62,15 @@ async def test_run_reload(): async with httpx.AsyncClient() as client: response = await client.get("http://127.0.0.1:8000") assert response.status_code == 204 + + +def test_run_invalid_app_config_combination(caplog: pytest.LogCaptureFixture) -> None: + with pytest.raises(SystemExit) as exit_exception: + run(app, reload=True) + assert exit_exception.value.code == 1 + assert caplog.records[-1].name == "uvicorn.error" + assert caplog.records[-1].levelno == WARNING + assert caplog.records[-1].message == ( + "You must pass the application as an import string to enable " + "'reload' or 'workers'." + ) diff --git a/uvicorn/main.py b/uvicorn/main.py index b3514b77e..9d48c562e 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -450,4 +450,4 @@ def run(app: typing.Union[ASGIApplication, str], **kwargs: typing.Any) -> None: if __name__ == "__main__": - main() + main() # pragma: no cover