Skip to main content
Glama
brianirish

Laravel MCP Companion

by brianirish
test_shutdown_handler.py17 kB
"""Unit tests for graceful shutdown handler.""" import pytest import signal from unittest.mock import patch, MagicMock, call from shutdown_handler import GracefulShutdown class TestGracefulShutdown: """Test graceful shutdown functionality.""" def test_init(self): """Test initialization with default and custom logger.""" # Default logger shutdown = GracefulShutdown() assert shutdown.logger is not None assert shutdown.shutdown_callbacks == [] assert shutdown.is_shutting_down is False # Custom logger mock_logger = MagicMock() shutdown_custom = GracefulShutdown(logger=mock_logger) assert shutdown_custom.logger == mock_logger @patch('shutdown_handler.signal.signal') def test_signal_handlers_registered(self, mock_signal): """Test that signal handlers are properly registered.""" shutdown = GracefulShutdown() # Check that signal handlers were registered expected_calls = [ call(signal.SIGINT, shutdown._handle_shutdown), call(signal.SIGTERM, shutdown._handle_shutdown), ] # SIGBREAK is Windows-specific if hasattr(signal, 'SIGBREAK'): expected_calls.append(call(signal.SIGBREAK, shutdown._handle_shutdown)) mock_signal.assert_has_calls(expected_calls, any_order=True) def test_register_callback(self): """Test registering shutdown callbacks.""" shutdown = GracefulShutdown() def test_callback(arg1, arg2, kwarg1=None): pass shutdown.register(test_callback, "arg1_value", "arg2_value", kwarg1="kwarg1_value") assert len(shutdown.shutdown_callbacks) == 1 callback, args, kwargs = shutdown.shutdown_callbacks[0] assert callback == test_callback assert args == ("arg1_value", "arg2_value") assert kwargs == {"kwarg1": "kwarg1_value"} def test_register_multiple_callbacks(self): """Test registering multiple shutdown callbacks.""" shutdown = GracefulShutdown() def callback1(): pass def callback2(): pass shutdown.register(callback1) shutdown.register(callback2) assert len(shutdown.shutdown_callbacks) == 2 @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_handle_shutdown_first_signal(self, mock_print, mock_exit): """Test handling first shutdown signal.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) callback_called = [] def test_callback(): callback_called.append(True) shutdown.register(test_callback) # Simulate SIGINT shutdown._handle_shutdown(signal.SIGINT, None) # Check that callback was called assert len(callback_called) == 1 assert shutdown.is_shutting_down is True # Check logging mock_logger.info.assert_called() mock_logger.debug.assert_called() # Check exit mock_exit.assert_called_once_with(0) @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_handle_shutdown_second_signal(self, mock_print, mock_exit): """Test handling second shutdown signal (force exit).""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) shutdown.is_shutting_down = True # Already shutting down # Simulate second signal shutdown._handle_shutdown(signal.SIGTERM, None) # Should force exit with code 1 mock_exit.assert_any_call(1) mock_logger.warning.assert_called() @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_handle_shutdown_callback_error(self, mock_print, mock_exit): """Test handling shutdown when callback raises exception.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) def failing_callback(): raise Exception("Callback failed") def working_callback(): pass shutdown.register(failing_callback) shutdown.register(working_callback) # Should handle the error and continue shutdown._handle_shutdown(signal.SIGINT, None) # Should have logged the error mock_logger.error.assert_called() # Should still exit normally mock_exit.assert_called_once_with(0) @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_handle_shutdown_callbacks_executed_in_reverse_order(self, mock_print, mock_exit): """Test that callbacks are executed in reverse registration order.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) execution_order = [] def callback1(): execution_order.append(1) def callback2(): execution_order.append(2) def callback3(): execution_order.append(3) # Register in order 1, 2, 3 shutdown.register(callback1) shutdown.register(callback2) shutdown.register(callback3) shutdown._handle_shutdown(signal.SIGINT, None) # Should execute in reverse order: 3, 2, 1 assert execution_order == [3, 2, 1] @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_handle_shutdown_with_callback_arguments(self, mock_print, mock_exit): """Test shutdown handling with callback arguments.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) callback_args = [] def test_callback(arg1, arg2, kwarg1=None, kwarg2=None): callback_args.extend([arg1, arg2, kwarg1, kwarg2]) shutdown.register(test_callback, "pos1", "pos2", kwarg1="kw1", kwarg2="kw2") shutdown._handle_shutdown(signal.SIGINT, None) assert callback_args == ["pos1", "pos2", "kw1", "kw2"] @patch('shutdown_handler.signal.Signals') @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_signal_name_handling(self, mock_print, mock_exit, mock_signals): """Test signal name extraction for logging.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) # Mock signal name mock_signal = MagicMock() mock_signal.name = "SIGINT" mock_signals.return_value = mock_signal shutdown._handle_shutdown(signal.SIGINT, None) # Should log with signal name log_calls = mock_logger.info.call_args_list assert any("SIGINT" in str(call) for call in log_calls) @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_signal_name_fallback(self, mock_print, mock_exit): """Test signal name fallback when signal.Signals not available.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) # Temporarily remove Signals attribute from signal module original_signals = getattr(signal, 'Signals', None) if hasattr(signal, 'Signals'): delattr(signal, 'Signals') try: shutdown._handle_shutdown(signal.SIGINT, None) finally: # Restore Signals attribute if it existed if original_signals is not None: signal.Signals = original_signals # Should still work and log with signal number mock_logger.info.assert_called() log_calls = mock_logger.info.call_args_list assert any(str(signal.SIGINT) in str(call) for call in log_calls) def test_callback_registration_with_no_args(self): """Test callback registration with no arguments.""" shutdown = GracefulShutdown() def simple_callback(): pass shutdown.register(simple_callback) callback, args, kwargs = shutdown.shutdown_callbacks[0] assert callback == simple_callback assert args == () assert kwargs == {} def test_empty_shutdown(self): """Test shutdown with no registered callbacks.""" with patch('shutdown_handler.os._exit') as mock_exit, \ patch('builtins.print'): mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) shutdown._handle_shutdown(signal.SIGINT, None) # Should still work and exit normally mock_exit.assert_called_once_with(0) mock_logger.info.assert_called() @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_callback_with_side_effects(self, mock_print, mock_exit): """Test callback that has side effects.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) side_effects = [] def callback_with_side_effects(): side_effects.append("cleanup_performed") # Simulate file cleanup, connection closing, etc. return "cleanup_result" shutdown.register(callback_with_side_effects) shutdown._handle_shutdown(signal.SIGINT, None) assert "cleanup_performed" in side_effects @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_multiple_callback_errors(self, mock_print, mock_exit): """Test handling multiple callback errors.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) def failing_callback_1(): raise ValueError("Error 1") def failing_callback_2(): raise RuntimeError("Error 2") def working_callback(): pass shutdown.register(failing_callback_1) shutdown.register(working_callback) shutdown.register(failing_callback_2) shutdown._handle_shutdown(signal.SIGINT, None) # Should have logged both errors error_calls = mock_logger.error.call_args_list assert len(error_calls) == 2 # Should still exit normally mock_exit.assert_called_once_with(0) def test_shutdown_state_management(self): """Test shutdown state management.""" shutdown = GracefulShutdown() assert shutdown.is_shutting_down is False with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): shutdown._handle_shutdown(signal.SIGINT, None) assert shutdown.is_shutting_down is True @pytest.mark.integration class TestShutdownIntegration: """Integration tests for shutdown handler.""" def test_shutdown_handler_in_context(self): """Test shutdown handler usage in realistic context.""" cleanup_performed = [] def cleanup_database(): cleanup_performed.append("database") def cleanup_files(): cleanup_performed.append("files") def cleanup_network(): cleanup_performed.append("network") shutdown_handler = GracefulShutdown() shutdown_handler.register(cleanup_database) shutdown_handler.register(cleanup_files) shutdown_handler.register(cleanup_network) with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): # Simulate shutdown shutdown_handler._handle_shutdown(signal.SIGTERM, None) # All cleanup should have been performed in reverse order assert cleanup_performed == ["network", "files", "database"] def test_resource_cleanup_scenario(self): """Test realistic resource cleanup scenario.""" resources = { "database_connection": True, "file_handles": True, "network_sockets": True, "temp_files": True } def cleanup_resources(): resources["database_connection"] = False resources["file_handles"] = False resources["network_sockets"] = False resources["temp_files"] = False shutdown_handler = GracefulShutdown() shutdown_handler.register(cleanup_resources) with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): shutdown_handler._handle_shutdown(signal.SIGINT, None) # All resources should be cleaned up assert all(not active for active in resources.values()) def test_shutdown_with_failing_and_succeeding_cleanup(self): """Test mixed success/failure cleanup scenario.""" cleanup_status = {} def critical_cleanup(): cleanup_status["critical"] = "success" def failing_cleanup(): cleanup_status["failing"] = "attempted" raise Exception("Cleanup failed") def optional_cleanup(): cleanup_status["optional"] = "success" shutdown_handler = GracefulShutdown() shutdown_handler.register(critical_cleanup) shutdown_handler.register(failing_cleanup) shutdown_handler.register(optional_cleanup) with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): shutdown_handler._handle_shutdown(signal.SIGINT, None) # Critical and optional should succeed, failing should be attempted assert cleanup_status["critical"] == "success" assert cleanup_status["failing"] == "attempted" assert cleanup_status["optional"] == "success" class TestShutdownHandlerEdgeCases: """Test edge cases and error scenarios.""" def test_callback_modifying_callback_list(self): """Test callback that tries to modify the callback list.""" shutdown = GracefulShutdown() def modifying_callback(): # Try to add another callback during shutdown def new_callback(): pass shutdown.register(new_callback) shutdown.register(modifying_callback) with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): # Should not crash shutdown._handle_shutdown(signal.SIGINT, None) def test_callback_infinite_loop_protection(self): """Test protection against callback infinite loops.""" shutdown = GracefulShutdown() call_count = [0] def recursive_callback(): call_count[0] += 1 if call_count[0] < 5: # Prevent actual infinite loop in test # This would be bad in real code pass shutdown.register(recursive_callback) with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): shutdown._handle_shutdown(signal.SIGINT, None) # Should complete without hanging assert call_count[0] > 0 @patch('shutdown_handler.os._exit') @patch('builtins.print') def test_callback_with_complex_exception(self, mock_print, mock_exit): """Test callback with complex exception handling.""" mock_logger = MagicMock() shutdown = GracefulShutdown(logger=mock_logger) def complex_failing_callback(): try: raise ValueError("Inner error") except ValueError as e: raise RuntimeError("Outer error") from e shutdown.register(complex_failing_callback) shutdown._handle_shutdown(signal.SIGINT, None) # Should handle complex exception chains mock_logger.error.assert_called() mock_exit.assert_called_once_with(0) def test_very_long_callback_list(self): """Test shutdown with many callbacks.""" shutdown = GracefulShutdown() execution_count = [0] def counting_callback(): execution_count[0] += 1 # Register many callbacks for _ in range(100): shutdown.register(counting_callback) with patch('shutdown_handler.os._exit'), \ patch('builtins.print'): shutdown._handle_shutdown(signal.SIGINT, None) # All callbacks should execute assert execution_count[0] == 100

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/brianirish/laravel-mcp-companion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server