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épendances

Doc-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 benchmarks

8.5 Résumé

Type de testCommande
Unitaire#[cfg(test)] mod tests dans chaque fichiercargo test
Intégrationtests/*.rscargo test
Doc-testDans /// docstringscargo test --doc
Benchmarkbenches/*.rscargo bench
Test spécifiquecargo 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

🔗 Voir aussi