mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +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
|
||||
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
|
||||
worker_class = "sync"
|
||||
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.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:
|
||||
@ -35,6 +35,36 @@ class MockLog:
|
||||
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():
|
||||
"""Test that a worker can load apps and handle requests."""
|
||||
print("=" * 60)
|
||||
@ -75,52 +105,60 @@ async def test_worker_request_handling():
|
||||
# Test handle_request with a proper request message
|
||||
print("\n3. Testing handle_request() - load_model...")
|
||||
request = make_request(
|
||||
request_id="test-001",
|
||||
request_id=1001,
|
||||
app_path="examples.dirty_example.dirty_app:MLApp",
|
||||
action="load_model",
|
||||
args=("gpt-4",),
|
||||
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" Result: {response.get('result', response.get('error'))}")
|
||||
|
||||
# Test inference
|
||||
print("\n4. Testing handle_request() - inference...")
|
||||
request = make_request(
|
||||
request_id="test-002",
|
||||
request_id=1002,
|
||||
app_path="examples.dirty_example.dirty_app:MLApp",
|
||||
action="inference",
|
||||
args=("default", "Hello AI!"),
|
||||
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" Result: {response.get('result', response.get('error'))}")
|
||||
|
||||
# Test error handling
|
||||
print("\n5. Testing error handling - unknown action...")
|
||||
request = make_request(
|
||||
request_id="test-003",
|
||||
request_id=1003,
|
||||
app_path="examples.dirty_example.dirty_app:MLApp",
|
||||
action="nonexistent_action",
|
||||
args=(),
|
||||
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" Error: {response.get('error', {}).get('message')}")
|
||||
|
||||
# Test app not found
|
||||
print("\n6. Testing error handling - app not found...")
|
||||
request = make_request(
|
||||
request_id="test-004",
|
||||
request_id=1004,
|
||||
app_path="nonexistent:App",
|
||||
action="test",
|
||||
args=(),
|
||||
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" Error type: {response.get('error', {}).get('error_type')}")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user