Files
utils/test/test_logging_config.py
T
mathias2784 9d13c6ec14
Tests / test_and_lint (push) Successful in 7s
File utils1 (#1)
Reviewed-on: #1
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-10-01 19:13:45 +00:00

228 lines
7.3 KiB
Python

import logging
import re
import time
from collections.abc import Generator
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
from unittest.mock import patch
import pytest
from pydantic import ValidationError
from uvicorn.logging import DefaultFormatter
from utils.logging_config import LoggingSettings
SECONDS_PER_DAY = 24 * 60 * 60
@pytest.fixture
def restore_root_logger() -> Generator[None]:
root = logging.getLogger()
old_handlers = root.handlers[:]
old_level = root.level
for handler in old_handlers:
root.removeHandler(handler)
try:
yield
finally:
for handler in root.handlers:
root.removeHandler(handler)
handler.close()
for handler in old_handlers:
root.addHandler(handler)
root.setLevel(old_level)
def test_sets_valid_log_level(restore_root_logger: None) -> None:
LoggingSettings(LOG_LEVEL="DEBUG", LOG_TO_FILE=False)
assert logging.getLogger().level == logging.DEBUG
def test_invalid_log_level_falls_back_to_info(restore_root_logger: None) -> None:
with patch("utils.logging_config.logger.error") as log_error:
LoggingSettings(LOG_LEVEL="NOT_A_LEVEL", LOG_TO_FILE=False)
assert logging.getLogger().level == logging.INFO
log_error.assert_called_once_with(
"Invalid log level %r; falling back to INFO.", "NOT_A_LEVEL"
)
def test_configures_console_handler_only(restore_root_logger: None) -> None:
LoggingSettings(LOG_TO_FILE=False)
handlers = logging.getLogger().handlers
assert len(handlers) == 1
assert type(handlers[0]) is logging.StreamHandler
assert isinstance(handlers[0].formatter, DefaultFormatter)
def test_configures_file_handler_and_creates_parent_dir(
tmp_path: Path,
restore_root_logger: None,
) -> None:
log_file = tmp_path / "nested" / "forecast.log"
LoggingSettings(LOG_TO_FILE=True, LOG_FILE_PATH=str(log_file))
handlers = logging.getLogger().handlers
assert any(type(handler) is logging.StreamHandler for handler in handlers)
file_handlers = [
handler for handler in handlers if isinstance(handler, logging.FileHandler)
]
assert len(file_handlers) == 1
assert log_file.parent.exists()
assert isinstance(file_handlers[0], TimedRotatingFileHandler)
assert file_handlers[0].when == "MIDNIGHT"
assert file_handlers[0].interval == SECONDS_PER_DAY
assert file_handlers[0].backupCount == 30
assert file_handlers[0].encoding is not None
assert file_handlers[0].encoding.lower() == "utf-8"
def test_configures_custom_log_backup_count(
tmp_path: Path, restore_root_logger: None
) -> None:
log_file = tmp_path / "app.log"
LoggingSettings(LOG_TO_FILE=True, LOG_FILE_PATH=str(log_file), LOG_BACKUP_COUNT=7)
file_handler = next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, TimedRotatingFileHandler)
)
assert file_handler.backupCount == 7
def test_writes_records_to_console_and_file(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
restore_root_logger: None,
) -> None:
log_file = tmp_path / "app.log"
LoggingSettings(LOG_FILE_PATH=str(log_file))
logging.getLogger("test.logging_output").info("record is written")
file_handler = next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, TimedRotatingFileHandler)
)
file_handler.flush()
assert "record is written" in capsys.readouterr().err
assert "record is written" in log_file.read_text(encoding="utf-8")
def test_rotates_active_log_file(tmp_path: Path, restore_root_logger: None) -> None:
log_file = tmp_path / "app.log"
LoggingSettings(LOG_FILE_PATH=str(log_file))
logger = logging.getLogger("test.logging_rotation")
file_handler = next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, TimedRotatingFileHandler)
)
logger.info("before rollover")
file_handler.flush()
file_handler.doRollover()
logger.info("after rollover")
file_handler.flush()
rotated_files = list(tmp_path.glob("app.*.log"))
assert len(rotated_files) == 1
assert re.fullmatch(r"app\.\d{4}-\d{2}-\d{2}\.log", rotated_files[0].name)
assert "before rollover" not in log_file.read_text(encoding="utf-8")
assert "after rollover" in log_file.read_text(encoding="utf-8")
def test_removes_old_rotated_files_after_backup_limit(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
restore_root_logger: None,
) -> None:
log_file = tmp_path / "app.log"
LoggingSettings(LOG_FILE_PATH=str(log_file), LOG_BACKUP_COUNT=2)
logger = logging.getLogger("test.logging_retention")
file_handler = next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, TimedRotatingFileHandler)
)
base_timestamp = int(time.time())
for day in range(4):
logger.info("record from day %s", day)
file_handler.flush()
rollover_timestamp = base_timestamp + day * SECONDS_PER_DAY
file_handler.rolloverAt = rollover_timestamp + SECONDS_PER_DAY
# Bind the loop variable via a default argument to avoid late binding (B023).
monkeypatch.setattr(time, "time", lambda now=rollover_timestamp: now)
file_handler.doRollover()
assert len(list(tmp_path.glob("app.*.log"))) == 2
def test_rejects_negative_log_backup_count(restore_root_logger: None) -> None:
with pytest.raises(ValidationError):
LoggingSettings(LOG_TO_FILE=False, LOG_BACKUP_COUNT=-1)
def test_setup_logging_is_idempotent(tmp_path: Path, restore_root_logger: None) -> None:
log_file = tmp_path / "app.log"
settings = LoggingSettings(LOG_TO_FILE=True, LOG_FILE_PATH=str(log_file))
first_file_handler = next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, logging.FileHandler)
)
first_handler_count = len(logging.getLogger().handlers)
settings.setup_logging()
second_handler_count = len(logging.getLogger().handlers)
assert first_handler_count == 2
assert second_handler_count == first_handler_count
assert first_file_handler.stream is None
def test_applies_formatter_from_settings(restore_root_logger: None) -> None:
log_format = "%(levelname)s|%(message)s"
date_format = "%Y"
LoggingSettings(
LOG_FORMAT=log_format,
LOG_DATE_FORMAT=date_format,
LOG_TO_FILE=False,
)
handlers = logging.getLogger().handlers
assert len(handlers) == 1
formatter = handlers[0].formatter
assert formatter is not None
assert formatter._fmt == log_format
assert formatter.datefmt == date_format
def test_logging_settings_can_be_overridden_by_environment(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
restore_root_logger: None,
) -> None:
log_file = tmp_path / "custom" / "application.log"
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
monkeypatch.setenv("LOG_TO_FILE", "true")
monkeypatch.setenv("LOG_FILE_PATH", str(log_file))
monkeypatch.setenv("LOG_BACKUP_COUNT", "90")
settings = LoggingSettings()
assert settings.LOG_LEVEL == "DEBUG"
assert settings.LOG_TO_FILE is True
assert settings.LOG_FILE_PATH == str(log_file)
assert settings.LOG_BACKUP_COUNT == 90