from __future__ import annotations import logging import sys from pathlib import Path import pytest import vuln_scan.utils.logging as logging_utils from vuln_scan.utils.logging import setup_logging # -------------------------------------------------------- # Fixtures # -------------------------------------------------------- @pytest.fixture def isolated_root_logger() -> logging.Logger: root_logger = logging.getLogger() original_handlers = list(root_logger.handlers) original_level = root_logger.level for handler in list(root_logger.handlers): root_logger.removeHandler(handler) try: yield root_logger finally: for handler in list(root_logger.handlers): root_logger.removeHandler(handler) for handler in original_handlers: root_logger.addHandler(handler) root_logger.setLevel(original_level) def _stderr_handlers(logger: logging.Logger) -> list[logging.StreamHandler]: return [ h for h in logger.handlers if type(h) is logging.StreamHandler and getattr(h, "stream", None) is sys.stderr ] # -------------------------------------------------------- # setup_logging — adds handler # -------------------------------------------------------- @pytest.mark.parametrize( "level, expected_level", [ pytest.param("debug", logging.DEBUG, id="debug"), pytest.param("info", logging.INFO, id="info"), pytest.param("warning", logging.WARNING, id="warning"), pytest.param("error", logging.ERROR, id="error"), pytest.param("critical", logging.CRITICAL, id="critical"), ], ) def test_setup_logging_adds_handler( isolated_root_logger: logging.Logger, monkeypatch: pytest.MonkeyPatch, level: str, expected_level: int, ) -> None: original_stream_handler = logging.StreamHandler class FakeStreamHandler(original_stream_handler): pass monkeypatch.setattr(logging_utils.logging, "StreamHandler", FakeStreamHandler) setup_logging(level=level) fake_handlers = [h for h in isolated_root_logger.handlers if isinstance(h, FakeStreamHandler)] assert isolated_root_logger.level == expected_level assert len(fake_handlers) == 1 assert fake_handlers[0].level == expected_level # -------------------------------------------------------- # setup_logging — formatter based on level # -------------------------------------------------------- class TestFormatter: def test_debug_format_includes_func_and_lineno( self, isolated_root_logger: logging.Logger, ) -> None: setup_logging(level="debug") handlers = _stderr_handlers(isolated_root_logger) assert len(handlers) == 1 fmt = handlers[0].formatter._fmt assert "%(funcName)s" in fmt assert "%(lineno)d" in fmt assert "%(asctime)s" in fmt assert "%(levelname)" in fmt assert "%(name)s" in fmt def test_info_format_excludes_func_and_lineno( self, isolated_root_logger: logging.Logger, ) -> None: setup_logging(level="info") handlers = _stderr_handlers(isolated_root_logger) assert len(handlers) == 1 fmt = handlers[0].formatter._fmt assert "%(funcName)s" not in fmt assert "%(lineno)d" not in fmt assert "%(asctime)s" in fmt assert "%(levelname)" in fmt assert "%(name)s" in fmt def test_datefmt_is_time_only( self, isolated_root_logger: logging.Logger, ) -> None: setup_logging(level="info") handlers = _stderr_handlers(isolated_root_logger) assert handlers[0].formatter.datefmt == "%H:%M:%S" # -------------------------------------------------------- # setup_logging — updates existing stderr handler # -------------------------------------------------------- def test_setup_logging_updates_existing_stderr_handler( isolated_root_logger: logging.Logger, ) -> None: stderr_handler = logging.StreamHandler(sys.stderr) non_stderr_handler = logging.StreamHandler(sys.stdout) stderr_handler.setLevel(logging.WARNING) non_stderr_handler.setLevel(logging.ERROR) isolated_root_logger.addHandler(stderr_handler) isolated_root_logger.addHandler(non_stderr_handler) isolated_root_logger.addHandler(logging.NullHandler()) before_count = len(isolated_root_logger.handlers) setup_logging(level="info") assert len(isolated_root_logger.handlers) == before_count assert stderr_handler.level == logging.INFO assert non_stderr_handler.level == logging.ERROR # untouched def test_setup_logging_updates_existing_handler_formatter( isolated_root_logger: logging.Logger, ) -> None: stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setLevel(logging.WARNING) stderr_handler.setFormatter(logging.Formatter("%(message)s")) isolated_root_logger.addHandler(stderr_handler) setup_logging(level="debug") assert stderr_handler.level == logging.DEBUG assert "%(funcName)s" in stderr_handler.formatter._fmt # -------------------------------------------------------- # setup_logging — ignores FileHandler subclass # -------------------------------------------------------- def test_setup_logging_ignores_file_handler( isolated_root_logger: logging.Logger, tmp_path: Path, ) -> None: file_handler = logging.FileHandler(str(tmp_path / "test.log")) file_handler.setLevel(logging.WARNING) isolated_root_logger.addHandler(file_handler) setup_logging(level="info") assert file_handler.level == logging.WARNING # untouched assert len(_stderr_handlers(isolated_root_logger)) == 1 # -------------------------------------------------------- # setup_logging — invalid level defaults to INFO # -------------------------------------------------------- def test_setup_logging_invalid_level_defaults_to_info( isolated_root_logger: logging.Logger, ) -> None: setup_logging(level="nonsense") assert isolated_root_logger.level == logging.INFO # -------------------------------------------------------- # setup_logging — default level is info # -------------------------------------------------------- def test_setup_logging_default_level( isolated_root_logger: logging.Logger, ) -> None: setup_logging() assert isolated_root_logger.level == logging.INFO