mirror of https://github.com/razor-ai/soup.git
110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""Tests for v0.34.0 Part A — smart logging tiers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
from soup_cli.utils.log_level import (
|
|
LOG_LEVELS,
|
|
LogLevel,
|
|
parse_log_level,
|
|
resolve_python_log_level,
|
|
setup_logging,
|
|
)
|
|
|
|
# Rich help renderer can split a flag like --log-level with ANSI colour
|
|
# escapes between `--`, `-log`, `-level` when the terminal is narrow (CI
|
|
# runners have a narrower default width than local shells). Strip ANSI so
|
|
# substring assertions are robust across all CI matrix jobs.
|
|
_ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*[mK]")
|
|
|
|
|
|
def _strip_ansi(text: str) -> str:
|
|
return _ANSI_ESCAPE.sub("", text)
|
|
|
|
class TestLogLevelEnum:
|
|
def test_four_tiers_defined(self):
|
|
assert set(LOG_LEVELS) == {"quiet", "normal", "verbose", "debug"}
|
|
|
|
def test_parse_valid(self):
|
|
assert parse_log_level("quiet") == LogLevel.QUIET
|
|
assert parse_log_level("normal") == LogLevel.NORMAL
|
|
assert parse_log_level("verbose") == LogLevel.VERBOSE
|
|
assert parse_log_level("debug") == LogLevel.DEBUG
|
|
|
|
def test_parse_case_insensitive(self):
|
|
assert parse_log_level("QUIET") == LogLevel.QUIET
|
|
assert parse_log_level("Verbose") == LogLevel.VERBOSE
|
|
|
|
def test_parse_invalid(self):
|
|
with pytest.raises(ValueError, match="invalid log level"):
|
|
parse_log_level("loud")
|
|
|
|
def test_parse_null_byte_rejected(self):
|
|
with pytest.raises(ValueError):
|
|
parse_log_level("quiet\x00")
|
|
|
|
def test_resolve_python_level(self):
|
|
assert resolve_python_log_level(LogLevel.QUIET) == logging.ERROR
|
|
assert resolve_python_log_level(LogLevel.NORMAL) == logging.WARNING
|
|
assert resolve_python_log_level(LogLevel.VERBOSE) == logging.INFO
|
|
assert resolve_python_log_level(LogLevel.DEBUG) == logging.DEBUG
|
|
|
|
|
|
class TestSetupLogging:
|
|
def test_returns_root_logger(self):
|
|
logger = setup_logging(LogLevel.NORMAL)
|
|
assert logger.level == logging.WARNING
|
|
|
|
def test_quiet_suppresses_info(self):
|
|
logger = setup_logging(LogLevel.QUIET)
|
|
assert not logger.isEnabledFor(logging.INFO)
|
|
|
|
def test_debug_enables_debug(self):
|
|
logger = setup_logging(LogLevel.DEBUG)
|
|
assert logger.isEnabledFor(logging.DEBUG)
|
|
|
|
def test_idempotent(self):
|
|
# Calling twice should not duplicate handlers
|
|
setup_logging(LogLevel.NORMAL)
|
|
n_handlers = len(logging.getLogger("soup").handlers)
|
|
setup_logging(LogLevel.NORMAL)
|
|
assert len(logging.getLogger("soup").handlers) == n_handlers
|
|
|
|
def test_tier_change_replaces_handler(self):
|
|
setup_logging(LogLevel.NORMAL)
|
|
setup_logging(LogLevel.DEBUG)
|
|
# Exactly one tier-tagged handler should remain — the DEBUG one.
|
|
tagged = [
|
|
handler for handler in logging.getLogger("soup").handlers
|
|
if getattr(handler, "_soup_log_tier", None) is not None
|
|
]
|
|
assert len(tagged) == 1
|
|
assert tagged[0]._soup_log_tier == LogLevel.DEBUG # type: ignore[attr-defined]
|
|
|
|
def test_parse_non_string_rejected(self):
|
|
with pytest.raises(ValueError, match="must be a string"):
|
|
parse_log_level(42) # type: ignore[arg-type]
|
|
|
|
|
|
class TestCliFlag:
|
|
def test_log_level_help_visible(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, ["--help"])
|
|
assert "--log-level" in _strip_ansi(result.output), result.output
|
|
|
|
def test_log_level_invalid_rejected(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, ["--log-level", "loud", "version"])
|
|
assert result.exit_code != 0
|
|
|
|
def test_log_level_quiet_accepted(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, ["--log-level", "quiet", "version"])
|
|
assert result.exit_code == 0, (result.output, repr(result.exception))
|