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

191 lines
6.0 KiB
Python

"""Quarantine management.
When a file cannot be migrated within the retry budget, it is:
1. Written to ``quarantine/<stem>_<track>_<timestamp>.json`` (machine-readable)
2. Written to ``quarantine/<stem>_<track>_<timestamp>.md`` (manual-review ticket)
The Markdown ticket is the automatically generated "manual ticket" requested
in the pipeline requirements.
"""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from migration_pipeline.config import QUARANTINE_DIR
from migration_pipeline.models import FailureClass, QuarantineEntry
# ---------------------------------------------------------------------------
# Qt5 deprecated-API detector
# ---------------------------------------------------------------------------
# A non-exhaustive list of Qt5 APIs that are removed / changed in Qt6.
_QT5_DEPRECATED_RE = re.compile(
r"\b("
r"QRegExp|QTextCodec|QMatrix|QDesktopWidget|"
r"QSignalMapper|QSortFilterProxyModel::lessThan|"
r"QAbstractItemModel::reset|"
r"QApplication::desktop|"
r"QFontMetrics::width\s*\(|"
r"QUrl::addQueryItem|"
r"QSettings::setDefaultFormat|"
r"QVariant::type\b|"
r"QMetaType::type\b|"
r"QTest::qSleep"
r")\b"
)
def _extract_affected_apis(code: str) -> list[str]:
return sorted(set(_QT5_DEPRECATED_RE.findall(code)))
# ---------------------------------------------------------------------------
# Failure classification
# ---------------------------------------------------------------------------
def _classify(
build_errors: str,
test_output: str,
elapsed: float,
budget: float,
) -> FailureClass:
if elapsed >= budget:
return FailureClass.BUDGET_EXCEEDED
lower_err = build_errors.lower()
if "undefined reference" in lower_err or "linker command failed" in lower_err:
return FailureClass.LINK_ERROR
if "error:" in build_errors:
return FailureClass.COMPILE_ERROR
if "failed" in test_output.lower():
return FailureClass.TEST_FAILURE
return FailureClass.UNKNOWN
def _recommend(fc: FailureClass, affected_apis: list[str]) -> str:
api_str = ", ".join(affected_apis) if affected_apis else "unbekannte APIs"
if fc == FailureClass.BUDGET_EXCEEDED:
return (
"Datei hat das Zeitbudget überschritten. "
"Empfehlung: Datei manuell in kleinere, unabhängige Units zerlegen und erneut starten."
)
if fc == FailureClass.COMPILE_ERROR:
return (
f"Compilerfehler. Prüfe Qt6-API-Änderungen für: {api_str}. "
"Porting Guide: https://doc.qt.io/qt-6/portingguide.html"
)
if fc == FailureClass.LINK_ERROR:
return (
"Linkfehler. Prüfe Qt6-Modulabhängigkeiten in CMakeLists.txt "
"(z.B. find_package(Qt6 ...) und target_link_libraries)."
)
if fc == FailureClass.TEST_FAILURE:
return (
"Semantische Abweichung in Tests. Manuelle Überprüfung der Fachlogik erforderlich. "
"Vergleiche Qt5- und Qt6-Verhalten für betroffene APIs."
)
if fc == FailureClass.CHUNK_MERGE_ERROR:
return (
"Chunk-Merge ist inkonsistent. "
"Empfehlung: Datei als Ganzes manuell migrieren oder Chunk-Grenzen anpassen."
)
return "Unbekannter Fehler. Bitte Logs manuell inspizieren."
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def quarantine_file(
*,
file_path: str,
track: str,
build_errors: str,
test_output: str,
last_code: str,
elapsed: float,
budget: float,
retry_count: int,
chunk_info: str,
) -> QuarantineEntry:
"""Create a QuarantineEntry and write JSON + Markdown ticket to disk."""
now = datetime.now(tz=timezone.utc)
ts = now.strftime("%Y%m%dT%H%M%SZ")
iso_ts = now.isoformat()
fc = _classify(build_errors, test_output, elapsed, budget)
apis = _extract_affected_apis(last_code)
recommendation = _recommend(fc, apis)
entry = QuarantineEntry(
file_path=file_path,
track=track,
failure_class=fc.value,
last_error=(build_errors or test_output)[:2000],
affected_apis=apis,
chunk_info=chunk_info,
manual_recommendation=recommendation,
timestamp=iso_ts,
retry_count=retry_count,
elapsed_seconds=round(elapsed, 1),
)
_write_quarantine_files(entry, ts)
return entry
# ---------------------------------------------------------------------------
# File writers
# ---------------------------------------------------------------------------
def _write_quarantine_files(entry: QuarantineEntry, ts: str) -> None:
QUARANTINE_DIR.mkdir(parents=True, exist_ok=True)
stem = Path(entry.file_path).stem
base_name = f"{stem}_{entry.track}_{ts}"
# --- JSON (machine-readable) -------------------------------------------
json_path = QUARANTINE_DIR / f"{base_name}.json"
json_path.write_text(json.dumps(entry.__dict__, indent=2, ensure_ascii=False))
# --- Markdown ticket (manual review) ------------------------------------
md_path = QUARANTINE_DIR / f"{base_name}.md"
md_path.write_text(_render_ticket(entry))
def _render_ticket(e: QuarantineEntry) -> str:
apis_md = "\n".join(f"- `{a}`" for a in e.affected_apis) or "_keine erkannt_"
error_block = e.last_error.strip() or "_kein Fehlertext_"
return f"""# Quarantäne-Ticket: {Path(e.file_path).name}
| Feld | Wert |
|---|---|
| **Datei** | `{e.file_path}` |
| **Track** | `{e.track}` |
| **Fehlerklasse** | `{e.failure_class}` |
| **Versuche** | {e.retry_count} |
| **Laufzeit** | {e.elapsed_seconds:.1f} s |
| **Zeitstempel** | {e.timestamp} |
| **Chunk-Info** | {e.chunk_info} |
## Letzte Fehlermeldung
```
{error_block}
```
## Betroffene Qt5-APIs
{apis_md}
## Empfehlung für manuelle Migration
{e.manual_recommendation}
---
_Automatisch generiert von der Qt5→Qt6 Migrationspipeline._
"""