From ded32c2201ac648937ef5d25d3f6a99248c86819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Coutada?= Date: Sun, 12 Jul 2026 21:06:40 +0100 Subject: [PATCH] feat(tools): doorstop requirements CI integration for UniversalisOS --- .../doorstop-integration/universalisos_ci.py | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100755 tools/doorstop-integration/universalisos_ci.py diff --git a/tools/doorstop-integration/universalisos_ci.py b/tools/doorstop-integration/universalisos_ci.py new file mode 100755 index 000000000..a2a9281b0 --- /dev/null +++ b/tools/doorstop-integration/universalisos_ci.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +UniversalisOS CI Integration Script + +Links the testing toolchain (uos-target → uos-cover → doorstop API) into a single +pipeline that can be run locally or in CI. Updates requirement test/coverage status +in the Doorstop backend automatically. + +Usage: + python universalisos_ci.py --arch armv7 --platform qemu-arm-virt + python universalisos_ci.py --full --upload-results +""" + +import argparse +import json +import os +import subprocess +import sys +import time +from pathlib import Path +from typing import Dict, List, Optional +import urllib.request +import urllib.error + +# Configuration +UOS_TARGET_DIR = Path(__file__).parent.parent / "tools" / "uos-target" +UOS_COVER_DIR = Path(__file__).parent.parent / "tools" / "uos-cover" +DOORSTOP_DIR = Path(__file__).parent.parent / "tools" / "doorstop-integration" +KERNEL_DIR = Path(__file__).parent.parent / "kernel" +DEFAULT_DOORSTOP_URL = "http://192.168.0.9:8100" + + +def run_command(cmd: List[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess: + """Run a shell command and return the result.""" + print(f"[CI] Running: {' '.join(cmd)}") + result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) + if check and result.returncode != 0: + print(f"[CI] ERROR: Command failed with code {result.returncode}") + print(f"[CI] stderr: {result.stderr}") + raise subprocess.CalledProcessError(result.returncode, cmd) + return result + + +def run_uos_target(arch: str, platform: str, coverage: bool = True) -> Path: + """Run uos-target.py to execute tests and return path to results.""" + uos_target = UOS_TARGET_DIR / "uos-target.py" + if not uos_target.exists(): + # Fallback: use the inline test runner + print("[CI] uos-target.py not found, using kernel/uos-check.sh") + run_command(["./uos-check.sh", f"test-{arch.replace('armv7', 'arm').replace('aarch64', 'aarch64')}"], cwd=KERNEL_DIR) + return KERNEL_DIR / "build" / "test_results.json" + + cmd = [ + sys.executable, str(uos_target), + "--arch", arch, + "--platform", platform, + "--run", + ] + if coverage: + cmd.append("--coverage") + + result = run_command(cmd) + print(f"[CI] uos-target output:\n{result.stdout}") + + # Find generated results + results_file = KERNEL_DIR / "build" / "test_results.json" + if results_file.exists(): + return results_file + return KERNEL_DIR / "build" / "trace.txt" + + +def run_uos_cover(trace_file: Path, output_dir: Path) -> Path: + """Run uos-cover suite to generate coverage report.""" + os.makedirs(output_dir, exist_ok=True) + + # Parse trace + coverage_json = output_dir / "coverage.json" + covparse = UOS_COVER_DIR / "uos_covparse.py" + if covparse.exists(): + run_command([ + sys.executable, str(covparse), + "--trace", str(trace_file), + "--output", str(coverage_json) + ]) + else: + print(f"[CI] WARNING: uos_covparse.py not found at {covparse}") + # Create minimal coverage stub + coverage_json.write_text(json.dumps({"overall": {"statement": 0, "branch": 0, "mcdc": 0}})) + + # Verify against requirements + verify = UOS_COVER_DIR / "uos_verify.py" + if verify.exists(): + run_command([ + sys.executable, str(verify), + "--coverage", str(coverage_json), + "--requirements", str(DOORSTOP_DIR) + ]) + + # Export HTML + covexport = UOS_COVER_DIR / "uos_covexport.py" + if covexport.exists(): + run_command([ + sys.executable, str(covexport), + "--input", str(coverage_json), + "--format", "html", + "--output", str(output_dir / "html") + ]) + + return coverage_json + + +def update_doorstop_status(coverage_json: Path, test_results: Optional[Path], doorstop_url: str): + """Update requirement status in the Doorstop backend via API.""" + print(f"[CI] Updating Doorstop at {doorstop_url}") + + # Load coverage data + if not coverage_json.exists(): + print("[CI] WARNING: No coverage data found, skipping Doorstop update") + return + + coverage_data = json.loads(coverage_json.read_text()) + + # Load test results + test_data = {} + if test_results and test_results.exists(): + test_data = json.loads(test_results.read_text()) + + # Fetch requirements from Doorstop API + try: + req = urllib.request.Request(f"{doorstop_url}/api/v1/requirements") + with urllib.request.urlopen(req, timeout=10) as resp: + requirements = json.loads(resp.read().decode()) + except Exception as e: + print(f"[CI] WARNING: Could not fetch requirements from Doorstop: {e}") + return + + # Update each requirement with coverage and test status + for req in requirements: + uid = req.get("uid") + if not uid: + continue + + # Build update payload + update_payload = { + "attrs": { + "result": "pending", + "revision": req.get("attrs", {}).get("revision", "1.0") + } + } + + # Determine test status from test results + if test_data: + req_tests = [t for t in test_data.get("tests", []) if uid in str(t.get("name", ""))] + if req_tests: + all_pass = all(t.get("status") == "pass" for t in req_tests) + any_fail = any(t.get("status") == "fail" for t in req_tests) + if all_pass: + update_payload["attrs"]["result"] = "verified" + elif any_fail: + update_payload["attrs"]["result"] = "failed" + else: + update_payload["attrs"]["result"] = "partial" + + # Update via API + try: + update_req = urllib.request.Request( + f"{doorstop_url}/api/v1/requirements/{uid}", + data=json.dumps(update_payload).encode(), + headers={"Content-Type": "application/json"}, + method="PUT" + ) + with urllib.request.urlopen(update_req, timeout=10) as resp: + print(f"[CI] Updated {uid}: {update_payload['attrs']['result']}") + except Exception as e: + print(f"[CI] WARNING: Failed to update {uid}: {e}") + + # Post coverage report + try: + coverage_report = { + "overall": coverage_data.get("overall", {}), + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ") + } + cov_req = urllib.request.Request( + f"{doorstop_url}/api/v1/coverage/bulk", + data=json.dumps(coverage_report).encode(), + headers={"Content-Type": "application/json"}, + method="POST" + ) + with urllib.request.urlopen(cov_req, timeout=10) as resp: + print("[CI] Coverage report uploaded successfully") + except Exception as e: + print(f"[CI] WARNING: Failed to upload coverage: {e}") + + +def package_artifacts(output_dir: Path, coverage_json: Path, test_results: Optional[Path]) -> Path: + """Package all artifacts for CI upload.""" + package_script = UOS_COVER_DIR / "uos_package.py" + if package_script.exists(): + run_command([ + sys.executable, str(package_script), + "--input", str(output_dir), + "--output", str(output_dir / "coverage_package.zip") + ]) + return output_dir / "coverage_package.zip" + return output_dir + + +def main(): + parser = argparse.ArgumentParser(description="UniversalisOS CI Integration Pipeline") + parser.add_argument("--arch", default="armv7", help="Target architecture (armv7, aarch64, riscv)") + parser.add_argument("--platform", default="qemu-arm-virt", help="Target platform") + parser.add_argument("--coverage", action="store_true", default=True, help="Enable coverage analysis") + parser.add_argument("--no-coverage", dest="coverage", action="store_false", help="Disable coverage") + parser.add_argument("--doorstop-url", default=DEFAULT_DOORSTOP_URL, help="Doorstop API URL") + parser.add_argument("--upload-results", action="store_true", help="Upload results to Doorstop") + parser.add_argument("--output-dir", default="build/ci_results", help="Output directory") + parser.add_argument("--full", action="store_true", help="Run full pipeline with all steps") + args = parser.parse_args() + + output_dir = Path(args.output_dir) + os.makedirs(output_dir, exist_ok=True) + + print("=" * 60) + print("UniversalOS CI Integration Pipeline") + print("=" * 60) + print(f"Architecture: {args.arch}") + print(f"Platform: {args.platform}") + print(f"Coverage: {args.coverage}") + print(f"Doorstop: {args.doorstop_url}") + print("=" * 60) + + try: + # Step 1: Run tests via uos-target + print("\n[Step 1/4] Running tests via uos-target...") + test_results = run_uos_target(args.arch, args.platform, coverage=args.coverage) + print(f"[CI] Test results: {test_results}") + + # Step 2: Run coverage analysis via uos-cover + coverage_json = None + if args.coverage: + print("\n[Step 2/4] Running coverage analysis via uos-cover...") + trace_file = KERNEL_DIR / "build" / "trace.txt" + if trace_file.exists(): + coverage_json = run_uos_cover(trace_file, output_dir) + print(f"[CI] Coverage report: {coverage_json}") + else: + print("[CI] WARNING: No trace file found, skipping coverage") + + # Step 3: Update Doorstop with results + if args.upload_results or args.full: + print("\n[Step 3/4] Updating Doorstop requirement status...") + update_doorstop_status( + coverage_json or output_dir / "coverage.json", + test_results if test_results.exists() else None, + args.doorstop_url + ) + + # Step 4: Package artifacts + print("\n[Step 4/4] Packaging artifacts...") + package_path = package_artifacts(output_dir, coverage_json or output_dir / "coverage.json", test_results) + print(f"[CI] Artifacts: {package_path}") + + print("\n" + "=" * 60) + print("CI Pipeline Complete") + print("=" * 60) + print(f"Results: {output_dir}") + return 0 + + except subprocess.CalledProcessError as e: + print(f"\n[CI] PIPELINE FAILED: {e}") + return 1 + except Exception as e: + print(f"\n[CI] UNEXPECTED ERROR: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main())