luk036/dedup-rust-tests

Trouve et supprime les tests unitaires Rust redondants, puis comble les lacunes de couverture avec des tests ciblés — le tout vérifié par une comparaison avant/après avec cargo llvm-cov.

Qu'est-ce que dedup-rust-tests ?

Cette compétence automatisée pour agents IA (compatible avec Claude Code, Cursor, Codex et autres) optimise les suites de tests Rust en deux phases. D'abord, le mode **DEDUP** détecte et supprime les tests redondants (mêmes assertions, mêmes chemins de code avec des valeurs équivalentes, ou smoke tests dupliqués), en s'appuyant sur `cargo llvm-cov` pour vérifier qu'aucune couverture n'est perdue. Ensuite, le mode **GAP-FILL** analyse le rapport de couverture pour identifier les monomorphisations non testées, les implémentations de traits ou les méthodes privées (`signum()`, `is_positive()`, etc.) sans couverture, et écrit automatiquement des tests ciblés. L'outil suit un workflow structuré en huit phases, de la cartographie initiale des tests à la génération d'un rapport final comparant les métriques de couverture avant et après. Il est particulièrement utile pour les bases de code Rust ayant grandi de manière organique avec des modules de tests redondants ou des lacunes dans la couverture de types génériques et de fonctionnalités optionnelles. Les tests basés sur les propriétés (quickcheck, proptest) ne sont jamais supprimés.

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Compétence : dedup-rust-tests

Les références de fichiers (@path) dans cette compétence sont relatives à ce répertoire.

Ce que je fais

J'ai deux modes de fonctionnement qui travaillent ensemble :

  1. DEDUP — Identifier et supprimer les tests unitaires Rust redondants où un autre test couvre déjà les mêmes chemins de code avec les mêmes valeurs d'entrée ou des valeurs équivalentes. Vérifié via une comparaison de couverture cargo llvm-cov (avant vs. après).

  2. GAP-FILL — Après la déduplication, analyser le rapport llvm-cov pour trouver du code non testé ou insuffisamment testé (monomorphisations spécifiques, implémentations de traits, code conditionnel), puis écrire des tests unitaires ciblés pour combler ces lacunes. Vérifié par l'amélioration de la couverture.

Les deux modes partagent un flux de travail commun de construction de base et de mesure de couverture.

Quand m'utiliser

Pour DEDUP :

  • Votre suite de tests Rust a grandi de manière organique avec plusieurs modules de tests couvrant les mêmes chemins de code
  • Un module de test en ligne « à l'ancienne » (#[cfg(test)] mod tests) coexiste avec des fichiers de test séparés plus récents (more_tests.rs) qui englobent les anciens tests
  • Un « test de fumée » (it_works) duplique des assertions de plusieurs tests ciblés
  • Vous avez hérité d'une base de code avec des scénarios de test dupliqués (par exemple, test_abs + test_fraction_abs testant tous deux la même méthode avec des entrées qui se chevauchent)
  • Vous voulez réduire le temps des tests CI sans perdre de couverture

Pour GAP-FILL :

  • Les types génériques ne sont testés que pour un seul type concret (par exemple, Fraction<i32>) — le rapport de couverture révèle des monomorphisations non testées (Fraction<i8>, Fraction<i128>)
  • Les implémentations de traits (From<i8>, TryFrom<...>) montrent une couverture de 0 %
  • Le code conditionnel (#[cfg(feature = "...")]) n'est pas testé
  • Les méthodes privées (signum(), is_positive()) ne sont pas couvertes car elles ne peuvent être testées qu'à l'intérieur de leur propre module

Ne m'utilisez PAS quand :

  • Les tests sont basés sur les propriétés / génératifs (quickcheck, proptest) testant des invariants — ils sont intentionnellement redondants par conception
  • Le test est le seul à exercer un chemin de code particulier, même si les valeurs semblent similaires
  • Le projet n'a aucun outil de couverture de code configuré
  • Vous avez seulement besoin d'ajouter des tests sans d'abord vérifier la redondance

Comment je fonctionne

Phase 0 : Comprendre le paysage des tests

# Lister tous les fichiers sources Rust
Get-ChildItem -Recurse -Filter *.rs

# Trouver toutes les annotations et modules de test
Select-String -Path "src/**/*.rs" -Pattern "#\[test\]|#\[cfg\(test\)\]|#\[quickcheck\]|mod tests"

# Compter les tests par fichier
Select-String -Path "src/**/*.rs" -Pattern "^\\s*fn (test_|check_)" | Group-Object -Property Filename

Cartographier la structure des tests :

  • Modules de test en ligne #[cfg(test)] mod tests { ... } dans les fichiers sources — méthodes privées accessibles ici
  • Fichiers de test d'intégration autonomes tests/*.rs
  • Modules de test conditionnels (#[cfg(feature = "...")])
  • Tests de propriétés quickcheck/proptest — ne jamais les supprimer
  • Tests de documentation (`/// ``` ... ````) — contribuent également à la couverture

Questions clés :

  • Y a-t-il plusieurs modules de test couvrant le même code ? (par exemple, fractions.rs::tests vs more_tests.rs)
  • Existe-t-il des tests « à l'ancienne » à côté de tests « complets » ?
  • Y a-t-il un test de fumée (it_works) qui duplique des assertions de tests ciblés ?
  • Quels paramètres de type générique sont exercés ?

Phase 1 : Détecter la redondance

Niveau 1 : Mêmes assertions avec des valeurs identiques

Deux tests sont redondants s'ils exercent la même fonction avec des valeurs d'entrée identiques ou strictement sous-ensemble :

// test_abs (redondant — sous-ensemble de test_fraction_abs)
#[test]
fn test_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // identique à ci-dessous
}

// test_fraction_abs (conserver — couvre plus)
#[test]
fn test_fraction_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // dupliqué
    assert_eq!(Fraction::new(3, -4).abs(), Fraction::new(3, 4));  // unique
}

