#!/usr/bin/env python3 """Merge several Chainsaw JUnit reports into one. run.sh splits the L2/L3 run into a LIGHT pass (concurrent) and a HEAVY pass (serial, parallel=1) so the storage/VM-heavy tests never spike embedded-etcd while other tests are mid-setup (see run.sh `run_chainsaw`). Each pass writes its own JUnit file; this stitches them back into the single stable artifact CI collects (`_artifacts/report.junit.xml`) so the split is invisible downstream. Chainsaw emits `` with one `` per test. Merging is a concatenation of the child `` elements plus a recomputed roll-up of tests/failures/errors/time on the root, so the combined report totals exactly the sum of the passes (no double counting, no dropped cases). Inputs that don't exist are skipped (a pass may legitimately select zero tests); at least one must exist. """ from __future__ import annotations import argparse import sys import xml.etree.ElementTree as ET from pathlib import Path def _int(el: ET.Element, attr: str) -> int: try: return int(el.get(attr, "0") or "0") except ValueError: return 0 def _float(el: ET.Element, attr: str) -> float: try: return float(el.get(attr, "0") or "0") except ValueError: return 0.0 def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--out", type=Path, required=True, help="merged JUnit path") ap.add_argument("--name", default="chainsaw-report", help="root testsuites name") ap.add_argument("inputs", nargs="+", type=Path, help="JUnit files to merge (missing ones skipped)") args = ap.parse_args() merged = ET.Element("testsuites", {"name": args.name}) tests = failures = errors = skipped = 0 total_time = 0.0 found = 0 for path in args.inputs: if not path.exists() or path.stat().st_size == 0: continue found += 1 root = ET.parse(path).getroot() # Accept either a wrapper or a bare . suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite")) for suite in suites: merged.append(suite) tests += _int(suite, "tests") failures += _int(suite, "failures") errors += _int(suite, "errors") skipped += _int(suite, "skipped") total_time += _float(suite, "time") if found == 0: print(f"merge_junit: none of the input reports exist: {args.inputs}", file=sys.stderr) return 1 merged.set("tests", str(tests)) merged.set("failures", str(failures)) merged.set("errors", str(errors)) if skipped: merged.set("skipped", str(skipped)) merged.set("time", f"{total_time:.6f}") args.out.parent.mkdir(parents=True, exist_ok=True) ET.ElementTree(merged).write(args.out, encoding="unicode", xml_declaration=False) print(f"merge_junit: merged {found} report(s) -> {args.out} " f"(tests={tests} failures={failures} errors={errors} skipped={skipped})") return 0 if __name__ == "__main__": raise SystemExit(main())