CommunityCodierung & Entwicklunggithub.com

luk036/dedup-rust-tests

Erkennt und entfernt redundante Rust-Unit-Tests und schließt anschließend Abdeckungslücken mit gezielten Tests – alles verifiziert durch Vorher/Nachher-Vergleich mit cargo llvm-cov.

Was ist dedup-rust-tests?

Dieses Skill hilft Entwicklern, Rust-Testsuiten zu bereinigen, indem es zuerst überflüssige Tests entfernt, die dieselben Codepfade mit identischen oder ähnlichen Eingabewerten abdecken, und dann Abdeckungslücken mit neuen, zielgerichteten Tests füllt. Es arbeitet in zwei Hauptmodi: DEDUP (identifiziert und entfernt doppelte Tests) und GAP-FILL (analysiert den llvm-cov-Bericht, um ungetestete Monomorphisierungen, Trait-Implementierungen oder feature-geschützten Code zu finden und entsprechende Tests zu schreiben). Der gesamte Prozess wird durch einen Vorher/Nachher-Vergleich der Codeabdeckung mit `cargo llvm-cov` verifiziert, sodass keine unbeabsichtigten Abdeckungsverluste auftreten. Das Skill ist besonders nützlich, wenn Testsuiten organisch gewachsen sind, alte und neue Testmodule nebeneinander existieren oder wenn generische Typen nur für einen konkreten Typ getestet wurden.

Funktioniert mit~Claude Code~Codex CLI~Cursor✓OpenCode
npx skills add https://github.com/luk036/agent-skills/tree/HEAD/skills/dedup-rust-tests

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Skill: dedup-rust-tests

Dateiverweise (@path) in diesem Skill beziehen sich auf dieses Verzeichnis.

Was ich tue

Ich habe zwei Betriebsmodi, die zusammenarbeiten:

  1. 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).

  2. 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::tests vs more_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üfungAnforderung
Alle Tests bestehenMuss bestehen
Pro-Datei-ZeilenabdeckungKeine Verringerung (Erhöhung ist OK – weniger Testzeilen im Binary = höheres Abdeckungsverhältnis)
Anzahl nicht abgedeckter RegionenSollte sinken oder gleich bleiben (Entfernen von totem Testcode kann das Abdeckungsverhältnis erhöhen)
Keine neuen ungedeckten CodepfadeVergleiche 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:

MusterBeispielWas fehlt
Ungetestete MonomorphisierungFraction<i8> nie instanziiertTests mit Fraction::<i8>::new(...) hinzufügen
Ungetestete Trait-ImplFrom<i8> for Fraction<i32>Fraction::from(5i8)-Test hinzufügen
Ungetestete TryFrom-ImplTryFrom<Fraction<i64>> for Fraction<i32>Fraction::<i32>::try_from(f_i64)-Test hinzufügen
Private Methodensignum(), is_positive() ungedeckt für i8Muss von innerhalb desselben #[cfg(test)] mod tests getestet werden
Feature-geschützter Code#[cfg(feature = "serde")] ungetestetTest mit --features serde hinzufügen
Neg für bestimmten TypNeg-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 Typ l (üblicherweise i64)
  • NtB5_8FractionnE... — Fraction mit Typ n (üblicherweise i8)
  • NtB5_8FractionxE... — Fraction mit Typ x (üblicherweise i128)
  • NtB5_8FractionpE... — Fraction mit Typ p (üblicherweise i8 in 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 über From<i32> for Fraction<i64>)
  • i8 — selten abgedeckt (benötigt expliziten Test)
  • i16 — selten abgedeckt
  • i128 — kann über quickcheck abgedeckt sein
  • BigInt — ü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<...>

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

Verwandte Skills