Skip to content

Commit

Permalink
Add tests for start.py start_server()
Browse files Browse the repository at this point in the history
- Add `PROCESS_MANAGER` environment variable: previously, the process
  manager was determined by the environment variable `WITH_RELOAD`.
  `WITH_RELOAD="true"` would start Uvicorn alone with reloading, or
  `WITH_RELOAD="false"` would run Uvicorn with Gunicorn as process
  manager without reloading.
- Reconfigure start.py `start_server()` for process managers
- Update README for new `PROCESS_MANAGER` environment variable
- Update debugger config for new `PROCESS_MANAGER` environment variable
- Add tests for start.py `start_server()`
- Parametrize tests for all app modules (Base ASGI, FastAPI, Starlette)
  • Loading branch information
br3ndonland committed Aug 30, 2020
1 parent 5821174 commit 92e6b68
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 7 deletions.
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"LOG_FORMAT": "uvicorn",
"LOG_LEVEL": "debug",
"LOGGING_CONF": "logging_conf.py",
"PROCESS_MANAGER": "uvicorn",
"WITH_RELOAD": "true"
},
"console": "integratedTerminal",
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ ENV APP_MODULE="custom.module:api" WORKERS_PER_CORE="2"
- [`BIND`](https://docs.gunicorn.org/en/latest/settings.html#server-socket): The actual host and port passed to Gunicorn.
- Default: `HOST:PORT` (`"0.0.0.0:80"`)
- Custom: `BIND="0.0.0.0:8080"`
- `PROCESS_MANAGER`: Manager for Uvicorn worker processes. As described in the [Uvicorn docs](https://www.uvicorn.org), "Uvicorn includes a Gunicorn worker class allowing you to run ASGI applications, with all of Uvicorn's performance benefits, while also giving you Gunicorn's fully-featured process management."
- Default: `"gunicorn"` (run Uvicorn with Gunicorn as the process manager)
- Custom: `"uvicorn"` (run Uvicorn alone for local development)
- [`WORKER_CLASS`](https://docs.gunicorn.org/en/latest/settings.html#worker-processes): The class to be used by Gunicorn for the workers.
- Default: `uvicorn.workers.UvicornWorker`
- Custom: For the alternate Uvicorn worker, `WORKER_CLASS="uvicorn.workers.UvicornH11Worker"`
Expand Down
27 changes: 20 additions & 7 deletions inboard/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,28 +100,41 @@ def start_server(
gunicorn_conf: Path = Path("/gunicorn_conf.py"),
logger: Logger = logging.getLogger(),
logging_conf_dict: Dict[str, Any] = None,
process_manager: str = str(os.getenv("PROCESS_MANAGER", "gunicorn")),
with_reload: bool = bool(os.getenv("WITH_RELOAD", False)),
worker_class: str = str(os.getenv("WORKER_CLASS", "uvicorn.workers.UvicornWorker")),
) -> None:
"""Start the Uvicorn or Gunicorn server."""
try:
if with_reload:
logger.debug("Running Uvicorn without Gunicorn and with reloading")
if process_manager == "gunicorn":
logger.debug("Running Uvicorn with Gunicorn.")
subprocess.run(
[
"gunicorn",
"-k",
worker_class,
"-c",
gunicorn_conf.name,
app_module,
"--reload",
str(with_reload),
]
)
elif process_manager == "uvicorn":
logger.debug("Running Uvicorn without Gunicorn.")
uvicorn.run(
app_module,
host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv("PORT", "80")),
log_config=logging_conf_dict,
log_level=os.getenv("LOG_LEVEL", "info"),
reload=True,
reload=with_reload,
)
else:
logger.debug("Running Uvicorn with Gunicorn.")
subprocess.run(
["gunicorn", "-k", worker_class, "-c", gunicorn_conf.name, app_module]
)
raise NameError("Process manager needs to be either uvicorn or gunicorn.")
except Exception as e:
logger.debug(f"Error when starting server with start script: {e}")
raise


if __name__ == "__main__":
Expand Down
126 changes: 126 additions & 0 deletions tests/test_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os
from pathlib import Path
from typing import Any, Dict

import pytest # type: ignore
from _pytest.monkeypatch import MonkeyPatch # type: ignore
Expand Down Expand Up @@ -245,3 +246,128 @@ def test_run_pre_start_script_no_file(
mocker.call("No pre-start script found."),
]
)


class TestStartServer:
"""Start Uvicorn and Gunicorn servers using the method in `start.py`.
---
"""

@pytest.mark.parametrize(
"app_module",
[
"inboard.base.main:app",
"inboard.fastapibase.main:app",
"inboard.starlettebase.main:app",
],
)
def test_start_server_uvicorn(
self,
app_module: str,
gunicorn_conf_path: Path,
logging_conf_dict: Dict[str, Any],
mock_logger: logging.Logger,
mocker: MockerFixture,
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.setenv("LOG_FORMAT", "uvicorn")
monkeypatch.setenv("LOG_LEVEL", "debug")
monkeypatch.setenv("PROCESS_MANAGER", "uvicorn")
start.start_server(
app_module=app_module,
gunicorn_conf=gunicorn_conf_path,
logger=mock_logger,
logging_conf_dict=logging_conf_dict,
with_reload=False,
)
mock_logger.debug.assert_called_once_with("Running Uvicorn with Gunicorn.") # type: ignore # noqa: E501

@pytest.mark.parametrize(
"app_module",
[
"inboard.base.main:app",
"inboard.fastapibase.main:app",
"inboard.starlettebase.main:app",
],
)
def test_start_server_uvicorn_gunicorn(
self,
app_module: str,
gunicorn_conf_path: Path,
logging_conf_dict: Dict[str, Any],
mock_logger: logging.Logger,
mocker: MockerFixture,
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.setenv("LOG_FORMAT", "gunicorn")
monkeypatch.setenv("LOG_LEVEL", "debug")
monkeypatch.setenv("PROCESS_MANAGER", "gunicorn")
start.start_server(
app_module=app_module,
gunicorn_conf=gunicorn_conf_path,
logger=mock_logger,
logging_conf_dict=logging_conf_dict,
)
mock_logger.debug.assert_called_once_with("Running Uvicorn with Gunicorn.") # type: ignore # noqa: E501

def test_start_server_uvicorn_incorrect_module(
self,
gunicorn_conf_path: Path,
logging_conf_dict: Dict[str, Any],
mock_logger: logging.Logger,
mocker: MockerFixture,
monkeypatch: MonkeyPatch,
) -> None:
with pytest.raises(ModuleNotFoundError):
monkeypatch.setenv("LOG_LEVEL", "debug")
monkeypatch.setenv("WITH_RELOAD", "false")
start.start_server(
app_module="incorrect.base.main:app",
gunicorn_conf=gunicorn_conf_path,
logger=mock_logger,
logging_conf_dict=logging_conf_dict,
process_manager="uvicorn",
)
logger_error_msg = "Error when starting server with start script:"
module_error_msg = "No module named incorrect.base.main:app"
mock_logger.debug.assert_has_calls( # type: ignore
calls=[
mocker.call("Running Uvicorn without Gunicorn."),
mocker.call(f"{logger_error_msg} {module_error_msg}"),
]
)

@pytest.mark.parametrize(
"app_module",
[
"inboard.base.main:app",
"inboard.fastapibase.main:app",
"inboard.starlettebase.main:app",
],
)
def test_start_server_uvicorn_incorrect_process_manager(
self,
app_module: str,
gunicorn_conf_path: Path,
logging_conf_dict: Dict[str, Any],
mock_logger: logging.Logger,
mocker: MockerFixture,
monkeypatch: MonkeyPatch,
) -> None:
with pytest.raises(NameError):
monkeypatch.setenv("LOG_LEVEL", "debug")
monkeypatch.setenv("WITH_RELOAD", "false")
start.start_server(
app_module=app_module,
gunicorn_conf=gunicorn_conf_path,
logger=mock_logger,
logging_conf_dict=logging_conf_dict,
process_manager="incorrect",
)
logger_error_msg = "Error when starting server with start script:"
process_error_msg = (
"Process manager needs to be either uvicorn or gunicorn."
)
mock_logger.debug.assert_called_once_with( # type: ignore
f"{logger_error_msg} {process_error_msg}"
)

0 comments on commit 92e6b68

Please sign in to comment.