Spaces:
Paused
Paused
| # -*- coding: utf-8 -*- | |
| """ | |
| Tests for JSONFormatter in logging_utils/core/rendering.py | |
| Coverage target: 80%+ | |
| """ | |
| import json | |
| import logging | |
| import pytest | |
| from logging_utils.core.rendering import JSONFormatter | |
| class TestJSONFormatter: | |
| """Tests for JSONFormatter class.""" | |
| def clean_context(self): | |
| """Clear context variables before each test to prevent leakage.""" | |
| from logging_utils.core.context import request_id_var, source_var | |
| # Get tokens for any existing values and reset them | |
| try: | |
| _old_req_id = request_id_var.get() | |
| token1 = request_id_var.set("") | |
| except LookupError: | |
| token1 = None | |
| try: | |
| _old_source = source_var.get() | |
| token2 = source_var.set("SYS") | |
| except LookupError: | |
| token2 = None | |
| yield | |
| # Cleanup | |
| if token1: | |
| request_id_var.reset(token1) | |
| if token2: | |
| source_var.reset(token2) | |
| def formatter(self): | |
| """Create JSONFormatter instance.""" | |
| return JSONFormatter() | |
| def sample_record(self): | |
| """Create a sample log record.""" | |
| record = logging.LogRecord( | |
| name="test_logger", | |
| level=logging.INFO, | |
| pathname="/test/path.py", | |
| lineno=42, | |
| msg="Test message", | |
| args=(), | |
| exc_info=None, | |
| ) | |
| return record | |
| def test_format_basic_record(self, formatter, sample_record): | |
| """Test formatting a basic log record.""" | |
| output = formatter.format(sample_record) | |
| # Should be valid JSON | |
| parsed = json.loads(output) | |
| assert "timestamp" in parsed | |
| assert parsed["level"] == "INFO" | |
| assert parsed["message"] == "Test message" | |
| assert parsed["logger"] == "test_logger" | |
| assert "source" in parsed # Default source | |
| def test_format_with_request_id_context(self, formatter, sample_record): | |
| """Test formatting includes request_id from context.""" | |
| from logging_utils.core.context import request_id_var | |
| token = request_id_var.set("req12345") | |
| try: | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| assert parsed["request_id"] == "req12345" | |
| finally: | |
| request_id_var.reset(token) | |
| def test_format_with_source_context(self, formatter, sample_record): | |
| """Test formatting includes source from context.""" | |
| from logging_utils.core.context import source_var | |
| token = source_var.set("API") | |
| try: | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| assert "API" in parsed["source"] | |
| finally: | |
| source_var.reset(token) | |
| def test_format_without_context(self, formatter, sample_record): | |
| """Test formatting works without context variables set.""" | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| # Should have default values | |
| assert "source" in parsed | |
| assert "request_id" not in parsed or parsed.get("request_id") == "" | |
| def test_format_with_exception(self, formatter): | |
| """Test formatting includes exception info.""" | |
| 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/path.py", | |
| lineno=42, | |
| msg="Error occurred", | |
| args=(), | |
| exc_info=exc_info, | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert "exception" in parsed | |
| assert parsed["exception"]["type"] == "ValueError" | |
| assert parsed["exception"]["message"] == "Test error" | |
| assert "traceback" in parsed["exception"] | |
| assert "ValueError: Test error" in parsed["exception"]["traceback"] | |
| def test_format_includes_extra_fields(self, formatter, sample_record): | |
| """Test formatting includes funcName, lineno, pathname.""" | |
| sample_record.funcName = "test_function" | |
| sample_record.lineno = 123 | |
| sample_record.pathname = "/test/module.py" | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| assert parsed["funcName"] == "test_function" | |
| assert parsed["lineno"] == 123 | |
| assert parsed["pathname"] == "/test/module.py" | |
| def test_format_timestamp_iso8601(self, formatter, sample_record): | |
| """Test timestamp is in ISO 8601 format.""" | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| timestamp = parsed["timestamp"] | |
| # Should be like: 2024-12-15T15:30:00.123Z | |
| assert "T" in timestamp | |
| assert timestamp.endswith("Z") | |
| assert len(timestamp) == 24 # YYYY-MM-DDTHH:MM:SS.mmmZ | |
| def test_format_different_log_levels(self, formatter): | |
| """Test formatting works for all log levels.""" | |
| levels = [ | |
| (logging.DEBUG, "DEBUG"), | |
| (logging.INFO, "INFO"), | |
| (logging.WARNING, "WARNING"), | |
| (logging.ERROR, "ERROR"), | |
| (logging.CRITICAL, "CRITICAL"), | |
| ] | |
| for level, level_name in levels: | |
| record = logging.LogRecord( | |
| name="test", | |
| level=level, | |
| pathname="", | |
| lineno=1, | |
| msg=f"Test {level_name}", | |
| args=(), | |
| exc_info=None, | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert parsed["level"] == level_name | |
| assert parsed["message"] == f"Test {level_name}" | |
| def test_format_unicode_message(self, formatter): | |
| """Test formatting handles unicode characters.""" | |
| record = logging.LogRecord( | |
| name="test", | |
| level=logging.INFO, | |
| pathname="", | |
| lineno=1, | |
| msg="Test message with unicode", | |
| args=(), | |
| exc_info=None, | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert "Test message with unicode" in parsed["message"] | |
| def test_format_message_with_args(self, formatter): | |
| """Test formatting with format args.""" | |
| record = logging.LogRecord( | |
| name="test", | |
| level=logging.INFO, | |
| pathname="", | |
| lineno=1, | |
| msg="User %s logged in with id %d", | |
| args=("john", 42), | |
| exc_info=None, | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert "john" in parsed["message"] | |
| assert "42" in parsed["message"] | |
| def test_format_empty_message(self, formatter): | |
| """Test formatting empty message.""" | |
| record = logging.LogRecord( | |
| name="test", | |
| level=logging.INFO, | |
| pathname="", | |
| lineno=1, | |
| msg="", | |
| args=(), | |
| exc_info=None, | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert parsed["message"] == "" | |
| def test_format_strips_request_id_whitespace(self, formatter, sample_record): | |
| """Test formatting strips whitespace from request_id.""" | |
| from logging_utils.core.context import request_id_var | |
| token = request_id_var.set(" abc123 ") | |
| try: | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| assert parsed["request_id"] == "abc123" | |
| finally: | |
| request_id_var.reset(token) | |
| def test_format_empty_request_id_excluded(self, formatter, sample_record): | |
| """Test empty request_id is not included in output.""" | |
| from logging_utils.core.context import request_id_var | |
| token = request_id_var.set(" ") # All whitespace | |
| try: | |
| output = formatter.format(sample_record) | |
| parsed = json.loads(output) | |
| # Empty request_id should not be in output | |
| assert "request_id" not in parsed or parsed["request_id"] == "" | |
| finally: | |
| request_id_var.reset(token) | |
| class TestJSONFormatterIntegration: | |
| """Integration tests for JSONFormatter with logging handlers.""" | |
| def test_with_stream_handler(self): | |
| """Test JSONFormatter works with StreamHandler.""" | |
| import io | |
| stream = io.StringIO() | |
| handler = logging.StreamHandler(stream) | |
| handler.setFormatter(JSONFormatter()) | |
| logger = logging.getLogger("test_json_integration") | |
| logger.handlers.clear() | |
| logger.addHandler(handler) | |
| logger.setLevel(logging.DEBUG) | |
| logger.info("Integration test message") | |
| output = stream.getvalue() | |
| parsed = json.loads(output.strip()) | |
| assert parsed["message"] == "Integration test message" | |
| assert parsed["level"] == "INFO" | |
| def test_multiple_log_entries(self): | |
| """Test multiple log entries produce valid JSON lines.""" | |
| import io | |
| stream = io.StringIO() | |
| handler = logging.StreamHandler(stream) | |
| handler.setFormatter(JSONFormatter()) | |
| logger = logging.getLogger("test_json_multi") | |
| logger.handlers.clear() | |
| logger.addHandler(handler) | |
| logger.setLevel(logging.DEBUG) | |
| logger.info("First message") | |
| logger.warning("Second message") | |
| logger.error("Third message") | |
| output = stream.getvalue() | |
| lines = output.strip().split("\n") | |
| assert len(lines) == 3 | |
| for line in lines: | |
| parsed = json.loads(line) | |
| assert "timestamp" in parsed | |
| assert "level" in parsed | |
| assert "message" in parsed | |