Règle : Si les assertions du test A sont toutes contenues dans le test B, le test A est redondant. Supprimer A.

Niveau 2 : Même chemin de code avec des valeurs équivalentes

Deux tests sont redondants s'ils exercent le même chemin de fonction avec des entrées équivalentes :

// test_mul_div (redondant — englobé par test_fraction_arithmetic)
#[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 (conserver — couvre mul/div ET aussi add/sub/neg/reciprocal)
#[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));
}

Règle : Si le test A ne couvre que les opérations X et Y, mais que le test B couvre X, Y et Z, supprimer A.

Niveau 3 : Test dupliqué entre modules

La même fonction est testée dans deux modules de test séparés avec des scénarios qui se chevauchent :

// fractions.rs::tests::test_fraction_from_i32_to_i64 (redondant)
#[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 (conserver — identique, conserver le plus récent)
#[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));
}

Règle : Lorsque les tests se trouvent à la fois dans des modules en ligne et séparés, et qu'ils testent la même chose à l'identique, supprimer le doublon. En général, conserver celui du fichier de test dédié.

Niveau 4 : Test de fumée englobé par des tests ciblés

// it_works (test de fumée redondant — supprimer)
#[test]
fn it_works() {
    assert_eq!(const_gcd(4, -6), 2);              // couvert par test_const_gcd
    assert_eq!(Fraction::new(30, -40), Fraction::new(-3, 4));  // couvert par test_fraction_construction
    assert_eq!(Fraction::from(30), Fraction::new(30, 1));      // couvert par test_fraction_construction
    assert_eq!(Fraction::<i32>::default(), Fraction::new(0, 1)); // couvert par test_fraction_construction
}

Règle : Un test de fumée qui duplique des assertions de plusieurs tests ciblés doit être supprimé. Il n'ajoute aucune valeur de couverture.

NE JAMAIS supprimer

  • Les tests quickcheck/proptest — ils s'exécutent avec des entrées aléatoires et vérifient des invariants
  • Le seul test pour un type spécifique — même s'il semble similaire, il exerce une monomorphisation différente
  • Les tests qui sont les seuls à exercer un chemin de code — vérifier le rapport de couverture

Phase 2 : Construire la base de référence (AVANT la suppression)

# Générer la couverture de base
cargo llvm-cov --all-features --workspace --summary-only

# Sauvegarder également un rapport détaillé pour la comparaison ligne par ligne
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-before.lcov

Enregistrer les métriques de base :

  • Pourcentages de couverture des régions, lignes, fonctions
  • Nombre de régions manquées
  • Nombre total de tests : cargo test --all-features --workspace 2>&1 | Select-String "test result: ok. (\d+) passed"

Phase 3 : Supprimer les tests redondants

Supprimer chaque fonction de test redondante de son fichier à l'aide de modifications précises. Après chaque lot de suppression, exécuter :

cargo test --all-features --workspace

Si la compilation échoue en raison d'importations inutilisées (par exemple, use num_integer::gcd; qui n'était utilisé que par le test supprimé), les nettoyer.

Tâches de nettoyage courantes :

  • Supprimer les importations #[cfg(test)] use ...; qui n'étaient que pour les tests supprimés
  • Supprimer les use num_integer::gcd; au niveau du module si utilisées uniquement par le test de fumée supprimé
  • Supprimer le code de test commenté qui fait référence aux fonctions supprimées

Phase 4 : Reconstruire et collecter les métriques APRÈS DEDUP

cargo llvm-cov --all-features --workspace --summary-only
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-after-dedup.lcov

Critères de verdict pour DEDUP :

