From 28667600ac91f3dfe2da7481d13532e332e14b15 Mon Sep 17 00:00:00 2001 From: Claude Sonnet 4.6 Date: Mon, 15 Jun 2026 16:23:00 +0000 Subject: conftest: add readiness check before test session Poll each service base URL until it responds with a non-502/503 status before the test session begins. Retries on: - ConnectionError: process not yet bound to its port - 502/503: Nginx upstream-not-ready response (served while the upstream gunicorn process is still starting up behind the reverse proxy) Any other HTTP response is treated as "service is up". Polls every 2 seconds for up to 120 seconds, then raises RuntimeError with a clear message. This prevents spurious failures when smoke tests are queued immediately after a service restart. --- conftest.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 5b5eb87..c4a51dc 100644 --- a/conftest.py +++ b/conftest.py @@ -16,6 +16,7 @@ For auth-flow tests also set: """ import os +import time import pytest import requests @@ -25,19 +26,47 @@ _GN3_DEFAULT = "https://cd.genenetwork.org/api3" _GN_AUTH_DEFAULT = "https://auth-cd.genenetwork.org" +def _wait_for(url: str, timeout: int = 120, interval: int = 2) -> None: + """Poll url until the upstream service is up or timeout elapses. + + Retries on ConnectionError (process not yet bound) and on 502/503 + (Nginx upstream-not-ready responses). Any other HTTP response is + treated as "service is up". + """ + deadline = time.monotonic() + timeout + last_exc = None + while time.monotonic() < deadline: + try: + resp = requests.get(url, timeout=5, allow_redirects=True) + if resp.status_code not in (502, 503): + return + except requests.exceptions.ConnectionError as exc: + last_exc = exc + time.sleep(interval) + raise RuntimeError( + f"Service at {url} did not become available within {timeout}s" + ) from last_exc + + @pytest.fixture(scope="session") def gn2_url() -> str: - return os.environ.get("GN2_BASE_URL", _GN2_DEFAULT).rstrip("/") + url = os.environ.get("GN2_BASE_URL", _GN2_DEFAULT).rstrip("/") + _wait_for(url) + return url @pytest.fixture(scope="session") def gn3_url() -> str: - return os.environ.get("GN3_BASE_URL", _GN3_DEFAULT).rstrip("/") + url = os.environ.get("GN3_BASE_URL", _GN3_DEFAULT).rstrip("/") + _wait_for(url) + return url @pytest.fixture(scope="session") def gn_auth_url() -> str: - return os.environ.get("GN_AUTH_BASE_URL", _GN_AUTH_DEFAULT).rstrip("/") + url = os.environ.get("GN_AUTH_BASE_URL", _GN_AUTH_DEFAULT).rstrip("/") + _wait_for(url) + return url @pytest.fixture(scope="session") -- cgit 1.4.1