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
"""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