diff --git a/tests/gitea/custom/test_lfs_roundtrip.py b/tests/gitea/custom/test_lfs_roundtrip.py index f118066..fb9544d 100644 --- a/tests/gitea/custom/test_lfs_roundtrip.py +++ b/tests/gitea/custom/test_lfs_roundtrip.py @@ -23,6 +23,7 @@ import shutil import subprocess import sys import tempfile +import time import urllib.error import urllib.request @@ -45,7 +46,15 @@ def _lfs_available() -> bool: return os.path.exists(os.path.join(abra_dir, "recipes", "gitea", _LFS_OVERLAY)) -def _api(domain, path, method="GET", body=None, user="", password=""): +API_TIMEOUT = 20 + + +def _api(domain, path, method="GET", body=None, user="", password="", timeout=API_TIMEOUT): + """One API call → (status, JSON). Never raises: transient errors (connection refused / + dropped mid-restart, DNS, socket timeout) return (0, {}) so a caller's bounded poll can + simply retry, mirroring lifecycle.http_fetch. Only HTTPError was caught before; a socket + timeout on a slow post-restart boot (gitea 28.0.0 migration window, PR #10 run #31) escaped + as TimeoutError and aborted the restart poll itself.""" data = json.dumps(body).encode() if body is not None else None auth = base64.b64encode(f"{user}:{password}".encode()).decode() headers = {"Authorization": f"Basic {auth}"} @@ -55,7 +64,7 @@ def _api(domain, path, method="GET", body=None, user="", password=""): f"https://{domain}/api/v1{path}", data=data, headers=headers, method=method ) try: - with urllib.request.urlopen(req, timeout=20, context=_CTX) as r: + with urllib.request.urlopen(req, timeout=timeout, context=_CTX) as r: raw = r.read() return r.status, (json.loads(raw) if raw else {}) except urllib.error.HTTPError as e: @@ -64,6 +73,8 @@ def _api(domain, path, method="GET", body=None, user="", password=""): return e.code, json.loads(raw) except (ValueError, json.JSONDecodeError): return e.code, {} + except (urllib.error.URLError, TimeoutError, OSError): # TimeoutError ⊃ socket.timeout (UP041) + return 0, {} def _run_git(args, cwd, env=None): @@ -164,6 +175,8 @@ def test_lfs_roundtrip(live_app): finally: shutil.rmtree(fresh_dir, ignore_errors=True) + import recipe_meta # noqa: E402 — per-recipe declared timeouts (tests/STYLE.md §5) + # 7. JWT-secret stability: restart gitea + assert LFS_JWT_SECRET unchanged. # Read the current secret from inside the container's rendered app.ini. current_jwt = lifecycle.exec_in_app( @@ -182,14 +195,15 @@ def test_lfs_roundtrip(live_app): capture_output=True, timeout=120, ) - # Wait for gitea to come back up - - # Re-read meta from the live_app fixture (meta is not in scope here — use the stored meta) - import time - - deadline = time.time() + 120 + # Wait for gitea to come back up. Window + probe are DERIVED from the recipe's declared + # readiness (recipe_meta.HTTP_TIMEOUT / READY_PROBE — the /api/v1/version readiness that + # recipe_meta already declares for the harness, not a guess) — tests/STYLE.md §5. + wait_timeout = int(getattr(recipe_meta, "HTTP_TIMEOUT", 300)) + probe_path = "/version" # READY_PROBE's readiness path (rules carry only ok + domain) + deadline = time.time() + wait_timeout + status2 = 0 while time.time() < deadline: - status2, _ = _api(live_app, "/version", user=user, password=password) + status2, _ = _api(live_app, probe_path, user=user, password=password) if status2 == 200: break time.sleep(5)