VérificationExigence
Tous les tests passentDoit passer
Couverture des lignes par fichierAucune diminution (l'augmentation est OK — moins de lignes de test dans le binaire = ratio de couverture plus élevé)
Nombre de régions manquéesDevrait diminuer ou rester identique (la suppression de code de test mort peut augmenter le ratio de couverture)
Aucun nouveau chemin de code non couvertComparer les entrées FNDA:0 dans lcov avant/après

Si la couverture baisse sur un fichier, annuler immédiatement — le test n'était PAS redondant.

Phase 5 : Analyser les lacunes de couverture (mode GAP-FILL)

Analyser le fichier lcov pour les fonctions non couvertes

Les lignes FNDA:0 dans le fichier lcov révèlent exactement quelles monomorphisations ne sont pas testées :

FNDA:0,_RNvMNt...INtB2_8FractionnE7new_rawB4_   # Fraction<type_n>::new_raw — non testé
FNDA:0,_RNvMs2_Nt...INtB5_8FractionpE11is_positiveB7_  # Fraction<type_p>::is_positive — non testé
FNDA:0,_RNvXs7_Nt...INtB5_8FractionlEINtNt...4FromhE4fromB7_  # From<i8> for Fraction — non testé

Schémas courants de lacunes dans Rust :

SchémaExempleCe qui manque
Monomorphisation non testéeFraction<i8> jamais instanciéeAjouter des tests avec Fraction::<i8>::new(...)
Implémentation de trait non testéeFrom<i8> for Fraction<i32>Ajouter un test Fraction::from(5i8)
Implémentation TryFrom non testéeTryFrom<Fraction<i64>> for Fraction<i32>Ajouter un test Fraction::<i32>::try_from(f_i64)
Méthodes privéessignum(), is_positive() non couvert pour i8Doit être testé depuis le même #[cfg(test)] mod tests
Code conditionnel#[cfg(feature = "serde")] non testéAjouter un test avec --features serde
Neg pour type spécifiqueImplémentation de Neg pour Fraction<i8>Ajouter -Fraction::<i8>::new(...)

Décoder les noms obscurs de lcov

Les noms obscurs Rust dans lcov indiquent les paramètres de type. Suffixes clés à rechercher :

  • NtB5_8FractionlE... — Fraction avec type l (communément i64)
  • NtB5_8FractionnE... — Fraction avec type n (communément i8)
  • NtB5_8FractionxE... — Fraction avec type x (communément i128)
  • NtB5_8FractionpE... — Fraction avec type p (communément i8 dans certaines constructions)

La correspondance exacte dépend de l'ordre de monomorphisation. Recouper avec les types réellement utilisés dans les tests :

  • i32 — presque toujours couvert (utilisé partout)
  • i64 — vérifier s'il est couvert (souvent via From<i32> for Fraction<i64>)
  • i8 — rarement couvert (nécessite un test explicite)
  • i16 — rarement couvert
  • i128 — peut être couvert via quickcheck
  • BigInt — couvert via des tests conditionnels

Phase 6 : Écrire des tests ciblés

Pour les monomorphisations non testées (i8, i16, etc.)

Écrire un test complet couvrant la construction, les setters, l'arithmétique, la comparaison et les valeurs spéciales :

#[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));
}

Pour les méthodes privées (doivent être dans le même module)

Les méthodes privées comme signum(), is_positive(), is_negative() ne peuvent être testées qu'à l'intérieur d'un bloc #[cfg(test)] mod tests dans le même fichier source où elles sont définies :

// Dans fractions.rs, à l'intérieur de #[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));
}

Pour les implémentations de traits non testées

#[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));
}

Règles pour les valeurs de test

  • Utiliser des valeurs petites et sûres pour les types étroits (par exemple, le max de i8 est 127 — éviter les débordements)
  • Tester à la fois les valeurs positives et négatives
  • Tester les cas limites : dénominateur zéro (= infini), zéro/zéro (= NaN), dénominateur avec signe opposé
  • Tester les opérations d'identité : -(-x) == x, x * 1 == x, x + 0 == x
  • Ne pas tester les méthodes privées à partir de fichiers de test séparés — elles ne compileront pas

Phase 7 : Reconstruire et collecter les métriques APRÈS GAP-FILL

cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace
cargo llvm-cov --all-features --workspace --summary-only

Phase 8 : Rapport d'amélioration de la couverture

| Métrique                | Avant    | Après    | Δ       |
|-------------------------|----------|----------|---------|
| Nombre de tests         | N        | N'       | +Δ      |
| Régions fractions.rs    | 93.76%   | 98.45%   | +4.69%  |
| Lignes fractions.rs     | 92.77%   | 98.97%   | +6.20%  |
| Fonctions fractions.rs  | 88.10%   | 98.67%   | +10.57% |
| Régions manquées        | 70       | 15       | -55     |

Tests redondants supprimés : 11
Lacunes de couverture comblées : Fraction<i8>, From<i8>, From<u8>, TryFrom<...>

Individual skills in this repo

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

Skills associés