test(core): capture handler stream directly instead of monkey-patching caplog
Browse files- conftest.py +0 -37
- tests/core/test_logger.py +35 -4
conftest.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
"""Root conftest: ensure caplog captures from non-propagating loggers."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import logging
|
| 5 |
-
from contextlib import contextmanager
|
| 6 |
-
from typing import Generator
|
| 7 |
-
|
| 8 |
-
import pytest
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
@pytest.fixture(autouse=True)
|
| 12 |
-
def _patch_caplog_for_no_propagate(caplog):
|
| 13 |
-
"""Attach the caplog handler directly to every named logger that has
|
| 14 |
-
propagate=False so that ``caplog`` can still capture their records."""
|
| 15 |
-
_orig_at_level = caplog.at_level.__func__ # type: ignore[attr-defined]
|
| 16 |
-
|
| 17 |
-
@contextmanager
|
| 18 |
-
def patched_at_level(
|
| 19 |
-
self,
|
| 20 |
-
level: int,
|
| 21 |
-
logger: str | None = None,
|
| 22 |
-
) -> Generator[None, None, None]:
|
| 23 |
-
logger_obj = logging.getLogger(logger) if logger else logging.getLogger()
|
| 24 |
-
added = False
|
| 25 |
-
if not logger_obj.propagate:
|
| 26 |
-
logger_obj.addHandler(self.handler)
|
| 27 |
-
added = True
|
| 28 |
-
try:
|
| 29 |
-
with _orig_at_level(self, level, logger):
|
| 30 |
-
yield
|
| 31 |
-
finally:
|
| 32 |
-
if added:
|
| 33 |
-
logger_obj.removeHandler(self.handler)
|
| 34 |
-
|
| 35 |
-
import types
|
| 36 |
-
caplog.at_level = types.MethodType(patched_at_level, caplog)
|
| 37 |
-
yield
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/core/test_logger.py
CHANGED
|
@@ -26,8 +26,39 @@ def test_get_logger_default_level_is_info() -> None:
|
|
| 26 |
assert logger.level == logging.INFO
|
| 27 |
|
| 28 |
|
| 29 |
-
def
|
| 30 |
-
logger
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
logger.info("hello-world")
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
assert logger.level == logging.INFO
|
| 27 |
|
| 28 |
|
| 29 |
+
def test_get_logger_emits_record_to_handler_stream() -> None:
|
| 30 |
+
"""The logger writes records through its StreamHandler."""
|
| 31 |
+
import io
|
| 32 |
+
|
| 33 |
+
logger = get_logger("neurobridge.emit_capture")
|
| 34 |
+
handler = logger.handlers[0]
|
| 35 |
+
buf = io.StringIO()
|
| 36 |
+
original_stream = handler.stream
|
| 37 |
+
handler.stream = buf
|
| 38 |
+
try:
|
| 39 |
logger.info("hello-world")
|
| 40 |
+
finally:
|
| 41 |
+
handler.stream = original_stream
|
| 42 |
+
|
| 43 |
+
output = buf.getvalue()
|
| 44 |
+
assert "hello-world" in output
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_get_logger_format_includes_level_and_name() -> None:
|
| 48 |
+
"""Format string must include level and logger name (per AGENTS.md §3 traceability)."""
|
| 49 |
+
import io
|
| 50 |
+
|
| 51 |
+
logger = get_logger("neurobridge.format_check")
|
| 52 |
+
handler = logger.handlers[0]
|
| 53 |
+
buf = io.StringIO()
|
| 54 |
+
original_stream = handler.stream
|
| 55 |
+
handler.stream = buf
|
| 56 |
+
try:
|
| 57 |
+
logger.info("payload")
|
| 58 |
+
finally:
|
| 59 |
+
handler.stream = original_stream
|
| 60 |
+
|
| 61 |
+
output = buf.getvalue()
|
| 62 |
+
assert "INFO" in output
|
| 63 |
+
assert "neurobridge.format_check" in output
|
| 64 |
+
assert "payload" in output
|