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 :
-
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). -
GAP-FILL — Après la déduplication, analyser le rapport
llvm-covpour 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_abstestant 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::testsvsmore_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érification | Exigence |
|---|---|
| Tous les tests passent | Doit passer |
| Couverture des lignes par fichier | Aucune diminution (l'augmentation est OK — moins de lignes de test dans le binaire = ratio de couverture plus élevé) |
| Nombre de régions manquées | Devrait diminuer ou rester identique (la suppression de code de test mort peut augmenter le ratio de couverture) |
| Aucun nouveau chemin de code non couvert | Comparer 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éma | Exemple | Ce qui manque |
|---|---|---|
| Monomorphisation non testée | Fraction<i8> jamais instanciée | Ajouter des tests avec Fraction::<i8>::new(...) |
| Implémentation de trait non testée | From<i8> for Fraction<i32> | Ajouter un test Fraction::from(5i8) |
| Implémentation TryFrom non testée | TryFrom<Fraction<i64>> for Fraction<i32> | Ajouter un test Fraction::<i32>::try_from(f_i64) |
| Méthodes privées | signum(), is_positive() non couvert pour i8 | Doit ê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écifique | Implé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 typel(communémenti64)NtB5_8FractionnE...— Fraction avec typen(communémenti8)NtB5_8FractionxE...— Fraction avec typex(communémenti128)NtB5_8FractionpE...— Fraction avec typep(communémenti8dans 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 viaFrom<i32> for Fraction<i64>)i8— rarement couvert (nécessite un test explicite)i16— rarement couverti128— peut être couvert via quickcheckBigInt— 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
i8est 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<...>