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

157 lines
4.9 KiB
Python

"""Test-file generation for migrated C++ code.
Strategy (B + C combined, no pre-existing test infrastructure):
- QTest for Qt-heavy translation units (QObject / signals / slots)
- gtest for pure-logic / non-UI code
- Smoke CTest for every file (build + link + default-construction check)
Generated files are written to ``{worktree_root}/tests_generated/`` and
registered in that directory's CMakeLists.txt (idempotent via a guard
comment).
"""
from __future__ import annotations
import asyncio
from pathlib import Path
import httpx
from migration_pipeline.api_client import call_api
# ---------------------------------------------------------------------------
# System prompts
# ---------------------------------------------------------------------------
_QTEST_SYSTEM = (
"Du bist ein QA-Ingenieur für Qt6/C++20. "
"Schreibe einen eigenständigen QTest Unit-Test für den folgenden migrierten Qt6-Code. "
"Verwende QCOMPARE, QVERIFY und QTEST_MAIN. "
"Gib NUR den C++ Quellcode zurück, ohne Markdown oder Erklärungen."
)
_GTEST_SYSTEM = (
"Du bist ein QA-Ingenieur für C++20. "
"Schreibe einen eigenständigen Google Test (gtest) Unit-Test für den folgenden C++ Code. "
"Gib NUR den C++ Quellcode zurück, ohne Markdown oder Erklärungen."
)
_SMOKE_SYSTEM = (
"Du bist ein QA-Ingenieur. "
"Schreibe einen minimalen Smoke-Test mit Google Test, der sicherstellt, dass die Datei "
"kompiliert, linkt und die Default-Konstruktion der Hauptklasse nicht crasht. "
"Gib NUR den C++ Quellcode zurück, ohne Markdown oder Erklärungen."
)
# ---------------------------------------------------------------------------
# Qt-content heuristic
# ---------------------------------------------------------------------------
_QT_MARKERS = (
"QObject",
"QWidget",
"QApplication",
"QMainWindow",
"emit ",
"signals:",
"slots:",
"Q_OBJECT",
"Q_EMIT",
)
def _is_qt_heavy(code: str) -> bool:
return any(marker in code for marker in _QT_MARKERS)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
async def generate_tests(
client: httpx.AsyncClient,
model: str,
source_file_path: str,
migrated_code: str,
worktree_root: Path,
track_id: str,
) -> str:
"""Generate unit + smoke tests for *migrated_code*.
Returns the CTest test-name prefix (e.g. ``test_track_a_conservative_MyClass``).
Both the unit test and smoke test are registered under that prefix.
"""
base_name = Path(source_file_path).stem
test_prefix = f"test_{track_id}_{base_name}"
tests_dir = worktree_root / "tests_generated"
tests_dir.mkdir(parents=True, exist_ok=True)
use_qtest = _is_qt_heavy(migrated_code)
unit_system = _QTEST_SYSTEM if use_qtest else _GTEST_SYSTEM
framework = "Qt6::Core Qt6::Test" if use_qtest else "GTest::gtest_main"
# Generate unit test and smoke test in parallel.
unit_code, smoke_code = await asyncio.gather(
call_api(
client, model, unit_system,
f"Erstelle einen Unit-Test für folgenden migrierten Qt6-Code:\n\n{migrated_code}",
),
call_api(
client, model, _SMOKE_SYSTEM,
f"Erstelle einen Smoke-Test für:\n\n{migrated_code}",
),
)
unit_file = tests_dir / f"{test_prefix}_unit.cpp"
smoke_file = tests_dir / f"{test_prefix}_smoke.cpp"
unit_file.write_text(unit_code)
smoke_file.write_text(smoke_code)
_register_tests_in_cmake(
tests_dir,
test_prefix,
unit_file.name,
smoke_file.name,
framework,
)
return test_prefix
# ---------------------------------------------------------------------------
# CMakeLists.txt registration (idempotent)
# ---------------------------------------------------------------------------
_GUARD_PREFIX = "# MIGRATION_PIPELINE:"
def _register_tests_in_cmake(
tests_dir: Path,
test_prefix: str,
unit_filename: str,
smoke_filename: str,
framework_libs: str,
) -> None:
cmake_path = tests_dir / "CMakeLists.txt"
guard = f"{_GUARD_PREFIX}{test_prefix}"
# Read existing content to avoid duplicate registrations.
existing = cmake_path.read_text() if cmake_path.exists() else ""
if guard in existing:
return # already registered
entry = (
f"\n{guard}\n"
f"add_executable({test_prefix}_unit {unit_filename})\n"
f"target_link_libraries({test_prefix}_unit PRIVATE {framework_libs})\n"
f"add_test(NAME {test_prefix}_unit COMMAND {test_prefix}_unit)\n"
f"\nadd_executable({test_prefix}_smoke {smoke_filename})\n"
f"target_link_libraries({test_prefix}_smoke PRIVATE GTest::gtest_main)\n"
f"add_test(NAME {test_prefix}_smoke COMMAND {test_prefix}_smoke)\n"
)
with cmake_path.open("a") as fh:
fh.write(entry)