from __future__ import annotations import argparse import os from pathlib import Path import shutil import subprocess import sys import time import urllib.request ROOT = Path(__file__).resolve().parents[1] FRONTEND = ROOT / "frontend" VERIFICATION = ROOT / "verification" PORT = 9999 PARALLEL = os.environ.get("ADMINIVE_VERIFY_JOBS", "1") BASE_URL = f"http://127.0.0.1:{PORT}" SANITIZER_TARGETS = ["Adminive_Core_Adapter_Test", "Adminive_Advanced_Adapter_Test", "Adminive_Safety_Test", "Adminive_Managed_Test", "Adminive_View_Schema_Test", "Adminive_Drogon_Adapter_Test", "Adminive_Httplib_Adapter_Test"] def run(command: list[str], cwd: Path = ROOT, env: dict[str, str] | None = None) -> None: print("+", " ".join(command), flush=True) subprocess.run(command, cwd=cwd, env=env, check=True) def configure(build_dir: Path, configuration: str, extra: list[str] | None = None) -> None: command = ["cmake", "-S", str(ROOT), "-B", str(build_dir), f"-DCMAKE_BUILD_TYPE={configuration}", "-DBUILD_TESTING=ON"] if extra: command.extend(extra) run(command) def build(build_dir: Path, configuration: str, target: str | None = None) -> None: command = ["cmake", "--build", str(build_dir), "--config", configuration, "--parallel", PARALLEL] if target: command.extend(["--target", target]) run(command) def ctest(build_dir: Path, configuration: str, label: str | None = None) -> None: command = ["ctest", "--test-dir", str(build_dir), "-C", configuration, "--output-on-failure"] if label: command.extend(["-L", label]) run(command) def ctest_regex(build_dir: Path, configuration: str, names: list[str]) -> None: command = ["ctest", "--test-dir", str(build_dir), "-C", configuration, "--output-on-failure", "-R", "^(" + "|".join(names) + ")$"] run(command) def server_executable(build_dir: Path, configuration: str) -> Path: name = "Adminive_Server.exe" if os.name == "nt" else "Adminive_Server" candidates = [build_dir / "backend" / "service" / configuration / name, build_dir / "backend" / "service" / name] for candidate in candidates: if candidate.is_file(): return candidate raise RuntimeError(f"Adminive_Server not found under {build_dir}") def wait_for_server(process: subprocess.Popen[bytes]) -> None: deadline = time.monotonic() + 20 while time.monotonic() < deadline: if process.poll() is not None: raise RuntimeError(f"Adminive_Server exited with {process.returncode}") try: with urllib.request.urlopen(f"{BASE_URL}/admin/gallery/docs", timeout=1) as response: if response.status == 200: return except Exception: time.sleep(0.2) raise RuntimeError("Adminive_Server did not become ready") def npm_command() -> str: return "npm.cmd" if os.name == "nt" else "npm" def prepare_frontend() -> None: npm = npm_command() run([npm, "ci"], FRONTEND) run([npm, "run", "test:unit"], FRONTEND) run([npm, "run", "build"], FRONTEND) run([npm, "exec", "--", "playwright", "install", "chromium"], FRONTEND) def run_e2e(build_dir: Path, configuration: str, edge: bool) -> None: npm = npm_command() build(build_dir, configuration, "Adminive_Server") if not (ROOT / "frontend_dist" / "index.html").is_file(): run([npm, "run", "build"], FRONTEND) executable = server_executable(build_dir, configuration) process = subprocess.Popen([str(executable), str(PORT)], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) try: wait_for_server(process) env = os.environ.copy() env["ADMINIVE_E2E_BASE_URL"] = BASE_URL script = "test:e2e:edge" if edge else "test:e2e" run([npm, "run", script], FRONTEND, env) finally: if process.poll() is None: process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=5) if process.stdout: output = process.stdout.read().decode(errors="replace") if output: print(output, end="") def full_verification() -> None: if VERIFICATION.exists(): shutil.rmtree(VERIFICATION) debug_dir = VERIFICATION / "debug" release_dir = VERIFICATION / "release" asan_dir = VERIFICATION / "asan-ubsan" tsan_dir = VERIFICATION / "tsan" configure(debug_dir, "Debug") build(debug_dir, "Debug") ctest(debug_dir, "Debug") configure(release_dir, "Release") build(release_dir, "Release") ctest(release_dir, "Release") if os.name == "nt": configure(asan_dir, "Debug", ["-DADMINIVE_ENABLE_ASAN=ON"]) for target in SANITIZER_TARGETS: build(asan_dir, "Debug", target) ctest_regex(asan_dir, "Debug", SANITIZER_TARGETS) print("SKIP: UBSan and TSan verification require Clang or GNU and are not reported as passed on Windows MSVC", flush=True) else: configure(asan_dir, "Debug", ["-DADMINIVE_ENABLE_ASAN=ON", "-DADMINIVE_ENABLE_UBSAN=ON"]) for target in SANITIZER_TARGETS: build(asan_dir, "Debug", target) ctest_regex(asan_dir, "Debug", SANITIZER_TARGETS) configure(tsan_dir, "Debug", ["-DADMINIVE_ENABLE_TSAN=ON"]) build(tsan_dir, "Debug", "Adminive_Managed_Test") ctest(tsan_dir, "Debug", "concurrency") prepare_frontend() run_e2e(release_dir, "Release", False) if os.name == "nt": run_e2e(release_dir, "Release", True) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--e2e-only", action="store_true") parser.add_argument("--edge", action="store_true") parser.add_argument("--binary-dir", type=Path) parser.add_argument("--configuration", default="Release") args = parser.parse_args() if args.edge and os.name != "nt": raise RuntimeError("Edge E2E requires Windows with Microsoft Edge installed") if args.e2e_only: if args.binary_dir is None: raise RuntimeError("--binary-dir is required with --e2e-only") run_e2e(args.binary_dir.resolve(), args.configuration or "Release", args.edge) return 0 full_verification() return 0 if __name__ == "__main__": sys.exit(main())