feat(tools): doorstop requirements CI integration for UniversalisOS
This commit is contained in:
parent
5b69d6616e
commit
ded32c2201
1 changed files with 280 additions and 0 deletions
280
tools/doorstop-integration/universalisos_ci.py
Executable file
280
tools/doorstop-integration/universalisos_ci.py
Executable file
|
|
@ -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())
|
||||
Loading…
Reference in a new issue