# # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. """Tests for control socket command handlers.""" import os import signal import time from unittest.mock import MagicMock, patch import pytest 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