Was ich tue
Ich habe zwei Betriebsmodi, die zusammenwirken:
-
DEDUP – Identifiziere und entferne redundante Testdateien/-szenarien, bei denen eine andere Datei bereits dieselben Codepfade mit denselben Eingabewerten abdeckt. Verifiziert durch gcov-Coverage-Vergleich (vorher vs. nachher).
-
GAP-FILL – Analysiere nach der Deduplizierung den Coverage-Bericht, um ungetesteten oder schlecht getesteten Code (Header und kompilierte Quellen) zu finden, und schreibe gezielte Unit-Tests, um diese Lücken zu schließen. Verifiziert durch gcov-Coverage-Verbesserung (vorher vs. nachher).
Beide Modi teilen einen gemeinsamen Basis-Build- und Coverage-Messworkflow.
Wann ich verwendet werden soll
Für DEDUP:
- Ihre C++-Testsuite ist organisch gewachsen und enthält mehrere Dateien, die denselben Code abdecken
- Gap-Filler-Testdateien (
*_gaps.cpp) wurden neben umfassenden Testdateien (*_comprehensive.cpp) hinzugefügt - Sie haben eine Codebasis mit doppelten Testszenarien übernommen
- Sie möchten die CI-Testzeit verkürzen, ohne Coverage zu verlieren
Für GAP-FILL:
- Sie haben kompilierte
.cpp-Quelldateien ohne zugehörige Testdatei - Der Coverage-Bericht zeigt Header mit <90% Zeilenabdeckung
- Sie haben gerade redundante Tests entfernt und möchten die gewonnene Testzeit in das Füllen von Lücken investieren
- Sie müssen die Coverage vor einem Release-Termin erhöhen
Verwenden Sie mich NICHT, wenn:
- Tests parametrisiert/generativ dasselbe Property testen (das ist beabsichtigte Redundanz)
- Das Projekt hat keine Coverage-Tooling eingerichtet
- Sie nur Tests hinzufügen müssen, ohne zuerst auf Redundanz zu prüfen
Wie ich arbeite
Phase 0: Verstehen der Testlandschaft
Lesen Sie das Testverzeichnis und die CMakeLists.txt:
test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp # kompilierte Quellen
Prüfen Sie auf:
- Musterpaare:
*_gaps.cpp+*_comprehensive.cpp,*_basic.cpp+*_detailed.cpp - Kompilierte
.cpp-Dateien insource/ohne zugehörigetest_*.cpp - Header in
include/ohne dedizierte Testdatei - Test-Framework (doctest, Catch2, Google Test usw.) und CMake-Globbing
Hinweis:
- Test-Framework und verwendete Makros (
TEST_CASE,CHECK_EQusw.) - Ob CMakeLists.txt
file(GLOB ...)oder explizite Quelllisten verwendet - Das Coverage-Flag-Muster (normalerweise
-DENABLE_TEST_COVERAGE=ON)
Phase 1: Redundanz auf DATEI-Ebene erkennen
Vergleichen Sie für jede Kandidatendatei ihre Testszenarien mit jeder anderen Testdatei in der Suite.
Konkrete Vergleichsmethode:
-
Lesen Sie beide Dateien nebeneinander. Gruppieren Sie Testszenarien nach dem Code, den sie ausführen:
- Freie Funktionen (gcd, lcm usw.)
- Klassenmethoden (Konstruktoren, Operatoren, Zugriffsmethoden, Mutatoren)
- Template-Instanziierungen und Spezialisierungen
- Randfallabdeckung (Null, Unendlich, NaN, Grenzwerte)
-
Prüfen Sie auf Wertduplikation. Wenn beide Dateien dieselbe Funktion mit identischen Werten testen (z.B.
gcd_recur(12, 8) = 4in beiden), ist dieses Szenario redundant. Kleinere Variationen (Fibonacci-Zahlen in einer, aber nicht in der anderen) sind NICHT redundant – das sind einzigartige Eingaben. -
Markieren Sie eine Datei als "redundant", wenn >90% ihrer Testszenarien in einer anderen Datei mit gleicher oder umfassenderer Abdeckung erscheinen. Die verbleibenden <10% einzigartigen Szenarien (triviale Variationen) können gefahrlos entfernt werden.
Phase 2: Basislinie erstellen (VOR der Entfernung)
Erstellen Sie die Testsuite mit Coverage-Instrumentierung:
| Tool | Konfiguration |
|---|---|
| GCC + gcov | cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles" |
| Clang + llvm-cov | cmake -S test -B build/cov -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" -G "Ninja" |
| MSVC | Verwenden Sie OpenCppCoverage oder die integrierte Coverage von VS Enterprise |
Dann erstellen und ausführen:
cmake --build build/cov
./build/cov/<TestExecutable>
Coverage für Bibliotheksheader generieren
cd build/cov
# Für jeden Bibliotheksheader: gcov gegen ein Testobjekt ausführen, das ihn #includet
gcov -o CMakeFiles/<Tests>.dir/source/<test_file>.cpp.o <path>/include/<project>/<header>.hpp
# Für jede kompilierte Quelle: gcov gegen das Bibliotheksobjekt ausführen
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<source>.cpp.o <path>/source/<source>.cpp
Basis-Metriken aufzeichnen:
Header:
array_like.hpp: 100% (28/28)
bpqueue.hpp: 100% (73/73)
...
Kompilierte Quellen:
ell1d.cpp: 0% (0/23) ← UNGETESTET!
ldlt_mgr.cpp: 100% (10/10)
...
Testfälle: N | Assertions: M | Status: SUCCESS
Notieren Sie auch die genauen ungedeckten Zeilen für jede Datei (die #####:-Zeilen im .gcov-Output).
Phase 3: Redundante Dateien entfernen (DEDUP-Modus)
Remove-Item -Force test/source/test_*_gaps.cpp # PowerShell
git status test/
git diff --stat
Phase 4: Neu erstellen und NACH-DEDUP-Metriken sammeln
# Neu konfigurieren (CMake GLOB erkennt möglicherweise fehlende Dateien)
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>
Vergleichen Sie die Coverage mit der Basislinie. Wenn die Coverage eines Headers sinkt oder sich die ungedeckten Zeilen ändern, sofort rückgängig machen – die Datei war NICHT redundant.
Bewertungskriterien für DEDUP:
| Prüfung | Anforderung |
|---|---|
| Alle Tests bestehen | Muss bestehen (sowohl vorher als auch nachher) |
| Zeilenabdeckung pro Header | Exakt gleich vorher und nachher |
| Ungedeckte Zeilen | Identische Menge ungedeckter Zeilen |
| Testanzahl verringert | Erwartet – Testfälle entfernt |
| Assertion-Anzahl verringert | Erwartet – Assertions entfernt |
Phase 5: Coverage-Lücken analysieren (GAP-FILL-Modus)
Wenn nach der Deduplizierung (oder bei einem Neuanfang) der Coverage-Bericht Lücken zeigt, identifizieren Sie ungetesteten Code:
Lückentyp A: Kompilierte Quelle ohne Abdeckung
Prüfen Sie source/*.cpp gegen test/source/*.cpp. Jede .cpp in source/ ohne passende test_*.cpp ist wahrscheinlich ungetestet.
# Quelldateien ohne Testabdeckung finden
Get-ChildItem source/*.cpp | ForEach-Object {
$base = $_.Name -replace '\.cpp$'
$test = "test/source/test_$base.cpp"
if (-not (Test-Path $test)) { Write-Host "UNGETESTET: $($_.Name)" }
}
Lückentyp B: Bibliotheksheader mit niedriger Abdeckung
Sehen Sie sich den gcov-Bericht für Header mit <90% Zeilenabdeckung oder "keine ausführbaren Zeilen" an. Template-lastige Header können die Abdeckung der .cpp-Datei zuschreiben – prüfen Sie beide.
Lückentyp C: Funktionen/Verzweigungen innerhalb abgedeckter Dateien
Die ungedeckten Zeilen (#####: in gcov) innerhalb teilweise abgedeckter Dateien zeigen bestimmte Verzweigungen, die nicht ausgeführt werden. Dies sind die Ziele mit dem höchsten Wert.
Phase 6: Gezielte Tests schreiben
6a: Ungetesteten Quellcode lesen
Lesen Sie die Quelldatei vollständig, um zu verstehen:
- Die Klassen-/Funktions-API (Konstruktoren, Methoden, Parameter)
- Alle bedingten Verzweigungen (if/else, switch-Fälle, frühe Rückgaben)
- Randfälle (Null-/Nulleingaben, Grenzwerte, Fehlerbedingungen)
- Das mathematische oder algorithmische Verhalten
6b: Testfälle entwerfen, die jede Verzweigung abdecken
Für jede Methode die benötigten Testszenarien auflisten:
| Methode | Bedingung | Eingabe | Erwartetes Verhalten |
|---|---|---|---|
update(cut) | beta > tau | g=1, beta=10 mit _r=5 | Gibt NoSoln zurück, Zustand unverändert |
update(cut) | beta < -tau | g=1, beta=-10 mit _r=5 | Gibt NoEffect zurück, Zustand unverändert |
update(cut) | ` | beta | <= tau, g>0` |
update(cut) | ` | beta | <= tau, g<0` |
update_central_cut(cut) | g > 0 | g=1, beta=0 | Halbiert _r, verschiebt Zentrum nach links |
update_central_cut(cut) | g < 0 | g=-1, beta=0 | Halbiert _r, verschiebt Zentrum nach rechts |
Randfälle einschließen:
- Intervalle mit Nullbreite
- Grenzwerte (
beta == tau,beta == -tau) - Null-Gradient (falls zutreffend)
- Wiederholte/verkettete Operationen zur Überprüfung von Zustandsübergängen
6c: Testdatei schreiben
Schreiben Sie eine neue test_<module>.cpp in test/source/ nach den bestehenden Testmustern des Projekts:
#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>
#include <ellalgo/<module>.hpp> // zu testender Header
#include <ellalgo/ell_config.hpp> // falls für Typdefinitionen benötigt
#include <utility> // falls für std::pair benötigt
TEST_CASE("<Module>: Konstruktion") {
auto x = <Module>(...);
CHECK_EQ(x.some_method(), expected_value);
}
TEST_CASE("<Module>: Methode mit Erfolgspfad") {
auto x = <Module>(...);
auto status = x.update(...);
CHECK_EQ(status, CutStatus::Success);
CHECK_EQ(x.state(), expected_state);
}
TEST_CASE("<Module>: Methode mit Fehlerpfad") {
auto x = <Module>(...);
auto status = x.update(...); // löst beta > tau-Zweig aus
CHECK_EQ(status, CutStatus::NoSoln);
}
TEST_CASE("<Module>: Randfälle") {
// Nullbreiten-Intervall, Grenzbedingungen usw.
}
TEST_CASE("<Module>: Verkettete Operationen") {
// Mehrere Updates in Folge
}
Regeln für Testwerte:
- Verwenden Sie hartcodierte erwartete Werte (nicht aus demselben zu testenden Code berechnet)
- Kommentieren Sie jeden erwarteten Wert mit der mathematischen Herleitung (z.B.
// tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5) - Testen Sie jeden
if-Zweig mindestens einmal - Testen Sie jeden
switch-Fall mindestens einmal - Testen Sie mindestens eine verkettete/sequentielle Operation zur Überprüfung der Zustandspersistenz
Phase 7: Neu erstellen und NACH-GAP-FILL-Metriken sammeln
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>
# Coverage-Berichte neu generieren
gcov -o CMakeFiles/<Tests>.dir/source/test_<module>.cpp.o <path>/include/<project>/<module>.hpp
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<module>.cpp.o <path>/source/<module>.cpp
Phase 8: Coverage-Verbesserung melden
| Metrik | Vorher | Nachher | Δ |
|--------|--------|---------|---|
| Testfälle | N | N' | +Δ |
| Assertions | M | M' | +Δ |
| <module>.hpp | XX% | 100% | +Δ |
| <module>.cpp (kompiliert) | 0% | 100% | +100% |
| Alle anderen Dateien | ≥90% | ≥90% | 0% |
Coverage-Lücken geschlossen: ✅
KRITISCHE REGELN
Regel 1: Coverage ist die Wahrheit, nicht die Testanzahl
Eine Testdatei ist nur dann redundant, wenn ihre Entfernung die Zeilenabdeckung jedes Bibliotheksheaders unverändert lässt. Wenn die Coverage sinkt, hat die Datei einzigartige Codepfade abgedeckt. Sofort rückgängig machen.
Beim Hinzufügen von Tests ist ein neuer Test nur erfolgreich, wenn er die Coverage der Zieldatei erhöht. Wenn die Coverage der Zieldatei nicht steigt, übt der Test den Code nicht aus – überprüfen Sie die Testlogik.
Regel 2: Ungedeckte Zeilen prüfen, nicht nur den Prozentsatz
Zwei Läufe können "98,06%" anzeigen, aber unterschiedliche ungedeckte Zeilen haben. Vergleichen Sie immer die exakte Menge der #####:-Zeilen in der .gcov-Datei:
Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^ #####:"
Diese müssen vorher/nachher der Deduplizierung identisch sein. Für Gap-Fill müssen die #####:-Zeilen der Zieldatei abnehmen.
Regel 3: Deduplizieren Sie KEINE eigenschaftsbasierten Tests
Eigenschaftsbasierte / generative Tests (z.B. RapidCheck) sind absichtlich redundant. Sie überprüfen Invarianten mit zufälligen Eingaben. Diese sollten NICHT entfernt werden, selbst wenn sie sich mit beispielbasierten Tests überschneiden.
Regel 4: Deduplizieren Sie KEINE Tests mit unterschiedlichen Werten
Zwei Tests für denselben Operator mit unterschiedlichen Eingabewerten üben unterschiedliche Codepfade aus. Nur entfernen, wenn Eingabewerte und erwartete Ausgaben identisch sind.
Verwenden Sie beim Schreiben neuer Tests ANDERE Eingabewerte als in bestehenden Tests. Das Duplizieren derselben Werte erhöht die Coverage nicht.
Regel 5: Immer aus sauberem Zustand neu erstellen
.gcda-Dateien werden von gcov verbraucht und werden nach Quell- oder Binäränderungen ungültig. Vor der Messung immer neu erstellen und ausführen:
cmake --build build/cov --clean-first # oder von Grund auf neu erstellen
./build/cov/<TestExecutable>
Regel 6: CMake GLOB-Verhalten prüfen
Wenn CMakeLists.txt file(GLOB ...) mit CONFIGURE_DEPENDS verwendet, erkennt CMake automatisch hinzugefügte/entfernte Dateien beim Neuerstellen (CMake 3.12+). Bei älterem CMake nach dem Hinzufügen oder Entfernen von Testdateien manuell neu konfigurieren.
Stellen Sie beim Schreiben einer neuen Testdatei sicher, dass CMake sie aufnimmt:
- Suchen Sie nach
test_<module>.cppin der Build-Ausgabe - Wenn nicht vorhanden, neu konfigurieren:
cmake -S test -B build/cov
Regel 7: Erwartete Werte hartcodieren, nicht neu berechnen
Berechnen Sie beim Schreiben von Tests NIEMALS erwartete Werte mit demselben zu testenden Code. Leiten Sie sie immer von Hand ab und hinterlegen Sie sie als Literale. Dies fängt Regressionen ab, bei denen die Implementierung geändert wird, der Test aber trotzdem besteht, weil beide Seiten denselben fehlerhaften Code verwenden.
Kommentieren Sie komplexe Herleitungen:
// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0 also u=3, l=5-5=0
// _r = half_nonnegative(3-0) = 1.5, _xc = 0 + 1.5 = 1.5
CHECK_EQ(e.xc(), 1.5);
Regel 8: Jede Verzweigung testen
Für jede Funktion mit bedingter Logik alle Verzweigungen auflisten und mindestens einen Test pro Verzweigung schreiben:
| Verzweigung | Testszenario |
|---|---|
if (beta > tau) | Kein Lösungspfad |
if (beta < -tau) | Kein Effektpfad |
else (Erfolg) | Erfolgspfad |
g > 0 innerhalb Erfolg | Positiver Gradient |
g < 0 innerhalb Erfolg | Negativer Gradient |
Grenze beta == tau | Erfolg trotzdem (≤) |
Grenze beta == -tau | Hängt von < vs ≤ ab |
Regel 9: Kompilierte Quellen gegenüber Headern priorisieren
Kompilierte .cpp-Dateien haben mit größerer Wahrscheinlichkeit eine NULL-Abdeckung, da Header-Only-Template-Code oft durch andere Tests beiläufig ausgeführt wird. Priorisieren Sie das Testen:
source/*.cppohne zugehörigetest_*.cpp→ höchste Priorität- Header mit <90% Zeilenabdeckung → mittlere Priorität
- Ungedeckte Verzweigungen in ansonsten abgedeckten Dateien → niedrigere Priorität
Regel 10: Projekt-Testkonventionen einhalten
Bevor Sie Tests schreiben, studieren Sie 2-3 bestehende Testdateien, um Folgendes zu überprüfen:
- Eingerückt Stil und Formatierung
- Testfall-Benennungskonventionen (
snake_case,CamelCaseusw.) - Verwendete Makros (
CHECK,REQUIRE,CHECK_EQ,CHECK_CLOSEusw.) - Ausnahmebehandlung (werden Ausnahmen erwartet oder nicht?)
- Test-Fixture-Verwendung (Klassen mit
SetUp/TearDownusw.) - Test-Framework-Header (#include-Header)
- Projekt-spezifische Test-Utilities oder Matcher
SPEZIFIKATION FÜR BENUTZERDEFINIERTE DATEIEN
- test/source/test_<module>.cpp
Standard-Unit-Testdatei für eine Quelldatei oder einen Header. Folgt den Test-Framework-Konventionen des Projekts (doctest, Catch2, Google Test usw.).
Enthält:
#define-Anpassungen für das Test-Framework (z.B.DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS)#include-Anweisungen für den zu testenden Header und das Test-FrameworkTEST_CASE-Blöcke, die jede öffentliche Methode/Funktion und deren Verzweigungen abdecken- Hartcodierte erwartete Werte mit Kommentaren zur mathematischen Herleitung
- test/CMakeLists.txt
Muss die neue Testdatei enthalten (entweder über file(GLOB ...) oder eine explizite add_executable-Liste). Wenn das Projekt file(GLOB ...) verwendet, stellen Sie sicher, dass CONFIGURE_DEPENDS gesetzt ist (CMake 3.12+), oder konfigurieren Sie nach dem Hinzufügen der Datei neu.
VERWANDTE FÄHIGKEITEN
luk036/analyze-cpp-code — Liest und fasst C++-Code aus einem Repository zusammen
luk036/read-dir — Liest Dateien rekursiv aus einem Verzeichnis
luk036/run-command — Führt Shell-Befehle im Repository aus