mcmqtt/tests/unit/test_logging_comprehensive.py
Ryan Malloy 8ab61eb1df 🚀 Initial release: mcmqtt FastMCP MQTT Server v2025.09.17
Complete FastMCP MQTT integration server featuring:

 Core Features:
- FastMCP native Model Context Protocol server with MQTT tools
- Embedded MQTT broker support with zero-configuration spawning
- Modular architecture: CLI, config, logging, server, MQTT, MCP, broker
- Comprehensive testing: 70+ tests with 96%+ coverage
- Cross-platform support: Linux, macOS, Windows

🏗️ Architecture:
- Clean separation of concerns across 7 modules
- Async/await patterns throughout for maximum performance
- Pydantic models with validation and configuration management
- AMQTT pure Python embedded brokers
- Typer CLI framework with rich output formatting

🧪 Quality Assurance:
- pytest-cov with HTML reporting
- AsyncMock comprehensive unit testing
- Edge case coverage for production reliability
- Pre-commit hooks with black, ruff, mypy

📦 Production Ready:
- PyPI package with proper metadata
- MIT License
- Professional documentation
- uvx installation support
- MCP client integration examples

Perfect for AI agent coordination, IoT data collection, and
microservice communication with MQTT messaging patterns.
2025-09-17 05:46:08 -06:00

235 lines
9.1 KiB
Python

"""
Comprehensive unit tests for logging modules.
Tests logging setup and configuration functionality.
"""
import pytest
import logging
import sys
import tempfile
import os
from unittest.mock import patch, Mock, MagicMock
from mcmqtt.logging.setup import setup_logging
class TestSetupLogging:
"""Test logging configuration functionality."""
def test_setup_logging_default_stderr(self):
"""Test logging setup with default stderr handler."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging()
# Verify logging.basicConfig called with stderr handler
mock_basic.assert_called_once()
call_args = mock_basic.call_args
assert call_args[1]['level'] == logging.WARNING
assert len(call_args[1]['handlers']) == 1
assert isinstance(call_args[1]['handlers'][0], logging.StreamHandler)
assert call_args[1]['handlers'][0].stream == sys.stderr
# Verify structlog configured
mock_structlog.assert_called_once()
def test_setup_logging_file_handler(self):
"""Test logging setup with file handler."""
with tempfile.NamedTemporaryFile(delete=False) as tf:
log_file = tf.name
try:
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging(log_level="INFO", log_file=log_file)
# Verify logging.basicConfig called with file handler
mock_basic.assert_called_once()
call_args = mock_basic.call_args
assert call_args[1]['level'] == logging.INFO
assert len(call_args[1]['handlers']) == 1
assert isinstance(call_args[1]['handlers'][0], logging.FileHandler)
# Verify structlog configured
mock_structlog.assert_called_once()
finally:
os.unlink(log_file)
def test_setup_logging_debug_level(self):
"""Test logging setup with DEBUG level."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging(log_level="DEBUG")
call_args = mock_basic.call_args
assert call_args[1]['level'] == logging.DEBUG
def test_setup_logging_info_level(self):
"""Test logging setup with INFO level."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging(log_level="INFO")
call_args = mock_basic.call_args
assert call_args[1]['level'] == logging.INFO
def test_setup_logging_warning_level(self):
"""Test logging setup with WARNING level."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging(log_level="WARNING")
call_args = mock_basic.call_args
assert call_args[1]['level'] == logging.WARNING
def test_setup_logging_error_level(self):
"""Test logging setup with ERROR level."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging(log_level="ERROR")
call_args = mock_basic.call_args
assert call_args[1]['level'] == logging.ERROR
def test_setup_logging_format_string(self):
"""Test logging setup with correct format string."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging()
call_args = mock_basic.call_args
expected_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
assert call_args[1]['format'] == expected_format
def test_setup_logging_structlog_configuration(self):
"""Test structlog configuration details."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
setup_logging()
# Verify structlog.configure was called
mock_structlog.assert_called_once()
# Check the call arguments
call_args = mock_structlog.call_args
assert 'processors' in call_args[1]
assert 'wrapper_class' in call_args[1]
assert 'logger_factory' in call_args[1]
assert 'cache_logger_on_first_use' in call_args[1]
# Verify cache setting
assert call_args[1]['cache_logger_on_first_use'] is True
def test_setup_logging_structlog_processors(self):
"""Test structlog processor configuration."""
import structlog
with patch('logging.basicConfig'), \
patch('structlog.configure') as mock_structlog:
setup_logging()
call_args = mock_structlog.call_args
processors = call_args[1]['processors']
# Should have multiple processors
assert len(processors) == 5
# Verify specific processors are included
processor_names = [proc.__name__ if hasattr(proc, '__name__') else str(proc) for proc in processors]
assert any('filter_by_level' in str(proc) for proc in processor_names)
assert any('add_logger_name' in str(proc) for proc in processor_names)
assert any('add_log_level' in str(proc) for proc in processor_names)
def test_setup_logging_case_insensitive_levels(self):
"""Test logging setup with case variations in log level."""
test_cases = [
("debug", logging.DEBUG),
("DEBUG", logging.DEBUG),
("Debug", logging.DEBUG),
("info", logging.INFO),
("INFO", logging.INFO),
("warning", logging.WARNING),
("WARNING", logging.WARNING),
("error", logging.ERROR),
("ERROR", logging.ERROR)
]
for log_level_str, expected_level in test_cases:
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure'):
setup_logging(log_level=log_level_str)
call_args = mock_basic.call_args
assert call_args[1]['level'] == expected_level
def test_setup_logging_multiple_calls(self):
"""Test multiple calls to setup_logging."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure') as mock_structlog:
# First call
setup_logging(log_level="DEBUG")
# Second call with different settings
setup_logging(log_level="ERROR", log_file="/tmp/test.log")
# Both calls should work
assert mock_basic.call_count == 2
assert mock_structlog.call_count == 2
def test_setup_logging_file_handler_creation(self):
"""Test file handler creation with actual file."""
with tempfile.NamedTemporaryFile(delete=False) as tf:
log_file = tf.name
try:
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure'):
setup_logging(log_file=log_file)
# Verify FileHandler was created
call_args = mock_basic.call_args
handler = call_args[1]['handlers'][0]
assert isinstance(handler, logging.FileHandler)
assert handler.baseFilename == os.path.abspath(log_file)
finally:
os.unlink(log_file)
def test_setup_logging_stderr_stream_handler(self):
"""Test stderr stream handler configuration."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure'):
setup_logging()
call_args = mock_basic.call_args
handler = call_args[1]['handlers'][0]
assert isinstance(handler, logging.StreamHandler)
assert handler.stream == sys.stderr
def test_setup_logging_no_stdout_interference(self):
"""Test that logging doesn't interfere with stdout."""
with patch('logging.basicConfig') as mock_basic, \
patch('structlog.configure'):
setup_logging()
# Verify no stdout handler
call_args = mock_basic.call_args
handlers = call_args[1]['handlers']
for handler in handlers:
if isinstance(handler, logging.StreamHandler):
assert handler.stream != sys.stdout