Skill: dedup-rust-tests
Dateiverweise (@path) in diesem Skill beziehen sich auf dieses Verzeichnis.
Was ich tue
Ich habe zwei Betriebsmodi, die zusammenarbeiten:
-
DEDUP — Identifiziert und entfernt redundante Rust-Unit-Tests, bei denen ein anderer Test bereits dieselben Codepfade mit denselben oder gleichwertigen Eingabewerten abdeckt. Verifiziert durch
cargo llvm-cov-Abdeckungsvergleich (Vorher vs. Nachher). -
GAP-FILL — Analysiert nach der Deduplizierung den
llvm-cov-Bericht, um ungetesteten oder schlecht getesteten Code zu finden (spezifische Monomorphisierungen, Trait-Implementierungen, feature-geschützten Code), und schreibt dann gezielte Unit-Tests, um diese Lücken zu schließen. Verifiziert durch Abdeckungsverbesserung.
Beide Modi teilen einen gemeinsamen Basis-Build- und Abdeckungsmessungs-Workflow.
Wann ich verwendet werden soll
Für DEDUP:
- Ihre Rust-Testsuite ist organisch gewachsen, mit mehreren Testmodulen, die dieselben Codepfade abdecken
- Ein "altmodisches" Inline-Testmodul (
#[cfg(test)] mod tests) existiert neben neueren, separaten Testdateien (more_tests.rs), die die älteren Tests ersetzen - Ein "Smoke-Test" (
it_works) dupliziert Assertions aus mehreren gezielten Tests - Sie haben eine Codebasis mit duplizierten Testszenarien geerbt (z.B.
test_abs+test_fraction_abs, die beide dieselbe Methode mit überlappenden Eingaben testen) - Sie möchten die CI-Testzeit verkürzen, ohne die Abdeckung zu verlieren
Für GAP-FILL:
- Generische Typen werden nur für einen konkreten Typ getestet (z.B.
Fraction<i32>) – der Abdeckungsbericht zeigt ungetestete Monomorphisierungen (Fraction<i8>,Fraction<i128>) - Trait-Implementierungen (
From<i8>,TryFrom<...>) zeigen 0 % Abdeckung - Feature-geschützter Code (
#[cfg(feature = "...")]) ist ungetestet - Private Methoden (
signum(),is_positive()) sind ungedeckt, weil sie nur innerhalb ihres eigenen Moduls getestet werden können
Verwende mich NICHT, wenn:
- Tests eigenschaftsbasiert / generativ sind (quickcheck, proptest), die Invarianten testen – diese sind absichtlich redundant
- Der Test der einzige ist, der einen bestimmten Codepfad ausführt, selbst wenn die Werte ähnlich erscheinen
- Das Projekt kein Code-Coverage-Tool konfiguriert hat
- Sie nur Tests hinzufügen möchten, ohne zuerst auf Redundanz zu prüfen
Wie ich arbeite
Phase 0: Die Testlandschaft verstehen
# Alle Rust-Quelldateien auflisten
Get-ChildItem -Recurse -Filter *.rs
# Alle Testannotationen und -module finden
Select-String -Path "src/**/*.rs" -Pattern "#\[test\]|#\[cfg\(test\)\]|#\[quickcheck\]|mod tests"
# Tests pro Datei zählen
Select-String -Path "src/**/*.rs" -Pattern "^\\s*fn (test_|check_)" | Group-Object -Property Filename
Kartieren Sie die Teststruktur:
- Inline
#[cfg(test)] mod tests { ... }innerhalb von Quelldateien – private Methoden sind hier zugänglich - Eigenständige
tests/*.rs-Integrationstestdateien - Feature-geschützte Testmodule (
#[cfg(feature = "...")]) - Quickcheck-/Proptest-Eigenschaftstests – niemals entfernen
- Doc-Tests (`/// ``` ... ````) – tragen ebenfalls zur Abdeckung bei
Schlüsselfragen:
- Gibt es mehrere Testmodule, die denselben Code abdecken? (z.B.
fractions.rs::testsvsmore_tests.rs) - Gibt es "altmodische" Tests neben "umfassenden" Tests?
- Gibt es einen Smoke-Test (
it_works), der Assertions aus gezielten Tests dupliziert? - Welche generischen Typparameter werden verwendet?
Phase 1: Redundanz erkennen
Stufe 1: Gleiche Assertions mit identischen Werten
Zwei Tests sind redundant, wenn sie dieselbe Funktion mit identischen oder strikt untergeordneten Eingabewerten ausführen:
// test_abs (redundant — Teilmenge von test_fraction_abs)
#[test]
fn test_abs() {
assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4)); // gleich wie unten
}
// test_fraction_abs (behalten — deckt mehr ab)
#[test]
fn test_fraction_abs() {
assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4)); // dupliziert
assert_eq!(Fraction::new(3, -4).abs(), Fraction::new(3, 4)); // einzigartig
}
Regel: Wenn die Assertions von Test A alle in Test B enthalten sind, ist Test A redundant. Entferne A.
Stufe 2: Gleicher Codepfad mit äquivalenten Werten
Zwei Tests sind redundant, wenn sie denselben Funktionspfad mit äquivalenten Eingaben ausführen:
// test_mul_div (redundant — durch test_fraction_arithmetic ersetzt)
#[test]
fn test_mul_div() {
assert_eq!(Fraction::new(3, 4) * Fraction::new(5, 6), Fraction::new(5, 8));
assert_eq!(Fraction::new(3, 4) / Fraction::new(5, 6), Fraction::new(9, 10));
}
// test_fraction_arithmetic (behalten — deckt mul/div UND auch add/sub/neg/reciprocal ab)
#[test]
fn test_fraction_arithmetic() {
assert_eq!(Fraction::new(1, 2) + Fraction::new(1, 3), Fraction::new(5, 6));
assert_eq!(Fraction::new(1, 2) - Fraction::new(1, 3), Fraction::new(1, 6));
assert_eq!(Fraction::new(1, 2) * Fraction::new(1, 3), Fraction::new(1, 6));
assert_eq!(Fraction::new(1, 2) / Fraction::new(1, 3), Fraction::new(3, 2));
assert_eq!(-Fraction::new(1, 2), Fraction::new(-1, 2));
assert_eq!(Fraction::new(1, 2).inv(), Fraction::new(2, 1));
}
Regel: Wenn Test A nur die Operationen X und Y abdeckt, aber Test B X, Y und Z abdeckt, entferne A.
Stufe 3: Doppelte Tests über Module hinweg
Dieselbe Funktion wird in zwei separaten Testmodulen mit überlappenden Szenarien getestet:
// fractions.rs::tests::test_fraction_from_i32_to_i64 (redundant)
#[test]
fn test_fraction_from_i32_to_i64() {
let frac: Fraction<i64> = Fraction::from(3_i32);
assert_eq!(frac, Fraction::new(3, 1));
}
// more_tests.rs::test_from_i32_to_i64 (behalten — identisch, behalte den neueren)
#[test]
fn test_from_i32_to_i64() {
let f_i32 = 3;
let f_i64 = Fraction::<i64>::from(f_i32);
assert_eq!(f_i64, Fraction::new(3i64, 1i64));
}
Regel: Wenn Tests sowohl in Inline- als auch in separaten Modulen leben und dasselbe identisch testen, entferne das Duplikat. Behalte im Allgemeinen den in der dedizierten Testdatei.
Stufe 4: Smoke-Test durch gezielte Tests ersetzt
// it_works (redundanter Smoke-Test — entfernen)
#[test]
fn it_works() {
assert_eq!(const_gcd(4, -6), 2); // abgedeckt durch test_const_gcd
assert_eq!(Fraction::new(30, -40), Fraction::new(-3, 4)); // abgedeckt durch test_fraction_construction
assert_eq!(Fraction::from(30), Fraction::new(30, 1)); // abgedeckt durch test_fraction_construction
assert_eq!(Fraction::<i32>::default(), Fraction::new(0, 1)); // abgedeckt durch test_fraction_construction
}
Regel: Ein Smoke-Test, der Assertions aus mehreren gezielten Tests dupliziert, sollte entfernt werden. Er bietet keinen Abdeckungswert.
NIEMALS entfernen
- Quickcheck-/Proptest-Tests — sie laufen mit zufälligen Eingaben und überprüfen Invarianten
- Den einzigen Test für einen bestimmten Typ — selbst wenn er ähnlich aussieht, führt er eine andere Monomorphisierung aus
- Tests, die der alleinige Ausführer eines Codepfads sind — überprüfen Sie den Abdeckungsbericht
Phase 2: Basislinie erstellen (VOR dem Entfernen)
# Basis-Abdeckung generieren
cargo llvm-cov --all-features --workspace --summary-only
# Auch detaillierten Bericht für zeilenweisen Vergleich speichern
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-before.lcov
Basis-Metriken erfassen:
- Regionen-, Zeilen-, Funktionsabdeckungsprozentsätze
- Anzahl der nicht abgedeckten Regionen
- Gesamttestanzahl:
cargo test --all-features --workspace 2>&1 | Select-String "test result: ok. (\d+) passed"
Phase 3: Die redundanten Tests entfernen
Entfernen Sie jede redundante Testfunktion aus ihrer Datei durch genaue Bearbeitungen. Führen Sie nach jeder Entfernungsrunde aus:
cargo test --all-features --workspace
Wenn die Kompilierung aufgrund ungenutzter Importe fehlschlägt (z.B. use num_integer::gcd;, das nur vom entfernten Test verwendet wurde), bereinigen Sie diese.
Häufige Aufräumarbeiten:
- Entfernen Sie
#[cfg(test)] use ...;-Importe, die nur für entfernte Tests waren - Entfernen Sie modulweite
use num_integer::gcd;, wenn nur vom entfernten Smoke-Test verwendet - Entfernen Sie auskommentierten Testcode, der auf entfernte Funktionen verweist
Phase 4: Neu erstellen und NACH-DEDUP-Metriken sammeln
cargo llvm-cov --all-features --workspace --summary-only
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-after-dedup.lcov
Bewertungskriterien für DEDUP:
| Prüfung | Anforderung |
|---|---|
| Alle Tests bestehen | Muss bestehen |
| Pro-Datei-Zeilenabdeckung | Keine Verringerung (Erhöhung ist OK – weniger Testzeilen im Binary = höheres Abdeckungsverhältnis) |
| Anzahl nicht abgedeckter Regionen | Sollte sinken oder gleich bleiben (Entfernen von totem Testcode kann das Abdeckungsverhältnis erhöhen) |
| Keine neuen ungedeckten Codepfade | Vergleiche FNDA:0-Einträge im lcov vorher/nachher |
Wenn die Abdeckung für irgendeine Datei sinkt, machen Sie die Änderung sofort rückgängig – der Test war NICHT redundant.
Phase 5: Abdeckungslücken analysieren (GAP-FILL-Modus)
Die lcov-Datei auf ungedeckte Funktionen parsen
Die FNDA:0-Zeilen in der lcov-Datei zeigen genau, welche Monomorphisierungen ungetestet sind:
FNDA:0,_RNvMNt...INtB2_8FractionnE7new_rawB4_ # Fraction<type_n>::new_raw — ungetestet
FNDA:0,_RNvMs2_Nt...INtB5_8FractionpE11is_positiveB7_ # Fraction<type_p>::is_positive — ungetestet
FNDA:0,_RNvXs7_Nt...INtB5_8FractionlEINtNt...4FromhE4fromB7_ # From<i8> for Fraction — ungetestet
Häufige Lückenmuster in Rust:
| Muster | Beispiel | Was fehlt |
|---|---|---|
| Ungetestete Monomorphisierung | Fraction<i8> nie instanziiert | Tests mit Fraction::<i8>::new(...) hinzufügen |
| Ungetestete Trait-Impl | From<i8> for Fraction<i32> | Fraction::from(5i8)-Test hinzufügen |
| Ungetestete TryFrom-Impl | TryFrom<Fraction<i64>> for Fraction<i32> | Fraction::<i32>::try_from(f_i64)-Test hinzufügen |
| Private Methoden | signum(), is_positive() ungedeckt für i8 | Muss von innerhalb desselben #[cfg(test)] mod tests getestet werden |
| Feature-geschützter Code | #[cfg(feature = "serde")] ungetestet | Test mit --features serde hinzufügen |
| Neg für bestimmten Typ | Neg-Impl für Fraction<i8> | -Fraction::<i8>::new(...) hinzufügen |
Dekodieren von lcov-gemangelten Namen
Die Rust-gemangelten Namen in lcov geben Typparameter an. Wichtige Suffixe:
NtB5_8FractionlE...— Fraction mit Typl(üblicherweisei64)NtB5_8FractionnE...— Fraction mit Typn(üblicherweisei8)NtB5_8FractionxE...— Fraction mit Typx(üblicherweisei128)NtB5_8FractionpE...— Fraction mit Typp(üblicherweisei8in einigen Builds)
Die genaue Zuordnung hängt von der Monomorphisierungsreihenfolge ab. Querverweis mit den tatsächlich in Tests verwendeten Typen:
i32— fast immer abgedeckt (überall verwendet)i64— prüfen, ob abgedeckt (oft überFrom<i32> for Fraction<i64>)i8— selten abgedeckt (benötigt expliziten Test)i16— selten abgedeckti128— kann über quickcheck abgedeckt seinBigInt— über feature-geschützte Tests abgedeckt
Phase 6: Gezielte Tests schreiben
Für ungetestete Monomorphisierungen (i8, i16, etc.)
Schreiben Sie einen umfassenden Test, der Konstruktion, Setter, Arithmetik, Vergleich und Sonderwerte abdeckt:
#[test]
fn test_i8_construction() {
let f = Fraction::new(1i8, 2i8);
assert_eq!(f, Fraction::new(1i8, 2));
let f_raw = Fraction::new_raw(3i8, 4i8);
assert_eq!(*f_raw.numer(), 3i8);
assert_eq!(*f_raw.denom(), 4i8);
}
#[test]
fn test_i8_setters() {
let mut f: Fraction<i8> = Fraction::new(1, 2);
f.set_zero();
assert!(f.is_zero());
f.set_one();
assert!(f.is_one());
f.set_infinite();
assert!(f.is_infinite());
f.set_nan();
assert!(f.is_nan());
}
#[test]
fn test_i8_arithmetic() {
let f1: Fraction<i8> = Fraction::new(1, 2);
let f2: Fraction<i8> = Fraction::new(1, 3);
assert_eq!(f1 + f2, Fraction::new(5i8, 6));
assert_eq!(f1 - f2, Fraction::new(1i8, 6));
assert_eq!(f1 * f2, Fraction::new(1i8, 6));
assert_eq!(f1 / f2, Fraction::new(3i8, 2));
assert_eq!(-f1, Fraction::new(-1i8, 2));
assert_eq!(f1.abs(), Fraction::new(1i8, 2));
}
Für private Methoden (müssen im selben Modul sein)
Private Methoden wie signum(), is_positive(), is_negative() können nur von innerhalb eines #[cfg(test)] mod tests-Blocks innerhalb derselben Quelldatei getestet werden, in der sie definiert sind:
// In fractions.rs, innerhalb #[cfg(test)] mod tests:
#[test]
fn test_i8_signum_and_predicates() {
let pos: Fraction<i8> = Fraction::new(3, 4);
assert!(pos.is_positive());
assert!(!pos.is_negative());
assert_eq!(pos.signum(), Fraction::new(1i8, 1));
let neg: Fraction<i8> = Fraction::new(-3, 4);
assert!(neg.is_negative());
assert_eq!(neg.signum(), Fraction::new(-1i8, 1));
let zero: Fraction<i8> = Fraction::new(0, 1);
assert_eq!(zero.signum(), Fraction::new(0i8, 1));
}
Für ungetestete Trait-Implementierungen
#[test]
fn test_from_i8_to_i32() {
let f: Fraction<i32> = Fraction::from(5i8);
assert_eq!(f, Fraction::new(5i32, 1));
}
#[test]
fn test_try_from_i64_to_i32() {
use core::convert::TryFrom;
let f_i64 = Fraction::new(5i64, 1i64);
let f_i32 = Fraction::<i32>::try_from(f_i64).unwrap();
assert_eq!(f_i32, Fraction::new(5i32, 1));
}
Regeln für Testwerte
- Verwenden Sie kleine, sichere Werte für schmale Typen (z.B. i8 max ist 127 – Überlauf vermeiden)
- Testen Sie sowohl positive als auch negative Werte
- Testen Sie Randfälle: Nenner Null (= unendlich), Null/Null (= NaN), Nenner mit entgegengesetztem Vorzeichen
- Testen Sie Identitätsoperationen:
-(-x) == x,x * 1 == x,x + 0 == x - Testen Sie KEINE privaten Methoden aus separaten Testdateien – sie werden nicht kompilieren
Phase 7: Neu erstellen und NACH-GAP-FILL-Metriken sammeln
cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace
cargo llvm-cov --all-features --workspace --summary-only
Phase 8: Abdeckungsverbesserung melden
| Metrik | Vorher | Nachher | Δ |
|-----------------------|----------|-----------|----------|
| Testanzahl | N | N' | +Δ |
| fractions.rs Regionen | 93.76% | 98.45% | +4.69% |
| fractions.rs Zeilen | 92.77% | 98.97% | +6.20% |
| fractions.rs Funktionen| 88.10% | 98.67% | +10.57% |
| Nicht abgedeckte Regionen| 70 | 15 | -55 |
Entfernte redundante Tests: 11
Geschlossene Abdeckungslücken: Fraction<i8>, From<i8>, From<u8>, TryFrom<...>