gunicorn/tests/docker/http2/conftest.py
Benoit Chesneau 95b7ffeeaa chore: prepare release 25.0.2
- Bump version to 25.0.2
- Update copyright year to 2026 in LICENSE and NOTICE
- Add license headers to all Python source files
- Add changelog entry for 25.0.2
2026-02-06 08:21:18 +01:00

201 lines
5.7 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Pytest fixtures for HTTP/2 Docker integration tests."""
import subprocess
import time
from pathlib import Path
import pytest
# Directory containing this conftest.py
DOCKER_DIR = Path(__file__).parent
CERTS_DIR = DOCKER_DIR / "certs"
def generate_self_signed_cert(certs_dir: Path) -> None:
"""Generate self-signed SSL certificates for testing."""
certs_dir.mkdir(parents=True, exist_ok=True)
cert_file = certs_dir / "server.crt"
key_file = certs_dir / "server.key"
# Skip if certs already exist and are recent (less than 1 day old)
if cert_file.exists() and key_file.exists():
age = time.time() - cert_file.stat().st_mtime
if age < 86400: # 1 day
return
# Generate self-signed certificate
subprocess.run(
[
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", str(key_file),
"-out", str(cert_file),
"-days", "1",
"-nodes",
"-subj", "/CN=localhost/O=Gunicorn Test/C=US",
"-addext", "subjectAltName=DNS:localhost,DNS:gunicorn-h2,IP:127.0.0.1"
],
check=True,
capture_output=True
)
# Set readable permissions
cert_file.chmod(0o644)
key_file.chmod(0o644)
def wait_for_service(url: str, timeout: int = 60) -> bool:
"""Wait for a service to become available."""
import ssl
import socket
from urllib.parse import urlparse
parsed = urlparse(url)
host = parsed.hostname or 'localhost'
port = parsed.port or 443
start_time = time.time()
while time.time() - start_time < timeout:
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
with socket.create_connection((host, port), timeout=5) as sock:
with ctx.wrap_socket(sock, server_hostname=host):
return True
except (socket.error, ssl.SSLError, OSError):
time.sleep(1)
return False
@pytest.fixture(scope="session")
def docker_compose_file():
"""Return the path to docker-compose.yml."""
return DOCKER_DIR / "docker-compose.yml"
@pytest.fixture(scope="session")
def certs_dir():
"""Generate and return the certs directory."""
generate_self_signed_cert(CERTS_DIR)
return CERTS_DIR
@pytest.fixture(scope="session")
def docker_services(docker_compose_file, certs_dir):
"""Start Docker services for the test session."""
compose_file = str(docker_compose_file)
# Check if Docker is available
try:
subprocess.run(
["docker", "info"],
check=True,
capture_output=True
)
except (subprocess.CalledProcessError, FileNotFoundError):
pytest.skip("Docker is not available")
# Check if docker compose is available
try:
subprocess.run(
["docker", "compose", "version"],
check=True,
capture_output=True
)
except subprocess.CalledProcessError:
pytest.skip("Docker Compose is not available")
# Build and start services
try:
subprocess.run(
["docker", "compose", "-f", compose_file, "build"],
check=True,
cwd=DOCKER_DIR
)
subprocess.run(
["docker", "compose", "-f", compose_file, "up", "-d"],
check=True,
cwd=DOCKER_DIR
)
# Wait for services to be healthy
gunicorn_ready = wait_for_service("https://127.0.0.1:8443", timeout=60)
nginx_ready = wait_for_service("https://127.0.0.1:8444", timeout=60)
if not gunicorn_ready:
# Get logs for debugging
result = subprocess.run(
["docker", "compose", "-f", compose_file, "logs", "gunicorn-h2"],
capture_output=True,
text=True,
cwd=DOCKER_DIR
)
pytest.fail(f"Gunicorn service failed to start. Logs:\n{result.stdout}\n{result.stderr}")
if not nginx_ready:
result = subprocess.run(
["docker", "compose", "-f", compose_file, "logs", "nginx-h2"],
capture_output=True,
text=True,
cwd=DOCKER_DIR
)
pytest.fail(f"Nginx service failed to start. Logs:\n{result.stdout}\n{result.stderr}")
yield {
"gunicorn": "https://127.0.0.1:8443",
"nginx": "https://127.0.0.1:8444"
}
finally:
# Stop and remove services
subprocess.run(
["docker", "compose", "-f", compose_file, "down", "-v", "--remove-orphans"],
cwd=DOCKER_DIR,
capture_output=True
)
@pytest.fixture
def gunicorn_url(docker_services):
"""Return the gunicorn service URL."""
return docker_services["gunicorn"]
@pytest.fixture
def nginx_url(docker_services):
"""Return the nginx proxy URL."""
return docker_services["nginx"]
@pytest.fixture
def h2_client():
"""Create an HTTP/2 capable client."""
httpx = pytest.importorskip("httpx")
client = httpx.Client(http2=True, verify=False, timeout=30.0)
yield client
client.close()
@pytest.fixture
def h1_client():
"""Create an HTTP/1.1 only client."""
httpx = pytest.importorskip("httpx")
client = httpx.Client(http2=False, verify=False, timeout=30.0)
yield client
client.close()
@pytest.fixture
def async_h2_client():
"""Create an async HTTP/2 capable client."""
httpx = pytest.importorskip("httpx")
async def create_client():
return httpx.AsyncClient(http2=True, verify=False, timeout=30.0)
return create_client