#!/usr/bin/env python3 """Emit a JUnit report marking every discovered Chainsaw test as *skipped*. Used in Phase 0 (and any run where no cluster is reachable): the behavior tests are valid scaffolds but cannot execute without a cluster, so instead of failing the suite we emit a JUnit file whose testcases are `` with the reason. This keeps the merge/phase gate green while the coverage report still shows which requirements have a verifying test. As later phases deliver a dev/test cluster, run.sh hands execution to Chainsaw, which emits the real pass/fail JUnit and this shim is no longer used. Each testcase carries its verified R-* IDs in the `classname` + a property so the JUnit remains requirement-tagged (R-TEST-04) even in skipped form. """ from __future__ import annotations import argparse import re import sys import xml.etree.ElementTree as ET from pathlib import Path from xml.dom import minidom LABEL_PREFIX = "conformance.home.k3s" LABEL_LINE_RE = re.compile( r"^\s*" + re.escape(LABEL_PREFIX) + r"/(R-[A-Z0-9]+-[0-9]+)\s*:\s*[\"']?true", re.I ) def collect(path: Path) -> tuple[str, list[str], str]: text = path.read_text() name_m = re.search(r"^\s*name:\s*(\S+)", text, re.M) name = name_m.group(1) if name_m else path.parent.name reqs = sorted({m.group(1).upper() for m in (LABEL_LINE_RE.match(l) for l in text.splitlines()) if m}) rel = str(path) layer = ("L0" if "l0-static" in rel else "L1" if "l1-rendering" in rel else "L2" if "l2-behavioral" in rel else "L3" if "l3-fault-injection" in rel else "META") return name, reqs, layer def main() -> int: ap = argparse.ArgumentParser(description=__doc__) here = Path(__file__).resolve().parent.parent ap.add_argument("--tests", type=Path, default=here / "tests") ap.add_argument("--out", type=Path, default=here / "_artifacts" / "report.junit.xml") ap.add_argument("--reason", default="no cluster reachable (Phase 0 scaffold): " "behavior test not executed") args = ap.parse_args() files = sorted(args.tests.rglob("chainsaw-test.yaml")) suite = ET.Element("testsuite", { "name": "home-k3s-conformance (scaffold/skipped)", "tests": str(len(files)), "failures": "0", "errors": "0", "skipped": str(len(files)), }) for f in files: name, reqs, layer = collect(f) tc = ET.SubElement(suite, "testcase", { # classname carries layer + requirement tags so the JUnit stays traceable. "classname": f"{layer}:{','.join(reqs) if reqs else 'UNTAGGED'}", "name": name, }) props = ET.SubElement(tc, "properties") ET.SubElement(props, "property", {"name": "requirements", "value": ",".join(reqs)}) ET.SubElement(props, "property", {"name": "layer", "value": layer}) ET.SubElement(props, "property", {"name": "path", "value": str(f)}) ET.SubElement(tc, "skipped", {"message": args.reason}) root = ET.Element("testsuites", { "name": "home-k3s-conformance", "tests": str(len(files)), "skipped": str(len(files)), "failures": "0", "errors": "0", }) root.append(suite) args.out.parent.mkdir(parents=True, exist_ok=True) xml = minidom.parseString(ET.tostring(root, "utf-8")).toprettyxml(indent=" ") args.out.write_text(xml) print(f"[junit] wrote skipped JUnit for {len(files)} scaffolded test(s) -> {args.out}", file=sys.stderr) return 0 if __name__ == "__main__": raise SystemExit(main())