openflexure-microscope-server/tests/unit_tests/test_logging.py

208 lines
8.1 KiB
Python

"""Test the server's logging configuration and handling."""
import logging
import os
import tempfile
from contextlib import contextmanager
from logging.handlers import RotatingFileHandler
import pytest
from fastapi import HTTPException
from fastapi.responses import PlainTextResponse
from openflexure_microscope_server import logging as ofm_logging
@contextmanager
def tmp_logging_dir():
"""Yield a temporary logging dir, and delete rotating file handlers before closing.
This is needed for unit tests on Windows for the temp dir to cleanup without an
error.
"""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
root_logger = logging.getLogger()
# Get the rotating file handlers. There should only be 1 (or none if mocked)
file_handlers = [
h for h in root_logger.handlers if isinstance(h, RotatingFileHandler)
]
for handler in file_handlers:
handler.close()
root_logger.removeHandler(handler)
def test_no_warnings_if_correct_permissions(caplog):
"""Check that configure_logging no warnings on normal operation.
Checking that:
* The logger can be set up without warnings.
* That the log file is created.
* That the log file contains the expected message about setting up the logger.
"""
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with caplog.at_level(logging.WARNING), tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
assert len(caplog.records) == 0
with open(ofm_logging.OFM_LOG_FILE, "r", encoding="utf-8") as log_file:
log_txt = log_file.read()
assert "OFM server root logger has been set up at INFO level" in log_txt
root_logger = logging.getLogger()
assert ofm_logging.OFM_HANDLER in root_logger.handlers
def test_permission_error_raises_warning(mocker, caplog):
"""Check that configure_logging raises warning on PermissionError."""
mocker.patch(
"openflexure_microscope_server.logging.RotatingFileHandler",
side_effect=PermissionError("Permission denied"),
)
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with caplog.at_level(logging.WARNING), tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
assert len(caplog.records) == 1
# Check OFM logger is added even if the file logger couldn't be.
root_logger = logging.getLogger()
assert ofm_logging.OFM_HANDLER in root_logger.handlers
def test_making_log_dir(caplog):
"""Check that configure_logging will make a dir if needed."""
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with caplog.at_level(logging.WARNING), tmp_logging_dir() as tmpdir:
log_dir = os.path.join(tmpdir, "new_dir")
assert not os.path.isdir(log_dir)
ofm_logging.configure_logging(log_dir)
assert len(caplog.records) == 0
assert os.path.isdir(log_dir)
def test_max_logs():
"""Proclaim that only the most recent 250 logs are stored in memory."""
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
# But I would log five hundred times.
for i in range(500):
logging.info("log %s", i)
logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
assert len(logs) == 250
assert logs[0].endswith("log 250")
assert logs[-1].endswith("log 499")
# And I would log five hundred more.
for i in range(500):
logging.info("log %s", 500 + i)
# Just to be the test who logged a thousand times to check your hand-el-or!
logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
assert len(logs) == 250
assert logs[0].endswith("log 750")
assert logs[-1].endswith("log 999")
def test_ofm_handler_ignores_uvicorn_access():
"""Check uvicorn.access logs are ignored by custom handler.
The uvicorn.access logger logs every time an endpoint is triggered.
"""
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
starting_len = len(logs)
logging.info("Root 1")
uvicorn_logger = logging.getLogger("uvicorn.access")
for i in range(500):
uvicorn_logger.info("Uvicorn log %s", i)
logging.info("Root 2")
logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
assert len(logs) == starting_len + 2
assert logs[starting_len].endswith("Root 1")
assert logs[-1].endswith("Root 2")
def test_server_responses():
"""Check that the FastAPI responses from the server are as expected.
* retrieve_log() -> PlainTextResponse containing the last 250 logs separated by
a new line
* retrieve_log_from_file() -> All the logs from the log file.
"""
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
for i in range(500):
logging.info("log %s", i)
log_response = ofm_logging.retrieve_log()
assert isinstance(log_response, PlainTextResponse)
logs = log_response.body.decode().split("\n")
assert len(logs) == 250
file_log_response = ofm_logging.retrieve_log_from_file()
assert isinstance(file_log_response, PlainTextResponse)
file_logs = file_log_response.body.decode().split("\n")
assert len(file_logs) > 500
def test_server_response_with_no_log_file():
"""Check that an HTTP exception is raised if logging isn't configured."""
# Reset handler at start of test
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
# Also Reset log file global
ofm_logging.OFM_LOG_FILE = None
with pytest.raises(HTTPException) as excinfo:
ofm_logging.retrieve_log_from_file()
assert excinfo.value.status_code == 500
def test_server_response_with_no_log_dir():
"""Check that an HTTP exception is raised if the log file cannot be accessed."""
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
for i in range(500):
logging.info("log %s", i)
file_log_response = ofm_logging.retrieve_log_from_file()
assert isinstance(file_log_response, PlainTextResponse)
# Close and delete the log folder, this should create an error.
with pytest.raises(HTTPException) as excinfo:
ofm_logging.retrieve_log_from_file()
assert excinfo.value.status_code == 500
FAKE_UVICORN_LOGGER = logging.getLogger("uvicorn.error")
@pytest.mark.parametrize(
("log_command", "names_in_log", "names_not_in_log"),
[
(FAKE_UVICORN_LOGGER.debug, [], ["<uvicorn>", "<uvicorn.error>"]),
(FAKE_UVICORN_LOGGER.info, ["<uvicorn>"], ["<uvicorn.error>"]),
(FAKE_UVICORN_LOGGER.warning, ["<uvicorn>"], ["<uvicorn.error>"]),
(FAKE_UVICORN_LOGGER.error, ["<uvicorn.error>"], ["<uvicorn>"]),
(FAKE_UVICORN_LOGGER.exception, ["<uvicorn.error>"], ["<uvicorn>"]),
],
)
def test_uvicorn_error_only_says_error_on_error(
log_command, names_in_log, names_not_in_log
):
"""Check that an HTTP exception is raised if the log file cannot be accessed.
Parametrised to check: debug doesn't log, info and warning log as <uvicorn>, and
error logs as <uvicorn.error>.
"""
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
log_command("Mockety mock mock!")
with open(ofm_logging.OFM_LOG_FILE, "r", encoding="utf-8") as log_file:
log_txt = log_file.read()
for name in names_in_log:
assert name in log_txt
for name in names_not_in_log:
assert name not in log_txt