# # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. """Tests for control socket command handlers.""" import signal import time from unittest.mock import MagicMock, patch from gunicorn.ctl.handlers import CommandHandlers class MockWorker: """Mock worker for testing.""" def __init__(self, pid, age, booted=True, aborted=False): self.pid = pid self.age = age self.booted = booted self.aborted = aborted self.tmp = MagicMock() self.tmp.last_update.return_value = time.monotonic() class MockListener: """Mock listener for testing.""" def __init__(self, address, fd=3): self._address = address self._fd = fd self.sock = MagicMock() self.sock.family = 2 # AF_INET def __str__(self): return self._address def fileno(self): return self._fd class MockConfig: """Mock config for testing.""" def __init__(self): self.bind = ['127.0.0.1:8000'] self.workers = 4 self.worker_class = 'sync' self.threads = 1 self.timeout = 30 self.graceful_timeout = 30 self.keepalive = 2 self.max_requests = 0 self.max_requests_jitter = 0 self.worker_connections = 1000 self.preload_app = False self.daemon = False self.pidfile = None self.proc_name = 'test_app' self.reload = False self.dirty_workers = 0 self.dirty_apps = [] self.dirty_timeout = 30 self.control_socket = 'gunicorn.ctl' self.control_socket_disable = False class MockArbiter: """Mock arbiter for testing.""" def __init__(self): self.cfg = MockConfig() self.pid = 12345 self.WORKERS = {} self.LISTENERS = [] self.dirty_arbiter_pid = 0 self.dirty_arbiter = None self.num_workers = 4 self._stats = { 'start_time': time.time() - 3600, # 1 hour ago 'workers_spawned': 10, 'workers_killed': 5, 'reloads': 2, } def wakeup(self): pass class TestShowWorkers: """Tests for show workers command.""" def test_show_workers_empty(self): """Test showing workers when none exist.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.show_workers() assert result["workers"] == [] assert result["count"] == 0 def test_show_workers_with_workers(self): """Test showing workers.""" arbiter = MockArbiter() arbiter.WORKERS = { 1001: MockWorker(1001, 1), 1002: MockWorker(1002, 2), 1003: MockWorker(1003, 3), } handlers = CommandHandlers(arbiter) result = handlers.show_workers() assert result["count"] == 3 assert len(result["workers"]) == 3 # Verify sorted by age ages = [w["age"] for w in result["workers"]] assert ages == sorted(ages) # Verify worker data worker = result["workers"][0] assert "pid" in worker assert "age" in worker assert "booted" in worker assert "last_heartbeat" in worker class TestShowStats: """Tests for show stats command.""" def test_show_stats(self): """Test showing stats.""" arbiter = MockArbiter() arbiter.WORKERS = { 1001: MockWorker(1001, 1), 1002: MockWorker(1002, 2), } handlers = CommandHandlers(arbiter) result = handlers.show_stats() assert result["pid"] == 12345 assert result["workers_current"] == 2 assert result["workers_target"] == 4 assert result["workers_spawned"] == 10 assert result["workers_killed"] == 5 assert result["reloads"] == 2 assert result["uptime"] is not None assert result["uptime"] > 0 class TestShowConfig: """Tests for show config command.""" def test_show_config(self): """Test showing config.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.show_config() assert result["workers"] == 4 assert result["timeout"] == 30 assert result["bind"] == ['127.0.0.1:8000'] class TestShowListeners: """Tests for show listeners command.""" def test_show_listeners_empty(self): """Test showing listeners when none exist.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.show_listeners() assert result["listeners"] == [] assert result["count"] == 0 def test_show_listeners(self): """Test showing listeners.""" arbiter = MockArbiter() arbiter.LISTENERS = [ MockListener("127.0.0.1:8000", fd=3), MockListener("127.0.0.1:8001", fd=4), ] handlers = CommandHandlers(arbiter) result = handlers.show_listeners() assert result["count"] == 2 assert len(result["listeners"]) == 2 assert result["listeners"][0]["address"] == "127.0.0.1:8000" class TestWorkerAdd: """Tests for worker add command.""" def test_worker_add_default(self): """Test adding one worker (default).""" arbiter = MockArbiter() arbiter.wakeup = MagicMock() handlers = CommandHandlers(arbiter) result = handlers.worker_add() assert result["added"] == 1 assert result["previous"] == 4 assert result["total"] == 5 assert arbiter.num_workers == 5 arbiter.wakeup.assert_called_once() def test_worker_add_multiple(self): """Test adding multiple workers.""" arbiter = MockArbiter() arbiter.wakeup = MagicMock() handlers = CommandHandlers(arbiter) result = handlers.worker_add(3) assert result["added"] == 3 assert result["total"] == 7 class TestWorkerRemove: """Tests for worker remove command.""" def test_worker_remove_default(self): """Test removing one worker (default).""" arbiter = MockArbiter() arbiter.wakeup = MagicMock() handlers = CommandHandlers(arbiter) result = handlers.worker_remove() assert result["removed"] == 1 assert result["previous"] == 4 assert result["total"] == 3 assert arbiter.num_workers == 3 arbiter.wakeup.assert_called_once() def test_worker_remove_cannot_go_below_one(self): """Test that worker count cannot go below 1.""" arbiter = MockArbiter() arbiter.num_workers = 2 arbiter.wakeup = MagicMock() handlers = CommandHandlers(arbiter) result = handlers.worker_remove(5) assert result["removed"] == 1 assert result["total"] == 1 assert arbiter.num_workers == 1 class TestWorkerKill: """Tests for worker kill command.""" def test_worker_kill_success(self): """Test killing a worker.""" arbiter = MockArbiter() arbiter.WORKERS = {1001: MockWorker(1001, 1)} handlers = CommandHandlers(arbiter) with patch('os.kill') as mock_kill: result = handlers.worker_kill(1001) assert result["success"] is True assert result["killed"] == 1001 mock_kill.assert_called_once_with(1001, signal.SIGTERM) def test_worker_kill_not_found(self): """Test killing a non-existent worker.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.worker_kill(9999) assert result["success"] is False assert "not found" in result["error"] class TestShowDirty: """Tests for show dirty command.""" def test_show_dirty_disabled(self): """Test showing dirty when disabled.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.show_dirty() assert result["enabled"] is False assert result["pid"] is None class TestDirtyAdd: """Tests for dirty add command.""" def test_dirty_add_not_running(self): """Test dirty add when dirty arbiter not running.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.dirty_add() assert result["success"] is False assert "not running" in result["error"] def test_dirty_add_no_socket(self): """Test dirty add when socket path not available.""" arbiter = MockArbiter() arbiter.dirty_arbiter_pid = 2000 handlers = CommandHandlers(arbiter) # No dirty_arbiter attribute and no env var with patch.dict('os.environ', {}, clear=True): result = handlers.dirty_add() assert result["success"] is False assert "socket" in result["error"].lower() class TestDirtyRemove: """Tests for dirty remove command.""" def test_dirty_remove_not_running(self): """Test dirty remove when dirty arbiter not running.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.dirty_remove() assert result["success"] is False assert "not running" in result["error"] def test_dirty_remove_no_socket(self): """Test dirty remove when socket path not available.""" arbiter = MockArbiter() arbiter.dirty_arbiter_pid = 2000 handlers = CommandHandlers(arbiter) # No dirty_arbiter attribute and no env var with patch.dict('os.environ', {}, clear=True): result = handlers.dirty_remove() assert result["success"] is False assert "socket" in result["error"].lower() class TestReload: """Tests for reload command.""" def test_reload(self): """Test reload command.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) with patch('os.kill') as mock_kill: result = handlers.reload() assert result["status"] == "reloading" mock_kill.assert_called_once_with(12345, signal.SIGHUP) class TestReopen: """Tests for reopen command.""" def test_reopen(self): """Test reopen command.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) with patch('os.kill') as mock_kill: result = handlers.reopen() assert result["status"] == "reopening" mock_kill.assert_called_once_with(12345, signal.SIGUSR1) class TestShutdown: """Tests for shutdown command.""" def test_shutdown_graceful(self): """Test graceful shutdown.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) with patch('os.kill') as mock_kill: result = handlers.shutdown() assert result["status"] == "shutting_down" assert result["mode"] == "graceful" mock_kill.assert_called_once_with(12345, signal.SIGTERM) def test_shutdown_quick(self): """Test quick shutdown.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) with patch('os.kill') as mock_kill: result = handlers.shutdown("quick") assert result["status"] == "shutting_down" assert result["mode"] == "quick" mock_kill.assert_called_once_with(12345, signal.SIGINT) class TestShowAll: """Tests for show all command.""" def test_show_all_basic(self): """Test show all command.""" arbiter = MockArbiter() arbiter.WORKERS = { 1001: MockWorker(1001, 1), 1002: MockWorker(1002, 2), } handlers = CommandHandlers(arbiter) result = handlers.show_all() assert "arbiter" in result assert result["arbiter"]["pid"] == 12345 assert result["arbiter"]["type"] == "arbiter" assert "web_workers" in result assert result["web_worker_count"] == 2 assert len(result["web_workers"]) == 2 assert "dirty_arbiter" in result assert result["dirty_arbiter"] is None # No dirty workers when no dirty arbiter assert result["dirty_worker_count"] == 0 def test_show_all_with_dirty(self): """Test show all with dirty arbiter running.""" arbiter = MockArbiter() arbiter.dirty_arbiter_pid = 2000 handlers = CommandHandlers(arbiter) result = handlers.show_all() assert result["dirty_arbiter"] is not None assert result["dirty_arbiter"]["pid"] == 2000 assert result["dirty_arbiter"]["type"] == "dirty_arbiter" class TestHelp: """Tests for help command.""" def test_help(self): """Test help command.""" arbiter = MockArbiter() handlers = CommandHandlers(arbiter) result = handlers.help() assert "commands" in result commands = result["commands"] assert "show all" in commands assert "show workers" in commands assert "worker add [N]" in commands assert "reload" in commands assert "shutdown [graceful|quick]" in commands