Files
jarca-ledger-mcp/scripts/ci_dashboard_http_smoke.py
T

128 lines
5.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""CI wrapper for the production Dashboard ASGI HTTP smoke gate.
Creates a synthetic fixture DB, starts the authenticated Streamlit ASGI wrapper,
waits for HTTP 200, and always attempts process cleanup. Output is intentionally
status-only.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import subprocess
import sys
import tempfile
import time
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
from scripts.ci_fixture_db import create_ci_fixture_db
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run the Dashboard ASGI HTTP smoke against a synthetic CI fixture DB.")
parser.add_argument("--host", default="127.0.0.1", help="Loopback host to bind")
parser.add_argument("--port", type=int, default=18501, help="Deterministic smoke port")
parser.add_argument("--timeout", type=int, default=45, help="Readiness timeout in seconds")
parser.add_argument("--page", default="요약", help="Dashboard page query param to request")
return parser.parse_args()
def wait_for_http(url: str, timeout_seconds: int) -> dict[str, Any]:
deadline = time.monotonic() + timeout_seconds
attempts = 0
last_error = "not_started"
while time.monotonic() < deadline:
attempts += 1
try:
with urllib.request.urlopen(url, timeout=2) as response:
if response.status == 200:
return {"ready": True, "status": response.status, "attempts": attempts}
last_error = f"HTTP {response.status}"
except Exception as exc: # noqa: BLE001 - compact readiness diagnostic
last_error = exc.__class__.__name__
time.sleep(1)
return {"ready": False, "attempts": attempts, "last_error": last_error}
def terminate_process(proc: subprocess.Popen[str], timeout: int = 10) -> dict[str, Any]:
if proc.poll() is not None:
return {"cleanup": "already_exited", "returncode": proc.returncode}
try:
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=timeout)
return {"cleanup": "terminated", "returncode": proc.returncode}
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=timeout)
return {"cleanup": "killed", "returncode": proc.returncode}
def main() -> int:
args = parse_args()
smoke_url = f"http://{args.host}:{args.port}/?page={urllib.parse.quote(args.page)}"
proc: subprocess.Popen[str] | None = None
with tempfile.TemporaryDirectory(prefix="jarca-ledger-ci-http-") as tmp:
tmp_path = Path(tmp)
fixture_path = tmp_path / "fixture.sqlite3"
try:
fixture = create_ci_fixture_db(fixture_path)
env = os.environ.copy()
env["JARCA_LEDGER_DB_PATH"] = str(fixture_path)
env["JARCA_DASHBOARD_ADMIN_ID"] = "a"
env["JARCA_DASHBOARD_ADMIN_PASSWORD"] = "p"
env["JARCA_DASHBOARD_SIGNING_SECRET"] = "s" * 32
env["FORWARDED_ALLOW_IPS"] = "127.0.0.1"
env["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
cmd = [
sys.executable,
"-m",
"uvicorn",
"dashboard.server:app",
"--host",
args.host,
"--port",
str(args.port),
"--proxy-headers",
]
proc = subprocess.Popen(
cmd,
cwd=Path(__file__).resolve().parents[1],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
readiness = wait_for_http(smoke_url, args.timeout)
if not readiness["ready"]:
cleanup = terminate_process(proc)
print(json.dumps({"success": False, "gate": "dashboard-http-smoke", "fixture": "synthetic", "readiness": readiness, "cleanup": cleanup}, ensure_ascii=False))
return 10
cleanup = terminate_process(proc)
success = cleanup.get("cleanup") in {"terminated", "already_exited", "killed"}
print(json.dumps({
"success": success,
"gate": "dashboard-http-smoke",
"fixture": "synthetic",
"fixture_integrity": fixture.get("integrity_check"),
"http_status": readiness.get("status"),
"attempts": readiness.get("attempts"),
"cleanup": cleanup.get("cleanup"),
}, ensure_ascii=False, indent=2))
return 0 if success else 11
except Exception as exc: # noqa: BLE001 - deterministic wrapper failure
cleanup = terminate_process(proc) if proc is not None else {"cleanup": "not_started"}
print(json.dumps({"success": False, "gate": "dashboard-http-smoke", "error_code": "wrapper_error", "error": exc.__class__.__name__, "cleanup": cleanup.get("cleanup")}, ensure_ascii=False))
return 1
if __name__ == "__main__":
raise SystemExit(main())