Files
2026-08-11 00:10:12 +08:00

150 lines
5.5 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from typing import Any
def _summary_path(path: Path) -> Path:
if path.is_dir():
return path / "summary.json"
return path
def load_baseline_summary(path: Path) -> dict[str, Any]:
summary_path = _summary_path(path)
try:
data = json.loads(summary_path.read_text(encoding="utf-8"))
except FileNotFoundError as error:
raise RuntimeError(f"missing baseline report summary: {summary_path}") from error
except json.JSONDecodeError as error:
raise RuntimeError(f"invalid baseline report summary JSON: {summary_path}") from error
if not isinstance(data, dict):
raise RuntimeError(f"baseline report summary must be an object: {summary_path}")
suites = data.get("suites")
if suites is not None and not isinstance(suites, list):
raise RuntimeError(f"baseline report summary suites must be a list: {summary_path}")
return data
def _suite_key(summary: dict[str, Any]) -> str | None:
suite = summary.get("suite")
return str(suite) if suite else None
def _by_suite(summaries: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
return {key: summary for summary in summaries if (key := _suite_key(summary)) is not None}
def _int_value(value: Any) -> int | None:
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None
def _total_failures(summary: dict[str, Any] | None) -> int | None:
if summary is None:
return None
return _int_value(summary.get("total_failures"))
def _gate_failures(summary: dict[str, Any] | None) -> int | None:
if summary is None:
return None
return _int_value(summary.get("gate_failures", summary.get("total_failures")))
def _case_count(summary: dict[str, Any] | None) -> int | None:
if summary is None:
return None
cases = summary.get("cases")
if isinstance(cases, (dict, list, tuple)):
return len(cases)
if cases is not None:
return _int_value(cases)
runs = summary.get("runs")
if isinstance(runs, (dict, list, tuple)):
return len(runs)
return None
def _delta(current: int | None, baseline: int | None) -> int | None:
if current is None or baseline is None:
return None
return current - baseline
def _suite_status(current: dict[str, Any] | None, baseline: dict[str, Any] | None) -> str:
if baseline is None:
return "added"
if current is None:
return "removed"
fields = (
_total_failures(current) != _total_failures(baseline),
_gate_failures(current) != _gate_failures(baseline),
_case_count(current) != _case_count(baseline),
)
return "changed" if any(fields) else "unchanged"
def build_report_diff(
*,
current_summaries: list[dict[str, Any]],
baseline_summary: dict[str, Any],
baseline_path: Path,
) -> dict[str, Any]:
baseline_summaries = [summary for summary in baseline_summary.get("suites", []) if isinstance(summary, dict)]
current_by_suite = _by_suite(current_summaries)
baseline_by_suite = _by_suite(baseline_summaries)
rows: list[dict[str, Any]] = []
for suite in sorted(current_by_suite.keys() | baseline_by_suite.keys()):
current = current_by_suite.get(suite)
baseline = baseline_by_suite.get(suite)
current_total = _total_failures(current)
baseline_total = _total_failures(baseline)
current_gate = _gate_failures(current)
baseline_gate = _gate_failures(baseline)
current_cases = _case_count(current)
baseline_cases = _case_count(baseline)
rows.append(
{
"suite": suite,
"status": _suite_status(current, baseline),
"baseline_total_failures": baseline_total,
"current_total_failures": current_total,
"total_failures_delta": _delta(current_total, baseline_total),
"baseline_gate_failures": baseline_gate,
"current_gate_failures": current_gate,
"gate_failures_delta": _delta(current_gate, baseline_gate),
"baseline_cases": baseline_cases,
"current_cases": current_cases,
"cases_delta": _delta(current_cases, baseline_cases),
}
)
baseline_total_failures = sum(_total_failures(summary) or 0 for summary in baseline_summaries)
current_total_failures = sum(_total_failures(summary) or 0 for summary in current_summaries)
baseline_gate_failures = sum(_gate_failures(summary) or 0 for summary in baseline_summaries)
current_gate_failures = sum(_gate_failures(summary) or 0 for summary in current_summaries)
by_status = {status: sum(1 for row in rows if row["status"] == status) for status in ("added", "removed", "changed", "unchanged")}
return {
"schema_version": 1,
"baseline": str(_summary_path(baseline_path)),
"summary": {
**by_status,
"total_changes": by_status["added"] + by_status["removed"] + by_status["changed"],
"baseline_total_failures": baseline_total_failures,
"current_total_failures": current_total_failures,
"total_failures_delta": current_total_failures - baseline_total_failures,
"baseline_gate_failures": baseline_gate_failures,
"current_gate_failures": current_gate_failures,
"gate_failures_delta": current_gate_failures - baseline_gate_failures,
},
"suites": rows,
}