280 lines
10 KiB
Python
Executable file
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())
|