- Create logging_config.py with get_logger() providing dual handlers: - Console handler (stderr) controlled by GRM_LOG_LEVEL env var (default INFO) - File handler (~/.local/state/grm/logs/grm.log) capturing everything at DEBUG - Replace all print() calls in executor.py and report.py with logger.info()/error() - Add error logging before raising AnsibleError in executor.run() - Add GRM_LOG_LEVEL to README configuration table and logging documentation - Update all unit tests to mock logger instead of using capsys - 114 tests, 100% coverage, pyright clean, ruff clean
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
"""Unit tests for logging configuration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Generator
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from gitea_runner_manager.logging_config import _level_from_env, get_logger
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _cleanup_loggers() -> Generator[None, None, None]:
|
|
"""Remove test loggers after each test to prevent state leakage."""
|
|
yield
|
|
for name in list(logging.Logger.manager.loggerDict.keys()):
|
|
if name.startswith("test_"):
|
|
logger = logging.getLogger(name)
|
|
logger.handlers.clear()
|
|
del logging.Logger.manager.loggerDict[name]
|
|
|
|
|
|
class TestLevelFromEnv:
|
|
def test_none_defaults_to_info(self) -> None:
|
|
assert _level_from_env(None) == logging.INFO
|
|
|
|
def test_empty_string_defaults_to_info(self) -> None:
|
|
assert _level_from_env("") == logging.INFO
|
|
|
|
def test_valid_levels(self) -> None:
|
|
assert _level_from_env("debug") == logging.DEBUG
|
|
assert _level_from_env("DEBUG") == logging.DEBUG
|
|
assert _level_from_env("warning") == logging.WARNING
|
|
assert _level_from_env("error") == logging.ERROR
|
|
assert _level_from_env("critical") == logging.CRITICAL
|
|
|
|
def test_invalid_fallback_to_info(self) -> None:
|
|
assert _level_from_env("foo") == logging.INFO
|
|
assert _level_from_env("VERBOSE") == logging.INFO
|
|
|
|
|
|
class TestGetLogger:
|
|
def test_returns_logger(self, tmp_path: Path) -> None:
|
|
with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path):
|
|
logger = get_logger("test_returns_logger")
|
|
|
|
assert isinstance(logger, logging.Logger)
|
|
assert logger.name == "test_returns_logger"
|
|
assert len(logger.handlers) == 2 # console + file
|
|
|
|
def test_caches_same_instance(self, tmp_path: Path) -> None:
|
|
with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path):
|
|
logger1 = get_logger("test_caches_same")
|
|
logger2 = get_logger("test_caches_same")
|
|
|
|
assert logger1 is logger2
|
|
|
|
def test_file_handler_writes(self, tmp_path: Path) -> None:
|
|
with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path):
|
|
logger = get_logger("test_file_writes")
|
|
logger.info("hello from test")
|
|
|
|
log_file = tmp_path / ".local" / "state" / "grm" / "logs" / "grm.log"
|
|
assert log_file.exists()
|
|
content = log_file.read_text()
|
|
assert "hello from test" in content
|
|
assert "INFO" in content
|
|
|
|
def test_respects_grm_log_level(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setenv("GRM_LOG_LEVEL", "DEBUG")
|
|
with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path):
|
|
logger = get_logger("test_env_level")
|
|
console = [h for h in logger.handlers if type(h) is logging.StreamHandler]
|
|
|
|
assert len(console) == 1
|
|
assert console[0].level == logging.DEBUG
|
|
|
|
def test_grm_log_level_warning(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setenv("GRM_LOG_LEVEL", "WARNING")
|
|
with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path):
|
|
logger = get_logger("test_env_warning")
|
|
console = [h for h in logger.handlers if type(h) is logging.StreamHandler]
|
|
|
|
assert len(console) == 1
|
|
assert console[0].level == logging.WARNING
|