gunicorn/tests/ctl/test_handlers.py
2026-02-13 02:35:02 +01:00

469 lines
13 KiB
Python

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