- test_infrastructure_logger.py: 21 tests for setup_logging (log levels, file creation, handlers, formatters, startup banner) and get_logger - test_uvicorn_logging_config.py: 28 tests for LOGGING_CONFIG structure, formatters, handlers, logger definitions, paths, and get_uvicorn_log_config
158 lines
6.3 KiB
Python
158 lines
6.3 KiB
Python
"""Unit tests for the infrastructure logger module.
|
|
|
|
Tests setup_logging and get_logger from src/infrastructure/logging/logger.py.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from src.infrastructure.logging.logger import get_logger, setup_logging
|
|
|
|
|
|
class TestSetupLogging:
|
|
"""Tests for setup_logging function."""
|
|
|
|
def test_returns_logger_instance(self, tmp_path):
|
|
"""setup_logging returns a logging.Logger."""
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
assert isinstance(logger, logging.Logger)
|
|
|
|
def test_logger_name_is_aniworld(self, tmp_path):
|
|
"""Returned logger has name 'aniworld'."""
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
assert logger.name == "aniworld"
|
|
|
|
def test_creates_log_directory(self, tmp_path):
|
|
"""Log directory is created if it does not exist."""
|
|
log_dir = tmp_path / "new_logs"
|
|
setup_logging(log_dir=log_dir)
|
|
assert log_dir.is_dir()
|
|
|
|
def test_creates_log_file(self, tmp_path):
|
|
"""A log file is created in the log directory."""
|
|
setup_logging(log_dir=tmp_path)
|
|
assert (tmp_path / "fastapi_app.log").exists()
|
|
|
|
def test_custom_log_file_name(self, tmp_path):
|
|
"""Custom log file name is used."""
|
|
setup_logging(log_file="custom.log", log_dir=tmp_path)
|
|
assert (tmp_path / "custom.log").exists()
|
|
|
|
def test_default_log_level_is_info(self, tmp_path):
|
|
"""Default log level falls back to INFO."""
|
|
with patch("src.infrastructure.logging.logger.settings") as mock_settings:
|
|
mock_settings.log_level = None
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
assert logger.getEffectiveLevel() == logging.INFO
|
|
|
|
def test_custom_log_level_debug(self, tmp_path):
|
|
"""Custom log level DEBUG is applied."""
|
|
logger = setup_logging(log_level="DEBUG", log_dir=tmp_path)
|
|
assert logger.getEffectiveLevel() == logging.DEBUG
|
|
|
|
def test_custom_log_level_warning(self, tmp_path):
|
|
"""Custom log level WARNING is applied."""
|
|
logger = setup_logging(log_level="WARNING", log_dir=tmp_path)
|
|
assert logger.getEffectiveLevel() == logging.WARNING
|
|
|
|
def test_log_level_case_insensitive(self, tmp_path):
|
|
"""Log level is case-insensitive."""
|
|
logger = setup_logging(log_level="debug", log_dir=tmp_path)
|
|
assert logger.getEffectiveLevel() == logging.DEBUG
|
|
|
|
def test_root_logger_has_two_handlers(self, tmp_path):
|
|
"""Root logger gets exactly console + file handlers."""
|
|
setup_logging(log_dir=tmp_path)
|
|
root = logging.getLogger()
|
|
# Should be at least 2 handlers (console + file)
|
|
handler_types = {type(h).__name__ for h in root.handlers}
|
|
assert "StreamHandler" in handler_types
|
|
assert "FileHandler" in handler_types
|
|
|
|
def test_clears_existing_root_handlers(self, tmp_path):
|
|
"""Existing root handlers are cleared to avoid duplicates."""
|
|
root = logging.getLogger()
|
|
root.addHandler(logging.StreamHandler())
|
|
initial_count = len(root.handlers)
|
|
setup_logging(log_dir=tmp_path)
|
|
# After setup, should have exactly 2 (console + file)
|
|
assert len(root.handlers) == 2
|
|
|
|
def test_writes_to_log_file(self, tmp_path):
|
|
"""Messages are written to the log file."""
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
logger.info("test message for file")
|
|
# Flush handlers
|
|
for handler in logging.getLogger().handlers:
|
|
handler.flush()
|
|
log_content = (tmp_path / "fastapi_app.log").read_text()
|
|
assert "test message for file" in log_content
|
|
|
|
def test_log_format_includes_timestamp(self, tmp_path):
|
|
"""File log entries include a timestamp."""
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
logger.info("timestamp check")
|
|
for handler in logging.getLogger().handlers:
|
|
handler.flush()
|
|
log_content = (tmp_path / "fastapi_app.log").read_text()
|
|
# Detailed formatter includes date
|
|
assert "-" in log_content # At minimum YYYY-MM-DD format
|
|
|
|
def test_log_format_includes_level_name(self, tmp_path):
|
|
"""File log entries include the log level name."""
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
logger.warning("level check")
|
|
for handler in logging.getLogger().handlers:
|
|
handler.flush()
|
|
log_content = (tmp_path / "fastapi_app.log").read_text()
|
|
assert "WARNING" in log_content
|
|
|
|
def test_startup_banner_logged(self, tmp_path):
|
|
"""Startup banner with log config info is written."""
|
|
setup_logging(log_dir=tmp_path)
|
|
for handler in logging.getLogger().handlers:
|
|
handler.flush()
|
|
log_content = (tmp_path / "fastapi_app.log").read_text()
|
|
assert "Logging configured successfully" in log_content
|
|
|
|
def test_level_from_settings(self, tmp_path):
|
|
"""Falls back to settings.log_level when no explicit level."""
|
|
with patch("src.infrastructure.logging.logger.settings") as mock_settings:
|
|
mock_settings.log_level = "ERROR"
|
|
logger = setup_logging(log_dir=tmp_path)
|
|
assert logger.getEffectiveLevel() == logging.ERROR
|
|
|
|
def test_invalid_log_level_defaults_to_info(self, tmp_path):
|
|
"""Invalid log level string falls back to INFO."""
|
|
logger = setup_logging(log_level="INVALID_LEVEL", log_dir=tmp_path)
|
|
assert logger.getEffectiveLevel() == logging.INFO
|
|
|
|
|
|
class TestGetLogger:
|
|
"""Tests for get_logger function."""
|
|
|
|
def test_returns_logger_with_given_name(self):
|
|
"""get_logger returns a logger with the requested name."""
|
|
logger = get_logger("my_module")
|
|
assert logger.name == "my_module"
|
|
|
|
def test_returns_same_logger_for_same_name(self):
|
|
"""Calling get_logger twice with same name returns same object."""
|
|
a = get_logger("shared_name")
|
|
b = get_logger("shared_name")
|
|
assert a is b
|
|
|
|
def test_hierarchical_name(self):
|
|
"""Dotted names produce hierarchical loggers."""
|
|
parent = get_logger("app")
|
|
child = get_logger("app.sub")
|
|
assert child.parent is parent or child.parent.name == "app"
|
|
|
|
def test_logger_is_standard_logging(self):
|
|
"""Returned object is a standard logging.Logger."""
|
|
logger = get_logger("test_std")
|
|
assert isinstance(logger, logging.Logger)
|