168 lines
5.0 KiB
Python
168 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FRONTEND_ROOT = ROOT / "frontend"
|
|
RUNTIME_ROOT = ROOT / "runtime"
|
|
E2E_URL = "http://127.0.0.1:8876/index.html"
|
|
|
|
|
|
def run(label: str, command: list[str]) -> None:
|
|
print(f"\n[{label}] {' '.join(command)}", flush=True)
|
|
subprocess.run(command, cwd=ROOT, check=True)
|
|
|
|
|
|
def python_test_command() -> list[str]:
|
|
return [sys.executable, "-m", "unittest", "discover", "-s", "tests"]
|
|
|
|
|
|
def verify_git_diff() -> None:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--show-toplevel"],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
print("\n[patch] skipped: standalone export is not a Git checkout")
|
|
return
|
|
repository_root = Path(result.stdout.strip()).resolve()
|
|
if ROOT != repository_root / "app":
|
|
print("\n[patch] skipped: standalone export is outside the canonical app path")
|
|
return
|
|
run("patch", ["git", "diff", "--check"])
|
|
|
|
|
|
def verify_database() -> None:
|
|
database = ROOT / "data" / "review.db"
|
|
if not database.exists():
|
|
print("\n[database] skipped: data/review.db does not exist")
|
|
return
|
|
with sqlite3.connect(f"file:{database.as_posix()}?mode=ro", uri=True) as connection:
|
|
result = connection.execute("PRAGMA integrity_check").fetchone()
|
|
if not result or result[0] != "ok":
|
|
raise RuntimeError(f"SQLite integrity check failed: {result}")
|
|
print(f"\n[database] integrity_check=ok size={database.stat().st_size}")
|
|
|
|
|
|
def url_reachable(url: str) -> bool:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=1) as response:
|
|
return response.status == 200
|
|
except (OSError, urllib.error.URLError):
|
|
return False
|
|
|
|
|
|
def start_e2e_server() -> subprocess.Popen[bytes] | None:
|
|
if url_reachable(E2E_URL):
|
|
print(f"\n[e2e-server] reusing {E2E_URL}")
|
|
return None
|
|
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
process = subprocess.Popen(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"http.server",
|
|
"8876",
|
|
"--bind",
|
|
"127.0.0.1",
|
|
"--directory",
|
|
"frontend",
|
|
],
|
|
cwd=ROOT,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
creationflags=creation_flags,
|
|
)
|
|
deadline = time.monotonic() + 15
|
|
while time.monotonic() < deadline:
|
|
if process.poll() is not None:
|
|
raise RuntimeError("Playwright static server exited before becoming ready")
|
|
if url_reachable(E2E_URL):
|
|
print(f"\n[e2e-server] started {E2E_URL}")
|
|
return process
|
|
time.sleep(0.1)
|
|
stop_e2e_server(process)
|
|
raise RuntimeError("Playwright static server did not become ready within 15 seconds")
|
|
|
|
|
|
def stop_e2e_server(process: subprocess.Popen[bytes] | None) -> None:
|
|
if process is None:
|
|
return
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=5)
|
|
print("\n[e2e-server] stopped")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Verify the standalone application"
|
|
)
|
|
parser.add_argument(
|
|
"--e2e",
|
|
action="store_true",
|
|
help="also run the Playwright browser suite",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
cache_root = RUNTIME_ROOT / "cache" / "python"
|
|
cache_root.mkdir(parents=True, exist_ok=True)
|
|
os.environ["PYTHONPYCACHEPREFIX"] = str(cache_root)
|
|
|
|
run("python", python_test_command())
|
|
run(
|
|
"api-registry",
|
|
[sys.executable, "tools/build_api_registry.py", "--check"],
|
|
)
|
|
run(
|
|
"architecture-inventory",
|
|
[sys.executable, "tools/build_architecture_inventory.py", "--check"],
|
|
)
|
|
node = shutil.which("node")
|
|
if not node:
|
|
raise RuntimeError("node is required for JavaScript syntax checks")
|
|
scripts = sorted(
|
|
path for path in FRONTEND_ROOT.rglob("*") if path.suffix in {".js", ".mjs"}
|
|
)
|
|
if not scripts:
|
|
raise RuntimeError(f"no JavaScript files found under {FRONTEND_ROOT}")
|
|
for script in scripts:
|
|
run(
|
|
"javascript",
|
|
[node, "--check", script.relative_to(ROOT).as_posix()],
|
|
)
|
|
verify_git_diff()
|
|
verify_database()
|
|
|
|
if args.e2e:
|
|
npx = shutil.which("npx.cmd" if sys.platform == "win32" else "npx")
|
|
if not npx:
|
|
raise RuntimeError("npx is required for the Playwright suite")
|
|
server = start_e2e_server()
|
|
try:
|
|
run("playwright", [npx, "playwright", "test", "--reporter=dot"])
|
|
finally:
|
|
stop_e2e_server(server)
|
|
print("\nBaseline verification passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|