| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- import argparse
- import json
- import re
- from collections import Counter
- from pathlib import Path
- def _base_test_name(nodeid: str) -> str:
- # Strip parameters like [param=..] from the last component
- name = nodeid.split("::")[-1]
- return re.sub(r"\[.*\]$", "", name)
- def _class_name(nodeid: str) -> str | None:
- parts = nodeid.split("::")
- # nodeid can be: file::Class::test or file::test
- if len(parts) >= 3:
- return parts[-2]
- return None
- def _file_path(nodeid: str) -> str:
- return nodeid.split("::")[0]
- def _modeling_key(file_path: str) -> str | None:
- # Extract "xxx" from test_modeling_xxx.py
- m = re.search(r"test_modeling_([A-Za-z0-9_]+)\.py$", file_path)
- if m:
- return m.group(1)
- return None
- def summarize(report_path: str):
- p = Path(report_path)
- if not p.exists():
- raise FileNotFoundError(f"Report file not found: {p.resolve()}")
- data = json.loads(p.read_text())
- tests = data.get("tests", [])
- # Overall counts
- outcomes = Counter(t.get("outcome", "unknown") for t in tests)
- # Filter failures (pytest-json-report uses "failed" and may have "error")
- failed = [t for t in tests if t.get("outcome") in ("failed", "error")]
- # 1) Failures per test file
- failures_per_file = Counter(_file_path(t.get("nodeid", "")) for t in failed)
- # 2) Failures per class (if any; otherwise "NO_CLASS")
- failures_per_class = Counter((_class_name(t.get("nodeid", "")) or "NO_CLASS") for t in failed)
- # 3) Failures per base test name (function), aggregating parametrized cases
- failures_per_testname = Counter(_base_test_name(t.get("nodeid", "")) for t in failed)
- # 4) Failures per test_modeling_xxx (derived from filename)
- failures_per_modeling_key = Counter()
- for t in failed:
- key = _modeling_key(_file_path(t.get("nodeid", "")))
- if key:
- failures_per_modeling_key[key] += 1
- return {
- "outcomes": outcomes,
- "failures_per_file": failures_per_file,
- "failures_per_class": failures_per_class,
- "failures_per_testname": failures_per_testname,
- "failures_per_modeling_key": failures_per_modeling_key,
- }
- def main():
- parser = argparse.ArgumentParser(description="Summarize pytest JSON report failures")
- parser.add_argument(
- "--report", default="report.json", help="Path to pytest JSON report file (default: report.json)"
- )
- args = parser.parse_args()
- try:
- summary = summarize(args.report)
- except FileNotFoundError as e:
- print(str(e))
- return
- outcomes = summary["outcomes"]
- print("=== Overall ===")
- total = sum(outcomes.values())
- print(f"Total tests: {total}")
- for k in sorted(outcomes):
- print(f"{k:>10}: {outcomes[k]}")
- def _print_counter(title, counter: Counter, label=""):
- print(f"\n=== {title} ===")
- if not counter:
- print("None")
- return
- for key, cnt in sorted(counter.items(), key=lambda x: (x[1], x[0])):
- if label:
- print(f"{cnt:4d} {label}{key}")
- else:
- print(f"{cnt:4d} {key}")
- _print_counter("Failures per test class", summary["failures_per_class"], label="class ")
- _print_counter("Failures per test_modeling_xxx", summary["failures_per_modeling_key"], label="model ")
- _print_counter("Failures per test file", summary["failures_per_file"])
- _print_counter("Failures per test name (base)", summary["failures_per_testname"])
- if __name__ == "__main__":
- main()
|