universalisos/tools/doorstop-integration/universalisos_ci.py

280 lines
10 KiB
Python
Executable file

#!/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())