Files
qt6-migration-pipeline/app/migration_pipeline/reporter.py
T

222 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Batch comparison reporter.
Generates a Markdown report that places Track A and Track B side-by-side
so the burn-phase decision can be made on objective metrics.
"""
from __future__ import annotations
import json
from pathlib import Path
from migration_pipeline.models import BatchResult, FileResult, FileStatus
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def write_comparison_report(
result_a: BatchResult,
result_b: BatchResult,
output_path: Path,
) -> None:
"""Write a Markdown comparison of Track A vs Track B to *output_path*."""
output_path.parent.mkdir(parents=True, exist_ok=True)
md = _render_markdown(result_a, result_b)
output_path.write_text(md, encoding="utf-8")
# Machine-readable companion file.
json_path = output_path.with_suffix(".json")
json_path.write_text(
json.dumps(_to_dict(result_a, result_b), indent=2, ensure_ascii=False),
encoding="utf-8",
)
# ---------------------------------------------------------------------------
# Markdown rendering
# ---------------------------------------------------------------------------
def _render_markdown(a: BatchResult, b: BatchResult) -> str:
ts = _utc_now()
lines: list[str] = [
f"# Migrationsbericht – Batch `{a.batch_id}`",
f"_Erstellt: {ts}_",
"",
"## Übersicht",
"",
"| Metrik | Track A – konservativ | Track B – modern |",
"|---|---|---|",
f"| Branch | `{a.branch}` | `{b.branch}` |",
f"| Dateien gesamt | {a.total_files} | {b.total_files} |",
f"| Erfolge | {a.success_count} ({a.success_rate:.0%}) | {b.success_count} ({b.success_rate:.0%}) |",
f"| Quarantäne | {a.quarantine_count} ({a.quarantine_rate:.0%}) | {b.quarantine_count} ({b.quarantine_rate:.0%}) |",
f"| Warnungen (gesamt, advisory) | {a.total_warnings} | {b.total_warnings} |",
f"| Mittlere Laufzeit | {a.mean_elapsed_seconds:.0f} s | {b.mean_elapsed_seconds:.0f} s |",
"",
"## Datei-Detailtabelle",
"",
"| Datei | Track A Status | Track A Zeit | Track A Warn | Track B Status | Track B Zeit | Track B Warn |",
"|---|---|---|---|---|---|---|",
]
# Build lookup for track B by file path.
b_by_file = {r.file_path: r for r in b.results}
for ra in a.results:
rb = b_by_file.get(ra.file_path)
name = Path(ra.file_path).name
lines.append(
f"| `{name}` "
f"| {_status_badge(ra.status)} "
f"| {ra.elapsed_seconds:.0f} s "
f"| {ra.gate.total_warnings} "
f"| {_status_badge(rb.status) if rb else '—'} "
f"| {f'{rb.elapsed_seconds:.0f} s' if rb else '—'} "
f"| {rb.gate.total_warnings if rb else '—'} |"
)
lines += [
"",
"## Quarantäne-Dateien",
"",
]
quarantine_a = [r for r in a.results if r.status == FileStatus.QUARANTINED.value]
quarantine_b = [r for r in b.results if r.status == FileStatus.QUARANTINED.value]
if quarantine_a or quarantine_b:
lines += [
"| Datei | Track | Fehlerklasse | Versuche | Empfehlung |",
"|---|---|---|---|---|",
]
for r in quarantine_a + quarantine_b:
q = r.quarantine_entry
if q:
name = Path(r.file_path).name
rec = q.manual_recommendation.replace("|", "\\|")[:80]
lines.append(
f"| `{name}` | {r.track} | `{q.failure_class}` "
f"| {q.retry_count} | {rec}… |"
)
else:
lines.append("_Keine Dateien in Quarantäne._")
lines += [
"",
"## Warnungen (Advisory)",
"",
"> Warnungen blockieren keine Quality Gates. Sie sind hier zur "
"Entscheidungshilfe für die Burn-Phase aufgeführt.",
"",
]
warn_rows = [
r for r in (a.results + b.results) if r.gate.total_warnings > 0
]
if warn_rows:
lines += [
"| Datei | Track | Anzahl Warnungen |",
"|---|---|---|",
]
for r in sorted(warn_rows, key=lambda x: -x.gate.total_warnings):
lines.append(
f"| `{Path(r.file_path).name}` | {r.track} "
f"| {r.gate.total_warnings} |"
)
else:
lines.append("_Keine Warnungen aufgetreten._")
lines += [
"",
"## Burn-Phase Empfehlung",
"",
_burn_recommendation(a, b),
"",
"---",
"_Automatisch generiert von der Qt5→Qt6 Migrationspipeline._",
]
return "\n".join(lines)
def _status_badge(status: str) -> str:
icons = {
FileStatus.SUCCESS.value: "✅ Erfolg",
FileStatus.QUARANTINED.value: "🔴 Quarantäne",
FileStatus.IN_PROGRESS.value: "⏳ Läuft",
FileStatus.PENDING.value: "⏸ Ausstehend",
}
return icons.get(status, status)
def _burn_recommendation(a: BatchResult, b: BatchResult) -> str:
if a.success_rate > b.success_rate + 0.05:
return (
"**Track A (konservativ)** hat eine höhere Erfolgsquote. "
"Empfehlung für die Burn-Phase: Track A als Basis, Track B für "
"ausgewählte, gut getestete Module."
)
if b.success_rate > a.success_rate + 0.05:
return (
"**Track B (modern)** hat eine höhere Erfolgsquote. "
"Empfehlung: Track B als Basis übernehmen."
)
if a.total_warnings < b.total_warnings:
return (
"Beide Tracks haben ähnliche Erfolgsquoten. "
"Track A hat weniger Warnungen – bevorzuge Track A für risikoarme Module, "
"Track B für Module mit guter Testabdeckung."
)
return (
"Beide Tracks sind gleichwertig. "
"Entscheidung liegt beim Team – empfohlene Kriterien: "
"Wartbarkeit, Warnungsfreiheit, vorhandene Testabdeckung."
)
# ---------------------------------------------------------------------------
# JSON serialisation helper
# ---------------------------------------------------------------------------
def _to_dict(a: BatchResult, b: BatchResult) -> dict:
def _result(r: BatchResult) -> dict:
return {
"batch_id": r.batch_id,
"track": r.track,
"branch": r.branch,
"total_files": r.total_files,
"success_count": r.success_count,
"quarantine_count": r.quarantine_count,
"total_warnings": r.total_warnings,
"mean_elapsed_seconds": round(r.mean_elapsed_seconds, 1),
"success_rate": round(r.success_rate, 4),
"quarantine_rate": round(r.quarantine_rate, 4),
"files": [_file_result(f) for f in r.results],
}
def _file_result(f: FileResult) -> dict:
d: dict = {
"file_path": f.file_path,
"status": f.status,
"iterations": f.iterations,
"elapsed_seconds": f.elapsed_seconds,
"warnings": f.gate.total_warnings,
"test_name": f.test_name,
}
if f.quarantine_entry:
d["quarantine"] = f.quarantine_entry.__dict__
return d
return {"track_a": _result(a), "track_b": _result(b)}
# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------
def _utc_now() -> str:
from datetime import datetime, timezone
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")