Aggregates all v0.2.0 sprint work (GARAA-30 through GARAA-40) and fixes 2 integration tests that broke when the codebase went async (DSPyLLMAdapter and full pipeline tests now properly await coroutines). 277 tests pass (260 unit + 17 integration). Co-Authored-By: Paperclip <noreply@paperclip.ing>
190 lines
6.7 KiB
Python
190 lines
6.7 KiB
Python
"""Unit tests for structured logging configuration."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from prometheus.cli.logging_setup import configure_logging, get_logger
|
|
|
|
|
|
class TestConfigureLogging:
|
|
def _count_handlers(self, name: str = "prometheus") -> int:
|
|
return len(logging.getLogger(name).handlers)
|
|
|
|
def test_default_creates_console_handler(self) -> None:
|
|
configure_logging(level=logging.INFO)
|
|
prom = logging.getLogger("prometheus")
|
|
assert len(prom.handlers) == 1
|
|
assert isinstance(prom.handlers[0], logging.StreamHandler)
|
|
prom.handlers.clear()
|
|
|
|
def test_json_format_produces_valid_json(self, capsys) -> None:
|
|
configure_logging(level=logging.INFO, log_format="json")
|
|
logger = get_logger("test_json")
|
|
logger.info("hello", extra={"structured": {"key": "value"}})
|
|
|
|
captured = capsys.readouterr()
|
|
# Output goes to stderr
|
|
line = captured.err.strip().split("\n")[-1]
|
|
data = json.loads(line)
|
|
assert data["message"] == "hello"
|
|
assert data["structured"]["key"] == "value"
|
|
assert data["level"] == "INFO"
|
|
assert "timestamp" in data
|
|
|
|
logging.getLogger("prometheus").handlers.clear()
|
|
|
|
def test_text_format_includes_structured_extras(self, capsys) -> None:
|
|
configure_logging(level=logging.INFO, log_format="text")
|
|
logger = get_logger("test_text")
|
|
logger.info("msg", extra={"structured": {"foo": "bar"}})
|
|
|
|
captured = capsys.readouterr()
|
|
assert "foo=bar" in captured.err
|
|
|
|
logging.getLogger("prometheus").handlers.clear()
|
|
|
|
def test_debug_level_shows_debug_messages(self, capsys) -> None:
|
|
configure_logging(level=logging.DEBUG)
|
|
logger = get_logger("test_debug")
|
|
logger.debug("debug msg")
|
|
|
|
captured = capsys.readouterr()
|
|
assert "debug msg" in captured.err
|
|
|
|
logging.getLogger("prometheus").handlers.clear()
|
|
|
|
def test_warning_level_hides_debug_messages(self, capsys) -> None:
|
|
configure_logging(level=logging.WARNING)
|
|
logger = get_logger("test_warn")
|
|
logger.debug("should not appear")
|
|
logger.info("also hidden")
|
|
|
|
captured = capsys.readouterr()
|
|
assert "should not appear" not in captured.err
|
|
assert "also hidden" not in captured.err
|
|
|
|
logging.getLogger("prometheus").handlers.clear()
|
|
|
|
def test_file_handler_writes_to_file(self, tmp_path: Path) -> None:
|
|
log_file = tmp_path / "test.log"
|
|
configure_logging(level=logging.INFO, log_file=str(log_file))
|
|
logger = get_logger("test_file")
|
|
logger.info("file message")
|
|
|
|
prom = logging.getLogger("prometheus")
|
|
# Flush handlers
|
|
for h in prom.handlers:
|
|
h.flush()
|
|
prom.handlers.clear()
|
|
|
|
content = log_file.read_text()
|
|
assert "file message" in content
|
|
|
|
def test_json_file_output(self, tmp_path: Path) -> None:
|
|
log_file = tmp_path / "test.json.log"
|
|
configure_logging(level=logging.INFO, log_format="json", log_file=str(log_file))
|
|
logger = get_logger("test_json_file")
|
|
logger.info("json file msg", extra={"structured": {"x": 1}})
|
|
|
|
prom = logging.getLogger("prometheus")
|
|
for h in prom.handlers:
|
|
h.flush()
|
|
prom.handlers.clear()
|
|
|
|
content = log_file.read_text().strip()
|
|
data = json.loads(content)
|
|
assert data["message"] == "json file msg"
|
|
assert data["structured"]["x"] == 1
|
|
|
|
def test_reconfigure_clears_old_handlers(self) -> None:
|
|
configure_logging(level=logging.INFO)
|
|
configure_logging(level=logging.DEBUG)
|
|
prom = logging.getLogger("prometheus")
|
|
assert len(prom.handlers) == 1
|
|
prom.handlers.clear()
|
|
|
|
def test_propagate_false_prevents_duplicate_output(self, capsys) -> None:
|
|
configure_logging(level=logging.INFO)
|
|
prom = logging.getLogger("prometheus")
|
|
assert prom.propagate is False
|
|
prom.handlers.clear()
|
|
|
|
|
|
class TestGetLogger:
|
|
def test_returns_child_of_prometheus(self) -> None:
|
|
logger = get_logger("mymodule")
|
|
assert logger.name == "prometheus.mymodule"
|
|
|
|
def test_inherits_level_from_parent(self) -> None:
|
|
configure_logging(level=logging.DEBUG)
|
|
logger = get_logger("child")
|
|
assert logger.getEffectiveLevel() <= logging.DEBUG
|
|
logging.getLogger("prometheus").handlers.clear()
|
|
|
|
|
|
class TestJsonFormatter:
|
|
def test_exception_included(self, capsys) -> None:
|
|
configure_logging(level=logging.ERROR, log_format="json")
|
|
logger = get_logger("test_exc")
|
|
try:
|
|
raise ValueError("boom")
|
|
except ValueError:
|
|
logger.error("failed", exc_info=True)
|
|
|
|
captured = capsys.readouterr()
|
|
line = captured.err.strip().split("\n")[-1]
|
|
data = json.loads(line)
|
|
assert "ValueError: boom" in data["exception"]
|
|
|
|
logging.getLogger("prometheus").handlers.clear()
|
|
|
|
|
|
class TestLoggingCLIIntegration:
|
|
"""Tests for CLI flags that configure logging."""
|
|
|
|
def test_verbose_flag_enables_info(self, tmp_path: Path) -> None:
|
|
"""Simulate what -v does — configure_logging at INFO level."""
|
|
configure_logging(level=logging.INFO)
|
|
logger = get_logger("evolution")
|
|
logger.info("test message")
|
|
|
|
prom = logging.getLogger("prometheus")
|
|
assert len(prom.handlers) == 1
|
|
prom.handlers.clear()
|
|
|
|
def test_debug_flag_enables_debug(self) -> None:
|
|
"""Simulate what --debug does — configure_logging at DEBUG level."""
|
|
configure_logging(level=logging.DEBUG)
|
|
logger = get_logger("evolution")
|
|
logger.debug("debug message")
|
|
|
|
prom = logging.getLogger("prometheus")
|
|
assert prom.level == logging.DEBUG
|
|
prom.handlers.clear()
|
|
|
|
def test_log_format_invalid_rejected(self) -> None:
|
|
"""Invalid log_format should be caught by OptimizationConfig validator."""
|
|
from pydantic import ValidationError
|
|
from prometheus.application.dto import OptimizationConfig
|
|
|
|
import pytest
|
|
|
|
with pytest.raises(ValidationError, match="log_format must be one of"):
|
|
OptimizationConfig(
|
|
seed_prompt="a",
|
|
task_description="b",
|
|
log_format="xml",
|
|
)
|
|
|
|
def test_log_format_text_and_json_accepted(self) -> None:
|
|
"""Both text and json log_format values should be valid."""
|
|
from prometheus.application.dto import OptimizationConfig
|
|
|
|
for fmt in ("text", "json"):
|
|
config = OptimizationConfig(
|
|
seed_prompt="a", task_description="b", log_format=fmt,
|
|
)
|
|
assert config.log_format == fmt
|