mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-13 03:58:33 +00:00
121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
"""Require a real passing execution of every pytest case across the CI jobs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
RECEIPTS = ("pytest-locked.xml", "pytest-ubuntu.xml", "pytest-windows.xml")
|
|
|
|
|
|
def reconcile(reports: list[Path], expected_files: list[str]) -> dict:
|
|
if not reports:
|
|
raise ValueError("No pytest execution reports supplied")
|
|
tests: dict[tuple[str, str], set[str]] = {}
|
|
durations: list[float] = []
|
|
for report in reports:
|
|
root = ET.parse(report).getroot()
|
|
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
|
|
if root.tag not in {"testsuite", "testsuites"} or not suites:
|
|
raise ValueError(f"Invalid pytest report: {report}")
|
|
seen: set[tuple[str, str]] = set()
|
|
duration = 0.0
|
|
for suite in suites:
|
|
cases = suite.findall("testcase")
|
|
if not cases:
|
|
raise ValueError(f"Empty pytest suite in {report}")
|
|
counts = {"tests": len(cases), "failures": 0, "errors": 0, "skipped": 0}
|
|
for case in cases:
|
|
key = (case.get("classname", ""), case.get("name", ""))
|
|
if not all(key) or key in seen:
|
|
raise ValueError(f"Missing or ambiguous pytest identity in {report}: {key}")
|
|
seen.add(key)
|
|
for status in ("failures", "errors", "skipped"):
|
|
tag = {"failures": "failure", "errors": "error", "skipped": "skipped"}[status]
|
|
counts[status] += len(case.findall(tag))
|
|
status = (
|
|
"failed"
|
|
if case.find("failure") is not None or case.find("error") is not None
|
|
else "pending"
|
|
if case.find("skipped") is not None
|
|
else "passed"
|
|
)
|
|
tests.setdefault(key, set()).add(status)
|
|
for name, count in counts.items():
|
|
if int(suite.get(name, "-1")) != count:
|
|
raise ValueError(f"Inconsistent pytest {name} count in {report}")
|
|
duration += float(suite.get("time", "0"))
|
|
durations.append(duration)
|
|
|
|
modules = {module for module, _ in tests}
|
|
for file in expected_files:
|
|
module = file.removesuffix(".py").replace("/", ".")
|
|
if not any(name == module or name.startswith(module + ".") for name in modules):
|
|
raise ValueError(f"Pytest file was never collected: {file}")
|
|
|
|
suites_by_module: dict[str, dict] = {}
|
|
failed: list[str] = []
|
|
pending = 0
|
|
for (module, name), statuses in sorted(tests.items()):
|
|
status = "failed" if "failed" in statuses else "passed" if "passed" in statuses else "pending"
|
|
full_name = f"{module}::{name}"
|
|
if status == "failed":
|
|
failed.append(full_name)
|
|
pending += status == "pending"
|
|
suite = suites_by_module.setdefault(
|
|
module,
|
|
{
|
|
"name": module,
|
|
"status": "passed",
|
|
"assertionResults": [],
|
|
"endTime": round(max(durations) * 1000),
|
|
},
|
|
)
|
|
suite["assertionResults"].append(
|
|
{
|
|
"fullName": full_name,
|
|
"ancestorTitles": [module],
|
|
"title": name,
|
|
"status": status,
|
|
}
|
|
)
|
|
if status == "failed":
|
|
suite["status"] = "failed"
|
|
return {
|
|
"success": not failed and pending == 0,
|
|
"numTotalTests": len(tests),
|
|
"numPassedTests": len(tests) - len(failed) - pending,
|
|
"numFailedTests": len(failed),
|
|
"numPendingTests": pending,
|
|
"numTotalTestSuites": len(suites_by_module),
|
|
"numFailedTestSuites": sum(s["status"] == "failed" for s in suites_by_module.values()),
|
|
"executionFailures": failed,
|
|
"startTime": 0,
|
|
"testResults": list(suites_by_module.values()),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) != 3:
|
|
raise ValueError("Usage: check_test_execution.py <reports-dir> <output.json>")
|
|
directory, output = map(Path, sys.argv[1:])
|
|
root = Path(__file__).resolve().parent
|
|
expected = [p.relative_to(root).as_posix() for p in (root / "tests").rglob("test_*.py")]
|
|
report = reconcile([directory / name for name in RECEIPTS], expected)
|
|
output.write_text(json.dumps(report), encoding="utf-8")
|
|
print(
|
|
f"Eval execution: {report['numPassedTests']}/{report['numTotalTests']} passed; "
|
|
f"{report['numPendingTests']} unverified; {report['numFailedTests']} failures"
|
|
)
|
|
for suite in report["testResults"]:
|
|
for case in suite["assertionResults"]:
|
|
if case["status"] != "passed":
|
|
print(f"{case['status']}: {case['fullName']}", file=sys.stderr)
|
|
return 0 if report["success"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|