chore(jarca-ledger): finalize approved batch docs, dashboard, and CI smoke updates
This commit is contained in:
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CI wrapper for the Streamlit dashboard HTTP smoke gate.
|
||||
|
||||
Creates a synthetic fixture DB, starts Streamlit against it, 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 Streamlit dashboard 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["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"streamlit",
|
||||
"run",
|
||||
"dashboard/app.py",
|
||||
"--server.address",
|
||||
args.host,
|
||||
"--server.port",
|
||||
str(args.port),
|
||||
"--server.headless",
|
||||
"true",
|
||||
"--browser.gatherUsageStats",
|
||||
"false",
|
||||
]
|
||||
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())
|
||||
Reference in New Issue
Block a user