Files
mistral-vibe/tests/core/test_file_logging.py
Mathias Gesbert e9a9217cc8 v2.7.4 (#579)
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai>
Co-authored-by: Peter Evers <pevers90@gmail.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@protonmail.com>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai>
Co-authored-by: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-04-09 18:40:46 +02:00

346 lines
10 KiB
Python

from __future__ import annotations
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from textwrap import dedent
from unittest.mock import MagicMock, patch
import pytest
from vibe.core.logger import (
StructuredLogFormatter,
apply_logging_config,
decode_log_message,
encode_log_message,
)
@pytest.fixture
def mock_log_dir(tmp_path: Path):
"""Mock LOG_DIR and LOG_FILE to use tmp_path for testing."""
mock_dir = MagicMock()
mock_dir.path = tmp_path
mock_file = MagicMock()
mock_file.path = tmp_path / "vibe.log"
with (
patch("vibe.core.logger.LOG_DIR", mock_dir),
patch("vibe.core.logger.LOG_FILE", mock_file),
):
yield tmp_path
class TestStructuredFormatter:
def test_format_contains_required_fields(self) -> None:
formatter = StructuredLogFormatter()
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None,
)
output = formatter.format(record)
parts = output.split(" ", 4)
assert len(parts) == 5
assert "T" in parts[0]
assert parts[1].isdigit()
assert parts[2].isdigit()
assert parts[3] == "INFO"
assert parts[4] == "Test message"
def test_format_includes_exception(self) -> None:
formatter = StructuredLogFormatter()
try:
raise ValueError("test error")
except ValueError:
import sys
exc_info = sys.exc_info()
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error occurred",
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
assert "Error occurred" in output
assert "ValueError" in output
assert "test error" in output
def test_format_escapes_newlines_in_message(self) -> None:
formatter = StructuredLogFormatter()
multiline_msg = dedent("""
Line one
Line two
Line three""")
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg=multiline_msg,
args=(),
exc_info=None,
)
output = formatter.format(record)
assert "\n" not in output
assert "Line one\\nLine two\\nLine three" in output
def test_format_escapes_newlines_in_exception(self) -> None:
formatter = StructuredLogFormatter()
try:
raise ValueError("test error")
except ValueError:
import sys
exc_info = sys.exc_info()
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error occurred",
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
assert "\n" not in output
assert "ValueError" in output
assert "test error" in output
assert "\\n" in output
def test_format_output_is_single_line(self) -> None:
formatter = StructuredLogFormatter()
try:
error_msg = dedent("""
multi
line
error""")
raise RuntimeError(error_msg)
except RuntimeError:
import sys
exc_info = sys.exc_info()
multiline_msg = dedent("""
Something
went
wrong""")
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg=multiline_msg,
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
lines = output.split("\n")
assert len(lines) == 1
class TestApplyLoggingConfig:
def test_adds_handler_to_logger(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_apply_logging")
initial_handler_count = len(test_logger.handlers)
apply_logging_config(test_logger)
assert len(test_logger.handlers) == initial_handler_count + 1
def test_creates_log_file(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_log_file")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.info("Test log entry")
log_file = mock_log_dir / "vibe.log"
assert log_file.exists()
def test_log_entry_format(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_format")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.warning("Test warning message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "WARNING" in content
assert "Test warning message" in content
def test_respects_log_level(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "WARNING")
test_logger = logging.getLogger("test_level_filter")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.debug("Debug message")
test_logger.info("Info message")
test_logger.warning("Warning message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "Debug message" not in content
assert "Info message" not in content
assert "Warning message" in content
def test_creates_log_directory_if_missing(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
log_dir = tmp_path / "nested" / "logs"
mock_dir = MagicMock()
mock_dir.path = log_dir
mock_file = MagicMock()
mock_file.path = log_dir / "vibe.log"
with (
patch("vibe.core.logger.LOG_DIR", mock_dir),
patch("vibe.core.logger.LOG_FILE", mock_file),
):
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_mkdir")
apply_logging_config(test_logger)
assert log_dir.exists()
def test_debug_mode_overrides_log_level(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "WARNING")
monkeypatch.setenv("DEBUG_MODE", "true")
test_logger = logging.getLogger("test_debug_mode")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.debug("Debug message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "Debug message" in content
def test_invalid_log_level_defaults_to_warning(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "INVALID")
test_logger = logging.getLogger("test_invalid_level")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.info("Info message")
test_logger.warning("Warning message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "Info message" not in content
assert "Warning message" in content
def test_log_max_bytes_from_env(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_MAX_BYTES", "5242880") # 5 MB
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_max_bytes")
apply_logging_config(test_logger)
# Verify handler was added with correct maxBytes
handler = test_logger.handlers[-1]
assert isinstance(handler, RotatingFileHandler)
assert handler.maxBytes == 5242880
class TestDecodeLogMessage:
def test_plain_message_unchanged(self) -> None:
assert decode_log_message("Hello world") == "Hello world"
def test_decodes_escaped_newline(self) -> None:
assert decode_log_message("hello\\nworld") == "hello\nworld"
def test_decodes_escaped_backslash(self) -> None:
assert decode_log_message("C:\\\\path") == "C:\\path"
def test_decodes_escaped_backslash_before_n(self) -> None:
# This is the bug case: C:\new encoded as C:\\new must decode back to C:\new
assert decode_log_message("C:\\\\new") == "C:\\new"
def test_roundtrip_with_newlines(self) -> None:
original = "line one\nline two\nline three"
encoded = encode_log_message(original)
assert decode_log_message(encoded) == original
def test_roundtrip_with_backslashes(self) -> None:
original = "C:\\Users\\test\\file.txt"
encoded = encode_log_message(original)
assert decode_log_message(encoded) == original
def test_roundtrip_with_backslash_n(self) -> None:
original = "C:\\new folder\\notes.txt"
encoded = encode_log_message(original)
assert decode_log_message(encoded) == original
def test_roundtrip_mixed(self) -> None:
original = "path: C:\\new\nand a newline"
encoded = encode_log_message(original)
assert decode_log_message(encoded) == original
def test_exception_encoding_escapes_backslashes(self) -> None:
formatter = StructuredLogFormatter()
try:
raise ValueError("error in C:\\new\\path")
except ValueError:
import sys
exc_info = sys.exc_info()
record = logging.LogRecord(
name="test",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="fail",
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
assert "\n" not in output
# The backslashes in the exception should be escaped
assert "C:\\\\new\\\\path" in output