pytest_helpers.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import argparse
  2. import json
  3. import re
  4. from collections import Counter
  5. from pathlib import Path
  6. def _base_test_name(nodeid: str) -> str:
  7. # Strip parameters like [param=..] from the last component
  8. name = nodeid.split("::")[-1]
  9. return re.sub(r"\[.*\]$", "", name)
  10. def _class_name(nodeid: str) -> str | None:
  11. parts = nodeid.split("::")
  12. # nodeid can be: file::Class::test or file::test
  13. if len(parts) >= 3:
  14. return parts[-2]
  15. return None
  16. def _file_path(nodeid: str) -> str:
  17. return nodeid.split("::")[0]
  18. def _modeling_key(file_path: str) -> str | None:
  19. # Extract "xxx" from test_modeling_xxx.py
  20. m = re.search(r"test_modeling_([A-Za-z0-9_]+)\.py$", file_path)
  21. if m:
  22. return m.group(1)
  23. return None
  24. def summarize(report_path: str):
  25. p = Path(report_path)
  26. if not p.exists():
  27. raise FileNotFoundError(f"Report file not found: {p.resolve()}")
  28. data = json.loads(p.read_text())
  29. tests = data.get("tests", [])
  30. # Overall counts
  31. outcomes = Counter(t.get("outcome", "unknown") for t in tests)
  32. # Filter failures (pytest-json-report uses "failed" and may have "error")
  33. failed = [t for t in tests if t.get("outcome") in ("failed", "error")]
  34. # 1) Failures per test file
  35. failures_per_file = Counter(_file_path(t.get("nodeid", "")) for t in failed)
  36. # 2) Failures per class (if any; otherwise "NO_CLASS")
  37. failures_per_class = Counter((_class_name(t.get("nodeid", "")) or "NO_CLASS") for t in failed)
  38. # 3) Failures per base test name (function), aggregating parametrized cases
  39. failures_per_testname = Counter(_base_test_name(t.get("nodeid", "")) for t in failed)
  40. # 4) Failures per test_modeling_xxx (derived from filename)
  41. failures_per_modeling_key = Counter()
  42. for t in failed:
  43. key = _modeling_key(_file_path(t.get("nodeid", "")))
  44. if key:
  45. failures_per_modeling_key[key] += 1
  46. return {
  47. "outcomes": outcomes,
  48. "failures_per_file": failures_per_file,
  49. "failures_per_class": failures_per_class,
  50. "failures_per_testname": failures_per_testname,
  51. "failures_per_modeling_key": failures_per_modeling_key,
  52. }
  53. def main():
  54. parser = argparse.ArgumentParser(description="Summarize pytest JSON report failures")
  55. parser.add_argument(
  56. "--report", default="report.json", help="Path to pytest JSON report file (default: report.json)"
  57. )
  58. args = parser.parse_args()
  59. try:
  60. summary = summarize(args.report)
  61. except FileNotFoundError as e:
  62. print(str(e))
  63. return
  64. outcomes = summary["outcomes"]
  65. print("=== Overall ===")
  66. total = sum(outcomes.values())
  67. print(f"Total tests: {total}")
  68. for k in sorted(outcomes):
  69. print(f"{k:>10}: {outcomes[k]}")
  70. def _print_counter(title, counter: Counter, label=""):
  71. print(f"\n=== {title} ===")
  72. if not counter:
  73. print("None")
  74. return
  75. for key, cnt in sorted(counter.items(), key=lambda x: (x[1], x[0])):
  76. if label:
  77. print(f"{cnt:4d} {label}{key}")
  78. else:
  79. print(f"{cnt:4d} {key}")
  80. _print_counter("Failures per test class", summary["failures_per_class"], label="class ")
  81. _print_counter("Failures per test_modeling_xxx", summary["failures_per_modeling_key"], label="model ")
  82. _print_counter("Failures per test file", summary["failures_per_file"])
  83. _print_counter("Failures per test name (base)", summary["failures_per_testname"])
  84. if __name__ == "__main__":
  85. main()