diff --git a/requirements_test.txt b/requirements_test.txt index 4f98848f..98e266d8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,4 @@ gevent -eventlet coverage pytest>=7.2.0 pytest-cov diff --git a/tests/workers/test_geventlet.py b/tests/workers/test_geventlet.py deleted file mode 100644 index 0719f038..00000000 --- a/tests/workers/test_geventlet.py +++ /dev/null @@ -1,416 +0,0 @@ -# -# This file is part of gunicorn released under the MIT license. -# See the NOTICE for more information. - -import pytest -import sys -from unittest import mock - - -def test_import(): - """Test that the eventlet worker module can be imported.""" - try: - import eventlet - except AttributeError: - if (3, 13) > sys.version_info >= (3, 12): - pytest.skip("Ignoring eventlet failures on Python 3.12") - raise - __import__('gunicorn.workers.geventlet') - - -class TestVersionRequirement: - """Tests for eventlet version requirement checks.""" - - def test_import_error_message(self): - """Test that ImportError gives correct version message.""" - with mock.patch.dict('sys.modules', {'eventlet': None}): - # Clear cached module if present - sys.modules.pop('gunicorn.workers.geventlet', None) - with pytest.raises(RuntimeError, match="eventlet 0.40.3"): - import importlib - import gunicorn.workers.geventlet - importlib.reload(gunicorn.workers.geventlet) - - def test_version_check_requires_0_40_3(self): - """Test that version check requires eventlet 0.40.3 or higher.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from packaging.version import parse as parse_version - min_version = parse_version('0.40.3') - current_version = parse_version(eventlet.__version__) - - # If we got this far, the import succeeded, meaning version is sufficient - assert current_version >= min_version - - -@pytest.fixture -def eventlet_worker(): - """Fixture to create an EventletWorker instance for testing.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import EventletWorker - - # Create a minimal mock config - cfg = mock.MagicMock() - cfg.keepalive = 2 - cfg.graceful_timeout = 30 - cfg.is_ssl = False - cfg.worker_connections = 1000 - - # Create worker with mocked dependencies - worker = EventletWorker.__new__(EventletWorker) - worker.cfg = cfg - worker.alive = True - worker.sockets = [] - worker.log = mock.MagicMock() - - return worker - - -class TestEventletWorker: - """Tests for EventletWorker class.""" - - def test_worker_class_exists(self): - """Test that EventletWorker class is properly defined.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import EventletWorker - from gunicorn.workers.base_async import AsyncWorker - - assert issubclass(EventletWorker, AsyncWorker) - - def test_patch_method_calls_use_hub(self, eventlet_worker): - """Test that patch() calls hubs.use_hub(). - - hubs.use_hub() must be called in patch() (after fork) because it creates - OS resources like kqueue that don't survive fork. - """ - from eventlet import hubs - - with mock.patch.object(hubs, 'use_hub') as mock_use_hub: - with mock.patch('gunicorn.workers.geventlet.patch_sendfile'): - eventlet_worker.patch() - - mock_use_hub.assert_called_once() - - def test_patch_method_calls_patch_sendfile(self, eventlet_worker): - """Test that patch() calls patch_sendfile().""" - from eventlet import hubs - - with mock.patch.object(hubs, 'use_hub'): - with mock.patch('gunicorn.workers.geventlet.patch_sendfile') as mock_sf: - eventlet_worker.patch() - - mock_sf.assert_called_once() - - def test_monkey_patch_called_at_import_time(self): - """Test that monkey_patch is called at module import time. - - Note: hubs.use_hub() and eventlet.monkey_patch() are called at module - import time (not in patch()) to ensure all imports are properly patched. - This test verifies the module was patched by checking eventlet state. - """ - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - # Verify eventlet has been patched by checking that socket is patched - import socket - from eventlet.greenio import GreenSocket - - # After monkey patching, socket.socket should be GreenSocket - assert socket.socket is GreenSocket - - def test_timeout_ctx_returns_eventlet_timeout(self, eventlet_worker): - """Test that timeout_ctx() returns an eventlet.Timeout.""" - import eventlet - - timeout = eventlet_worker.timeout_ctx() - assert isinstance(timeout, eventlet.Timeout) - - def test_timeout_ctx_uses_keepalive_config(self, eventlet_worker): - """Test that timeout_ctx() uses cfg.keepalive value.""" - import eventlet - - eventlet_worker.cfg.keepalive = 5 - with mock.patch.object(eventlet, 'Timeout') as mock_timeout: - eventlet_worker.timeout_ctx() - - mock_timeout.assert_called_once_with(5, False) - - def test_timeout_ctx_with_no_keepalive(self, eventlet_worker): - """Test that timeout_ctx() handles no keepalive (None or 0).""" - import eventlet - - eventlet_worker.cfg.keepalive = 0 - with mock.patch.object(eventlet, 'Timeout') as mock_timeout: - eventlet_worker.timeout_ctx() - - mock_timeout.assert_called_once_with(None, False) - - def test_handle_quit_spawns_greenthread(self, eventlet_worker): - """Test that handle_quit() spawns a greenthread.""" - import eventlet - - with mock.patch.object(eventlet, 'spawn') as mock_spawn: - eventlet_worker.handle_quit(None, None) - - mock_spawn.assert_called_once() - - def test_handle_usr1_spawns_greenthread(self, eventlet_worker): - """Test that handle_usr1() spawns a greenthread.""" - import eventlet - - with mock.patch.object(eventlet, 'spawn') as mock_spawn: - eventlet_worker.handle_usr1(None, None) - - mock_spawn.assert_called_once() - - def test_handle_wraps_ssl_when_configured(self, eventlet_worker): - """Test that handle() wraps socket with SSL when is_ssl is True.""" - from gunicorn.workers import geventlet - - eventlet_worker.cfg.is_ssl = True - mock_client = mock.MagicMock() - mock_listener = mock.MagicMock() - - with mock.patch.object(geventlet, 'ssl_wrap_socket') as mock_ssl: - mock_ssl.return_value = mock_client - with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle'): - eventlet_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000)) - - mock_ssl.assert_called_once_with(mock_client, eventlet_worker.cfg) - - def test_handle_no_ssl_when_not_configured(self, eventlet_worker): - """Test that handle() does not wrap SSL when is_ssl is False.""" - from gunicorn.workers import geventlet - - eventlet_worker.cfg.is_ssl = False - mock_client = mock.MagicMock() - mock_listener = mock.MagicMock() - - with mock.patch.object(geventlet, 'ssl_wrap_socket') as mock_ssl: - with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle'): - eventlet_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000)) - - mock_ssl.assert_not_called() - - -class TestAlreadyHandled: - """Tests for is_already_handled() method.""" - - def test_is_already_handled_new_style(self, eventlet_worker): - """Test is_already_handled with eventlet >= 0.30.3 (WSGI_LOCAL).""" - from gunicorn.workers import geventlet - - # Mock the new-style WSGI_LOCAL.already_handled - mock_wsgi_local = mock.MagicMock() - mock_wsgi_local.already_handled = True - - with mock.patch.object(geventlet, 'EVENTLET_WSGI_LOCAL', mock_wsgi_local): - with pytest.raises(StopIteration): - eventlet_worker.is_already_handled(mock.MagicMock()) - - def test_is_already_handled_old_style(self, eventlet_worker): - """Test is_already_handled with eventlet < 0.30.3 (ALREADY_HANDLED).""" - from gunicorn.workers import geventlet - - sentinel = object() - - with mock.patch.object(geventlet, 'EVENTLET_WSGI_LOCAL', None): - with mock.patch.object(geventlet, 'EVENTLET_ALREADY_HANDLED', sentinel): - with pytest.raises(StopIteration): - eventlet_worker.is_already_handled(sentinel) - - def test_is_already_handled_returns_parent_result(self, eventlet_worker): - """Test is_already_handled falls through to parent when not handled.""" - from gunicorn.workers import geventlet - - with mock.patch.object(geventlet, 'EVENTLET_WSGI_LOCAL', None): - with mock.patch.object(geventlet, 'EVENTLET_ALREADY_HANDLED', None): - with mock.patch('gunicorn.workers.base_async.AsyncWorker.is_already_handled') as mock_parent: - mock_parent.return_value = False - result = eventlet_worker.is_already_handled(mock.MagicMock()) - - assert result is False - mock_parent.assert_called_once() - - -class TestPatchSendfile: - """Tests for patch_sendfile() function.""" - - def test_patch_sendfile_adds_method_when_missing(self): - """Test that patch_sendfile adds sendfile to GreenSocket if missing.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import patch_sendfile, _eventlet_socket_sendfile - from eventlet.greenio import GreenSocket - - # Remove sendfile if it exists - original = getattr(GreenSocket, 'sendfile', None) - if hasattr(GreenSocket, 'sendfile'): - delattr(GreenSocket, 'sendfile') - - try: - patch_sendfile() - assert hasattr(GreenSocket, 'sendfile') - assert GreenSocket.sendfile == _eventlet_socket_sendfile - finally: - # Restore original state - if original is not None: - GreenSocket.sendfile = original - elif hasattr(GreenSocket, 'sendfile'): - delattr(GreenSocket, 'sendfile') - - def test_patch_sendfile_preserves_existing_method(self): - """Test that patch_sendfile does not override existing sendfile.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import patch_sendfile - from eventlet.greenio import GreenSocket - - # If sendfile exists, it should be preserved - if hasattr(GreenSocket, 'sendfile'): - original = GreenSocket.sendfile - patch_sendfile() - assert GreenSocket.sendfile == original - - -class TestEventletSocketSendfile: - """Tests for _eventlet_socket_sendfile() function.""" - - def test_sendfile_raises_on_non_blocking(self): - """Test that sendfile raises ValueError for non-blocking sockets.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import _eventlet_socket_sendfile - - mock_socket = mock.MagicMock() - mock_socket.gettimeout.return_value = 0 - - with pytest.raises(ValueError, match="non-blocking"): - _eventlet_socket_sendfile(mock_socket, mock.MagicMock()) - - def test_sendfile_seeks_to_offset(self): - """Test that sendfile seeks to offset if provided.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import _eventlet_socket_sendfile - - mock_socket = mock.MagicMock() - mock_socket.gettimeout.return_value = 1 - mock_file = mock.MagicMock() - mock_file.read.return_value = b'' - - _eventlet_socket_sendfile(mock_socket, mock_file, offset=100) - - mock_file.seek.assert_any_call(100) - - def test_sendfile_returns_total_sent(self): - """Test that sendfile returns the total bytes sent.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import _eventlet_socket_sendfile - - mock_socket = mock.MagicMock() - mock_socket.gettimeout.return_value = 1 - mock_socket.send.return_value = 10 - - mock_file = mock.MagicMock() - mock_file.read.side_effect = [b'x' * 10, b''] - - result = _eventlet_socket_sendfile(mock_socket, mock_file) - - assert result == 10 - - -class TestEventletServe: - """Tests for _eventlet_serve() function.""" - - def test_serve_creates_green_pool(self): - """Test that _eventlet_serve creates a GreenPool.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import _eventlet_serve - - mock_sock = mock.MagicMock() - mock_sock.accept.side_effect = eventlet.StopServe() - - with mock.patch.object(eventlet.greenpool, 'GreenPool') as mock_pool: - mock_pool_instance = mock.MagicMock() - mock_pool.return_value = mock_pool_instance - mock_pool_instance.waitall.return_value = None - - _eventlet_serve(mock_sock, mock.MagicMock(), 100) - - mock_pool.assert_called_once_with(100) - - -class TestEventletStop: - """Tests for _eventlet_stop() function.""" - - def test_stop_waits_for_client(self): - """Test that _eventlet_stop waits for the client greenlet.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import _eventlet_stop - - mock_client = mock.MagicMock() - mock_server = mock.MagicMock() - mock_conn = mock.MagicMock() - - _eventlet_stop(mock_client, mock_server, mock_conn) - - mock_client.wait.assert_called_once() - mock_conn.close.assert_called_once() - - def test_stop_closes_connection_on_greenlet_exit(self): - """Test that connection is closed even on GreenletExit.""" - try: - import eventlet - import greenlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import _eventlet_stop - - mock_client = mock.MagicMock() - mock_client.wait.side_effect = greenlet.GreenletExit() - mock_server = mock.MagicMock() - mock_conn = mock.MagicMock() - - # Should not raise - _eventlet_stop(mock_client, mock_server, mock_conn) - - mock_conn.close.assert_called_once()