mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-03 03:01:31 +08:00
test(dirty): add Docker setup for dirty example integration tests
- Add Dockerfile and docker-compose.yml for running examples in containers - Add test_integration.py for HTTP-level integration testing - Update test_worker_integration.py to use MockWriter for handle_request - Use integer request IDs for binary protocol compatibility - Add GUNICORN_BIND env var support in gunicorn_conf.py for Docker
This commit is contained in:
parent
00da70292f
commit
c0cc8c0de0
25
examples/dirty_example/Dockerfile
Normal file
25
examples/dirty_example/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# This file is part of gunicorn released under the MIT license.
|
||||||
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy gunicorn source
|
||||||
|
COPY . /app/gunicorn-src
|
||||||
|
|
||||||
|
# Install gunicorn and dependencies
|
||||||
|
# setproctitle is needed for process title changes
|
||||||
|
RUN pip install --no-cache-dir /app/gunicorn-src setproctitle
|
||||||
|
|
||||||
|
# Copy example files
|
||||||
|
COPY examples/dirty_example/ /app/examples/dirty_example/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Expose the port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Default command - run the example tests
|
||||||
|
CMD ["python", "-m", "pytest", "-v", "examples/dirty_example/"]
|
||||||
54
examples/dirty_example/docker-compose.yml
Normal file
54
examples/dirty_example/docker-compose.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#
|
||||||
|
# This file is part of gunicorn released under the MIT license.
|
||||||
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Run the example tests (protocol, dirty app, worker integration)
|
||||||
|
tests:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: examples/dirty_example/Dockerfile
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
echo '=== Running Protocol Tests ===' &&
|
||||||
|
python examples/dirty_example/test_protocol.py &&
|
||||||
|
echo '' &&
|
||||||
|
echo '=== Running Dirty App Tests ===' &&
|
||||||
|
python examples/dirty_example/test_dirty_app.py &&
|
||||||
|
echo '' &&
|
||||||
|
echo '=== Running Worker Integration Tests ===' &&
|
||||||
|
python examples/dirty_example/test_worker_integration.py &&
|
||||||
|
echo '' &&
|
||||||
|
echo '=== All tests passed! ==='
|
||||||
|
"
|
||||||
|
|
||||||
|
# Run the full gunicorn server with dirty workers
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: examples/dirty_example/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
environment:
|
||||||
|
- GUNICORN_BIND=0.0.0.0:8000
|
||||||
|
command: >
|
||||||
|
gunicorn examples.dirty_example.wsgi_app:app
|
||||||
|
-c examples/dirty_example/gunicorn_conf.py
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
# Run integration test against the server
|
||||||
|
integration-test:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: examples/dirty_example/Dockerfile
|
||||||
|
depends_on:
|
||||||
|
server:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- TEST_BASE_URL=http://server:8000
|
||||||
|
command: python examples/dirty_example/test_integration.py
|
||||||
@ -11,7 +11,9 @@ Run with:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Basic settings
|
# Basic settings
|
||||||
bind = "127.0.0.1:8000"
|
# Use 0.0.0.0 for Docker, override with GUNICORN_BIND env var if needed
|
||||||
|
import os
|
||||||
|
bind = os.environ.get("GUNICORN_BIND", "127.0.0.1:8000")
|
||||||
workers = 2
|
workers = 2
|
||||||
worker_class = "sync"
|
worker_class = "sync"
|
||||||
timeout = 30
|
timeout = 30
|
||||||
|
|||||||
81
examples/dirty_example/test_integration.py
Normal file
81
examples/dirty_example/test_integration.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# This file is part of gunicorn released under the MIT license.
|
||||||
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Integration test for the dirty example server.
|
||||||
|
|
||||||
|
This tests that the full gunicorn server with dirty workers responds
|
||||||
|
correctly to HTTP requests.
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
python examples/dirty_example/test_integration.py [base_url]
|
||||||
|
|
||||||
|
Default base_url is http://localhost:8000
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def test_endpoint(base, path, expected_key=None):
|
||||||
|
"""Test an endpoint and check for expected key in response."""
|
||||||
|
url = base + path
|
||||||
|
print(f"Testing: {url}")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=10) as resp:
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
print(f" Response: {str(data)[:200]}")
|
||||||
|
if expected_key and expected_key not in data:
|
||||||
|
print(f" ERROR: Expected key '{expected_key}' not found!")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(f" HTTP ERROR {e.code}: {e.reason}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Get base URL from env or command line
|
||||||
|
base = os.environ.get("TEST_BASE_URL", "http://localhost:8000")
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
base = sys.argv[1]
|
||||||
|
|
||||||
|
print(f"Testing dirty example server at: {base}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Define tests: (path, expected_key_in_response)
|
||||||
|
tests = [
|
||||||
|
("/", "endpoints"),
|
||||||
|
("/models", "models"),
|
||||||
|
("/load?name=test-model", "status"),
|
||||||
|
("/inference?model=default&data=hello", "prediction"),
|
||||||
|
("/fibonacci?n=20", "result"),
|
||||||
|
("/prime?n=17", "is_prime"),
|
||||||
|
("/stats", "ml_app"),
|
||||||
|
("/unload?name=test-model", "status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
failed = 0
|
||||||
|
for path, key in tests:
|
||||||
|
if not test_endpoint(base, path, key):
|
||||||
|
failed += 1
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
if failed:
|
||||||
|
print(f"FAILED: {failed} tests failed")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("SUCCESS: All integration tests passed!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -22,7 +22,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspa
|
|||||||
|
|
||||||
from gunicorn.config import Config
|
from gunicorn.config import Config
|
||||||
from gunicorn.dirty.worker import DirtyWorker
|
from gunicorn.dirty.worker import DirtyWorker
|
||||||
from gunicorn.dirty.protocol import DirtyProtocol, make_request
|
from gunicorn.dirty.protocol import DirtyProtocol, BinaryProtocol, make_request, HEADER_SIZE
|
||||||
|
|
||||||
|
|
||||||
class MockLog:
|
class MockLog:
|
||||||
@ -35,6 +35,36 @@ class MockLog:
|
|||||||
def reopen_files(self): pass
|
def reopen_files(self): pass
|
||||||
|
|
||||||
|
|
||||||
|
class MockWriter:
|
||||||
|
"""Mock StreamWriter that captures written responses."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.messages = []
|
||||||
|
self._buffer = b""
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self._buffer += data
|
||||||
|
|
||||||
|
async def drain(self):
|
||||||
|
# Decode messages from buffer using binary protocol
|
||||||
|
while len(self._buffer) >= HEADER_SIZE:
|
||||||
|
_, _, length = BinaryProtocol.decode_header(self._buffer[:HEADER_SIZE])
|
||||||
|
total_size = HEADER_SIZE + length
|
||||||
|
if len(self._buffer) >= total_size:
|
||||||
|
msg_data = self._buffer[:total_size]
|
||||||
|
self._buffer = self._buffer[total_size:]
|
||||||
|
msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)
|
||||||
|
result = {"type": msg_type_str, "id": request_id}
|
||||||
|
result.update(payload_dict)
|
||||||
|
self.messages.append(result)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
def get_last_response(self):
|
||||||
|
"""Get the last response message."""
|
||||||
|
return self.messages[-1] if self.messages else None
|
||||||
|
|
||||||
|
|
||||||
async def test_worker_request_handling():
|
async def test_worker_request_handling():
|
||||||
"""Test that a worker can load apps and handle requests."""
|
"""Test that a worker can load apps and handle requests."""
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@ -75,52 +105,60 @@ async def test_worker_request_handling():
|
|||||||
# Test handle_request with a proper request message
|
# Test handle_request with a proper request message
|
||||||
print("\n3. Testing handle_request() - load_model...")
|
print("\n3. Testing handle_request() - load_model...")
|
||||||
request = make_request(
|
request = make_request(
|
||||||
request_id="test-001",
|
request_id=1001,
|
||||||
app_path="examples.dirty_example.dirty_app:MLApp",
|
app_path="examples.dirty_example.dirty_app:MLApp",
|
||||||
action="load_model",
|
action="load_model",
|
||||||
args=("gpt-4",),
|
args=("gpt-4",),
|
||||||
kwargs={}
|
kwargs={}
|
||||||
)
|
)
|
||||||
response = await worker.handle_request(request)
|
writer = MockWriter()
|
||||||
|
await worker.handle_request(request, writer)
|
||||||
|
response = writer.get_last_response()
|
||||||
print(f" Response type: {response['type']}")
|
print(f" Response type: {response['type']}")
|
||||||
print(f" Result: {response.get('result', response.get('error'))}")
|
print(f" Result: {response.get('result', response.get('error'))}")
|
||||||
|
|
||||||
# Test inference
|
# Test inference
|
||||||
print("\n4. Testing handle_request() - inference...")
|
print("\n4. Testing handle_request() - inference...")
|
||||||
request = make_request(
|
request = make_request(
|
||||||
request_id="test-002",
|
request_id=1002,
|
||||||
app_path="examples.dirty_example.dirty_app:MLApp",
|
app_path="examples.dirty_example.dirty_app:MLApp",
|
||||||
action="inference",
|
action="inference",
|
||||||
args=("default", "Hello AI!"),
|
args=("default", "Hello AI!"),
|
||||||
kwargs={}
|
kwargs={}
|
||||||
)
|
)
|
||||||
response = await worker.handle_request(request)
|
writer = MockWriter()
|
||||||
|
await worker.handle_request(request, writer)
|
||||||
|
response = writer.get_last_response()
|
||||||
print(f" Response type: {response['type']}")
|
print(f" Response type: {response['type']}")
|
||||||
print(f" Result: {response.get('result', response.get('error'))}")
|
print(f" Result: {response.get('result', response.get('error'))}")
|
||||||
|
|
||||||
# Test error handling
|
# Test error handling
|
||||||
print("\n5. Testing error handling - unknown action...")
|
print("\n5. Testing error handling - unknown action...")
|
||||||
request = make_request(
|
request = make_request(
|
||||||
request_id="test-003",
|
request_id=1003,
|
||||||
app_path="examples.dirty_example.dirty_app:MLApp",
|
app_path="examples.dirty_example.dirty_app:MLApp",
|
||||||
action="nonexistent_action",
|
action="nonexistent_action",
|
||||||
args=(),
|
args=(),
|
||||||
kwargs={}
|
kwargs={}
|
||||||
)
|
)
|
||||||
response = await worker.handle_request(request)
|
writer = MockWriter()
|
||||||
|
await worker.handle_request(request, writer)
|
||||||
|
response = writer.get_last_response()
|
||||||
print(f" Response type: {response['type']}")
|
print(f" Response type: {response['type']}")
|
||||||
print(f" Error: {response.get('error', {}).get('message')}")
|
print(f" Error: {response.get('error', {}).get('message')}")
|
||||||
|
|
||||||
# Test app not found
|
# Test app not found
|
||||||
print("\n6. Testing error handling - app not found...")
|
print("\n6. Testing error handling - app not found...")
|
||||||
request = make_request(
|
request = make_request(
|
||||||
request_id="test-004",
|
request_id=1004,
|
||||||
app_path="nonexistent:App",
|
app_path="nonexistent:App",
|
||||||
action="test",
|
action="test",
|
||||||
args=(),
|
args=(),
|
||||||
kwargs={}
|
kwargs={}
|
||||||
)
|
)
|
||||||
response = await worker.handle_request(request)
|
writer = MockWriter()
|
||||||
|
await worker.handle_request(request, writer)
|
||||||
|
response = writer.get_last_response()
|
||||||
print(f" Response type: {response['type']}")
|
print(f" Response type: {response['type']}")
|
||||||
print(f" Error type: {response.get('error', {}).get('error_type')}")
|
print(f" Error type: {response.get('error', {}).get('error_type')}")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user