diff --git a/src/renderkit/logging_utils.py b/src/renderkit/logging_utils.py index 10c391e..1b3e405 100644 --- a/src/renderkit/logging_utils.py +++ b/src/renderkit/logging_utils.py @@ -40,6 +40,13 @@ def _has_handler(logger: logging.Logger, handler_key: str) -> bool: ) +def _get_handler(logger: logging.Logger, handler_key: str) -> logging.Handler | None: + for handler in logger.handlers: + if getattr(handler, "renderkit_handler", None) == handler_key: + return handler + return None + + def _log_level() -> int: level_name = os.environ.get("RENDERKIT_LOG_LEVEL", "INFO").upper() return getattr(logging, level_name, logging.INFO) @@ -92,7 +99,8 @@ def setup_logging( ) # 3. Manage File Handler - if not _has_handler(root_logger, "file"): + file_handler = _get_handler(root_logger, "file") + if file_handler is None: file_handler = RotatingFileHandler( log_path, maxBytes=5 * 1024 * 1024, @@ -100,10 +108,10 @@ def setup_logging( encoding="utf-8", ) file_handler.renderkit_handler = "file" - file_handler.setLevel(logging.INFO) # File captures INFO+ file_handler.setFormatter(file_formatter) root_logger.addHandler(file_handler) rk_logger.info("Logging to %s", log_path) + file_handler.setLevel(log_level) # 4. Manage Console Handler if enable_console is None: diff --git a/tests/conftest.py b/tests/conftest.py index b525477..13049e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,28 @@ """Shared pytest fixtures for RenderKit tests.""" +import logging + import pytest from renderkit.ui.qt_compat import QApplication +def _close_renderkit_logging_handlers() -> None: + root_logger = logging.getLogger() + for handler in root_logger.handlers.copy(): + if getattr(handler, "renderkit_handler", None): + root_logger.removeHandler(handler) + handler.close() + + +@pytest.fixture(autouse=True) +def cleanup_renderkit_logging_handlers(): + """Keep RenderKit root handlers from leaking between tests.""" + _close_renderkit_logging_handlers() + yield + _close_renderkit_logging_handlers() + + @pytest.fixture(scope="session") def qapp(): """Create a QApplication for Qt widget tests.""" diff --git a/tests/test_logging_utils.py b/tests/test_logging_utils.py new file mode 100644 index 0000000..145653e --- /dev/null +++ b/tests/test_logging_utils.py @@ -0,0 +1,62 @@ +"""Tests for RenderKit logging helpers.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from renderkit.logging_utils import setup_logging + + +def _flush_renderkit_handlers() -> None: + for handler in logging.getLogger().handlers: + if getattr(handler, "renderkit_handler", None): + handler.flush() + + +def test_env_debug_level_reaches_file_log(monkeypatch, tmp_path: Path) -> None: + """RENDERKIT_LOG_LEVEL should control the file sink level.""" + log_path = tmp_path / "renderkit-debug.log" + monkeypatch.setenv("RENDERKIT_LOG_PATH", str(log_path)) + monkeypatch.setenv("RENDERKIT_LOG_LEVEL", "DEBUG") + + setup_logging(enable_console=False) + logging.getLogger("renderkit.test").debug("debug sentinel") + logging.getLogger("renderkit.test").info("info sentinel") + _flush_renderkit_handlers() + + log_text = log_path.read_text(encoding="utf-8") + assert "debug sentinel" in log_text + assert "info sentinel" in log_text + + +def test_explicit_debug_level_reaches_file_log(monkeypatch, tmp_path: Path) -> None: + """The explicit setup_logging level should control the file sink.""" + log_path = tmp_path / "renderkit-explicit-debug.log" + monkeypatch.setenv("RENDERKIT_LOG_PATH", str(log_path)) + monkeypatch.setenv("RENDERKIT_LOG_LEVEL", "INFO") + + setup_logging(enable_console=False, level=logging.DEBUG) + logging.getLogger("renderkit.test").debug("explicit debug sentinel") + _flush_renderkit_handlers() + + log_text = log_path.read_text(encoding="utf-8") + assert "explicit debug sentinel" in log_text + + +def test_reconfigured_debug_level_updates_existing_file_handler( + monkeypatch, tmp_path: Path +) -> None: + """Repeated setup should update the existing file handler level.""" + log_path = tmp_path / "renderkit-reconfigured-debug.log" + monkeypatch.setenv("RENDERKIT_LOG_PATH", str(log_path)) + + setup_logging(enable_console=False, level=logging.INFO) + logging.getLogger("renderkit.test").debug("hidden debug sentinel") + setup_logging(enable_console=False, level=logging.DEBUG) + logging.getLogger("renderkit.test").debug("updated debug sentinel") + _flush_renderkit_handlers() + + log_text = log_path.read_text(encoding="utf-8") + assert "hidden debug sentinel" not in log_text + assert "updated debug sentinel" in log_text