8. Tests et Documentation
Rust a un système de tests intégré au langage et au compilateur. Pas besoin de framework externe pour les tests unitaires et d’intégration. La documentation est également testée — les exemples dans les docstrings sont exécutés à chaque
cargo test.
8.1 Tests Unitaires
Attribut #[test]
// Dans n'importe quel fichier .rs
#[cfg(test)] // compilé uniquement pendant les tests
mod tests {
use super::*;
#[test]
fn test_median_basic() {
let grads = vec![
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
vec![7.0, 8.0, 9.0],
];
let result = median(&grads);
assert_eq!(result, vec![4.0, 5.0, 6.0]);
}
#[test]
fn test_median_empty() {
let result = median(&[]);
assert!(result.is_none());
}
#[test]
fn test_trimmed_mean_single() {
let grads = vec![vec![42.0]];
let result = trimmed_mean(&grads, 0.1);
assert_eq!(result, Some(vec![42.0]));
}
}Macros d’assertion
#[test]
fn test_gar_trait() {
let median = Median;
let grads = vec![vec![1.0], vec![2.0], vec![10.0]];
// Assertions classiques
assert_eq!(median.name(), "Median");
assert_ne!(median.breakdown_point(), 0.0);
// Avec message personnalisé
let result = median.aggregate(&grads).unwrap();
assert!(result[0] < 10.0, "la médiane devrait être < 10, obtenu {}", result[0]);
// Tolérance pour flottants
let expected = 2.0;
assert!((result[0] - expected).abs() < 1e-6, "attendu {expected}, obtenu {}", result[0]);
}should_panic
#[test]
#[should_panic(expected = "dimension mismatch")]
fn test_krum_wrong_dimension() {
let krum = Krum::new(1);
krum.aggregate(&[
vec![1.0, 2.0],
vec![3.0], // dimension différente
]).unwrap();
}Tests paramétrés avec #[rstest]
use rstest::rstest;
#[rstest]
#[case(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], vec![7.0, 8.0, 9.0], vec![4.0, 5.0, 6.0])]
#[case(vec![10.0], vec![20.0], vec![30.0], vec![20.0])]
fn test_median_cases(
#[case] g1: Vec<f64>,
#[case] g2: Vec<f64>,
#[case] g3: Vec<f64>,
#[case] expected: Vec<f64>,
) {
let result = median(&[g1, g2, g3]).unwrap();
assert_eq!(result, expected);
}8.2 Tests d’Intégration
Les tests d’intégration se trouvent dans le dossier tests/ à la racine du projet. Chaque fichier .rs est compilé comme un crate séparé.
// tests/test_gar.rs — teste le crate comme un utilisateur externe
use gradient_core::gar::{Median, Krum, Gar};
#[test]
fn test_median_integration() {
let median = Median;
let grads = vec![
vec![1.0, 5.0, 3.0],
vec![2.0, 2.0, 8.0],
vec![9.0, 1.0, 4.0],
vec![4.0, 7.0, 2.0],
];
let result = median.aggregate(&grads).unwrap();
assert_eq!(result.len(), 3);
// Toutes les valeurs devraient être entre 1 et 9
assert!(result.iter().all(|&x| x >= 1.0 && x <= 9.0));
}
#[test]
fn test_krum_with_byzantine() {
let krum = Krum::new(1);
let honest = vec![
vec![1.0, 2.0],
vec![1.1, 2.1],
vec![0.9, 1.9],
];
let byzantine = vec![vec![1000.0, -1000.0]];
let mut all = honest.clone();
all.extend(byzantine);
let result = krum.aggregate(&all).unwrap();
// Krum devrait sélectionner un gradient honnête, pas le byzantin
assert!((result[0] - 1.0).abs() < 0.2, "Krum a sélectionné le byzantin: {:?}", result);
}Sous-modules de test
tests/
├── test_gar.rs
├── test_attack.rs
└── common/
└── mod.rs // helpers partagés entre tests
// tests/common/mod.rs
pub fn make_iid_gradients(n: usize, d: usize) -> Vec<Vec<f64>> {
(0..n).map(|_| {
(0..d).map(|_| rand::random::<f64>()).collect()
}).collect()
}
// utilisation dans test_gar.rs
mod common;
#[test]
fn test_median_large() {
let grads = common::make_iid_gradients(100, 1000);
let median = Median;
let result = median.aggregate(&grads).unwrap();
assert_eq!(result.len(), 1000);
}8.3 Documentation
Docstrings ///
/// Calcule la médiane coordonnée par coordonnée d'un ensemble de gradients.
///
/// Pour chaque dimension j, on trie les valeurs des n workers et on prend
/// la médiane. La médiane a un point de rupture de 50%.
///
/// # Arguments
///
/// * `gradients` — Slice de vecteurs, chaque vecteur est le gradient d'un worker.
/// Tous les vecteurs doivent avoir la même dimension.
///
/// # Returns
///
/// * `Some(Vec<f64>)` — Le gradient agrégé
/// * `None` — Si la liste est vide
///
/// # Exemple
///
/// ```
/// use gradient_core::gar::median::median;
///
/// let grads = vec![
/// vec![1.0, 5.0],
/// vec![2.0, 3.0],
/// vec![9.0, 4.0],
/// ];
/// let result = median(&grads).unwrap();
/// assert_eq!(result, vec![2.0, 4.0]);
/// ```
pub fn median(gradients: &[Vec<f64>]) -> Option<Vec<f64>> {
if gradients.is_empty() {
return None;
}
// ... implémentation
}Documentation de module //!
//! # Gradient Core
//!
//! Librairie d'agrégation robuste de gradients pour l'apprentissage
//! distribué byzantin. Implémente les GAR classiques :
//!
//! - **Median** — Médiane coordonnée par coordonnée (point de rupture 50%)
//! - **Krum** — Sélection par distance minimale (point de rupture 50%)
//! - **TrimmedMean** — Moyenne tronquée (point de rupture contrôlé par α)
//!
//! ## Exemple
//!
//! ```rust
//! use gradient_core::gar::{Median, Gar};
//!
//! let median = Median;
//! let grads = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
//! let result = median.aggregate(&grads).unwrap();
//! println!("{:?}", result);
//! ```
pub mod gar;
pub mod attack;Générer la documentation
cargo doc # génère target/doc/
cargo doc --open # génère + ouvre dans le navigateur
cargo doc --no-deps # documentation sans les dépendancesDoc-tests — les exemples sont des tests
Les exemples dans les docstrings sont exécutés par cargo test :
cargo test # exécute aussi tous les exemples de doc
cargo test --doc # seulement les doc-tests/// ```
/// // Cet exemple est testé automatiquement
/// let x = 2 + 2;
/// assert_eq!(x, 4);
/// ```
/// ```rust,ignore
/// // Cet exemple ne sera pas testé
/// let x = compile_error!("oups");
/// ```
/// ```rust,compile_fail
/// // On s'attend à ce que ce code ne compile PAS
/// let x: i32 = "hello";
/// ```8.4 Benchmarks
Benchmarks natifs (nightly)
// benches/my_bench.rs
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_median_100_workers(b: &mut Bencher) {
let grads: Vec<Vec<f64>> = (0..100)
.map(|_| (0..1000).map(|_| rand::random()).collect())
.collect();
b.iter(|| {
median(&grads)
});
}Avec criterion (recommandé, stable)
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "gar_bench"
harness = false// benches/gar_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench_median_10k(c: &mut Criterion) {
let grads: Vec<Vec<f64>> = (0..50)
.map(|_| (0..10_000).map(|_| rand::random()).collect())
.collect();
c.bench_function("median 50×10k", |b| {
b.iter(|| median(black_box(&grads)))
});
}
fn bench_krum_10k(c: &mut Criterion) {
let grads: Vec<Vec<f64>> = (0..20)
.map(|_| (0..1_000).map(|_| rand::random()).collect())
.collect();
c.bench_function("krum 20×1k", |b| {
b.iter(|| Krum::new(5).aggregate(black_box(&grads)))
});
}
criterion_group!(benches, bench_median_10k, bench_krum_10k);
criterion_main!(benches);cargo bench # lance les benchmarks8.5 Résumé
| Type de test | Où | Commande |
|---|---|---|
| Unitaire | #[cfg(test)] mod tests dans chaque fichier | cargo test |
| Intégration | tests/*.rs | cargo test |
| Doc-test | Dans /// docstrings | cargo test --doc |
| Benchmark | benches/*.rs | cargo bench |
| Test spécifique | — | cargo test test_median |
| Test (pattern) | — | cargo test median (tous les tests avec “median”) |
# Commandes utiles
cargo test # tout
cargo test -- --nocapture # voir les println! dans les tests
cargo test -- --test-threads=1 # séquentiel (utile pour tests d'isolation)
cargo test --release # tests en mode release
cargo clippy --tests # linter les tests aussi