mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
469 lines
13 KiB
Python
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
|