Discussion du code tooling

Concepts

Les concepts abordés dans cet exemple sont:

  1. La documentation.
  2. Les tests.
  3. Les outils en plus du compilateur

Discussion

Le Rust étant un langage moderne, il vient avec tout un tas de features qui sont très appréciables pour écrire du code robuste, propre et réutilisable. On va voir quelle est la syntaxe nécessaire pour écrire documentation et tests.

La documentation

Il y a différents moyens de documenter un code. Pour un guide bien plus complet que ce qui est résumé ici, vous pouvez vous référer à ce site.

Les commentaires

Le plus simple est d'y ajouter des commentaires. En Rust, tous caractères qui suivent des // sur la même ligne sont considérés comme un commentaire (ignorés par le compilateur)

#![allow(unused)]
fn main() {
// Ceci est un commentaire
// Et ceci un autre
let a = 2; // Ici on assigne 2 à a
}

On peut écrire également des commentaires sur plusieurs lignes sans avoir à mettre des // sur chacune. Pour ce faire on utilise la syntaxe /* ... */

#![allow(unused)]
fn main() {
/* 
 Ceci est un commentaire
 Et ceci un autre
 Et en voici un dernier
*/
let a = 2; /* Ici on assigne 2 à a */
}

Ce type de documentation se prête très bien à des commentaires sur les détails du code, mais n'est pas très adapté à écrire une documentation plus générale sur le comportement du code. Ainsi, on a autre type de commentaires, qui seront utilisés pour générer automatiquement de la documentation à l'aide de l'outil rustdoc.

La documentation par composants

La documentation d'un composant, que ça soit une struct, un enum, une fonction, etc. se fait en préfixant /// devant la ligne de documentation et la plaçant directement au dessus du composant à commenter. Ainsi les exemples suivants permettent de:

  • Documenter une constante
/// The SIZE constant allows to use statically sized arrays
const SIZE: usize = 9;
  • Documenter une fonction
/// Poorly emulates the parsing of a command line.
pub fn read_command_line() -> [i32; crate::SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
}
  • Documenter une fonction statique
impl<T: std::fmt::Display> SomethingOrNothing<T> {
    /// A static function that prints the content of a SomethingOrNothing.
    pub fn print(&self) {
        match self {
            SomethingOrNothing::Nothing => println!("Nothing."),
            SomethingOrNothing::Something(val) => println!("Something is: {}", val),
        }
    }
}
  • Documenter un trait
/// The [Minimum] trait computes the minimum value between two values of a type
pub trait Minimum: Copy {
    fn min(self, rhs: Self) -> Self;
}
  • Documenter un type énuméré et ses variantes
/// An generic enumerated type that has two variants.
///
/// - Nothing
/// - Something
#[derive(Clone, Copy)]
pub enum SomethingOrNothing<T> {
    /// A [SomethingOrNothing::Nothing]
    Nothing,
    /// A [SomethingOrNothing::Something] encapsulating a T
    Something(T),
}

La documentation d'une crate et des modules

Une librairie est appelée crate Rust. Afin de donner des informations de haut niveau sur le fonctionnement d'une librairie, on peut utiliser une syntaxe spéciale //! à mettre au début de chaque ligne de documentation (on peut également utiliser la syntaxe /*! ... */), comme ci-dessous

//! This is an example of Rust crate comments (or inner comments).
//! They will be rendered in the front page of your (crate) library.
//!
//! # How to generate the documentation
//!
//! In this program we wrote an algorithm that computes the minimum of
//! a sequence of integers.
//!
//! To create the documentation run the command
//! ```bash
//! cargo doc
//! ```
//! The obtain documentation can be found in the `target/doc/tooling/index.html` directory
//!
//! To view the documentation type
//! ```bash
//! cargo doc --open
//! ```
//! which will open the browser and show you the documentation.
//!
//! The documentation supports the CommonMarkdown syntax.
//!
//! Below we will use the `///` comments that will comment the code directly below.
//! We can also sue `//` but they will not be rendered.
//! All the lines written here could be enclosed in `/*! ... */` instead of being prefixed by `//!`.
//!
//! For more informations about writing documentation [follow that link](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html).
//!
//! # Tooling
//!
//! Also Rust comes with great tooling.
//! - [Clippy](https://doc.rust-lang.org/stable/clippy/): The officiel Rust linter.
//! - [Rustfmt](https://github.com/rust-lang/rustfmt): The official Rust code formatter.

Cette documentation se met dans le fichier lib.rs qui est également l'endroit où on importe les différents modules.

On peut également documenter les modules individuellement. Pour ce faire, il faut utiliser la même syntaxe que pour la documentation de la crate, mais mettre cette documentation au début du fichier contenant chaque module comme par exemple au début du fichier io.rs

//! Contains functions to interact with the user, either
//! by reading inputs from the terminal, either by writing values
//! in it.

Markdown

La documentation supporte la syntaxe du Common Markdown comme on peut le voir dans le code ci-dessus. On a en particulier la possibilité de mettre des titres avec des # ou du code avec des code fences. Il est également possible de mettre des liens vers d'autres parties de la documentation (avec les annotations tu type [MyStruct]) ce qui fait de la syntaxe un outil très puissant et intégré fortement au processus de développement.

Génération de la documentation

Tout ce travail d'annotation du code source permet d'utiliser rustdoc qui est un outil puissant de génération de documentation sous la forme principal d'un site web. Dans le répertoire où se trouve le fichier Cargo.toml, on peut exécuter la commande

cargo doc

et cela va générer la documentation dans un sous répertoire du projet. On peut également automatiquement ouvrir la documentation dans un navigateur à l'aide de la commande

cargo doc --open

Les tests

La documentation aide grandement à la (ré-)utilisation d'une librairie et à son développement (collaboratif ou non). Durant le processus de développement, il est également très utile (et important) d'écrire des tests pour le code. Rust propose un framework de tests totalement intégré au langage. On peut ainsi très facilement écrire des fonctions de test en ajoutant l'annotation #[test] directement au dessus de n'importe quelle fonction

#[test]
fn test_creation() {
    use something_or_nothing::SomethingOrNothing;

    let n1: SomethingOrNothing<i32> = SomethingOrNothing::default();
    assert!(n1 == SomethingOrNothing::Nothing);
    let n2: SomethingOrNothing<i32> = SomethingOrNothing::Something(1);
    assert!(n2 == SomethingOrNothing::Something(1));
}

La fonction test_creation() sera automatiquement appelée lors de l'appel à la commande

cargo test

Aucune fonction non annotée par #[test] n'est appelée quand on exécute cargo test. Si la fonction se termine sans erreur, le test est réussi (il est échoué si la fonction s'arrête en cours de route). On voit dans la fonction test_creation() l'appel à la macro assert!() qui prend en argument une expression booléenne, ici n1 == SomethingOrNothing::Nothing dans le premier appel. Si l'expression prend la valeur true, le code continue normalement son exécution, sinon il fait appel à la macro panic!()! et l'exécution est interrompue et un code d'erreur est retourné par le programme.

Dans l'appel à n1 == SomethingOrNothing::Nothing, on constate qu'on a besoin de vérifier si la valeur n1 est égale à SomethingOrNothing::Nothing. L'opérateur == n'est pas implémenté pour les types complexes, ainsi le code suivant ne compile pas

enum SomethingOrNothing<T> {
    Nothing,
    Something(T),
}
fn main() {
    let n1 = SomethingOrNothing::<i32>::Nothing;
    let b = n1 == SomethingOrNothing::<i32>::Nothing;
}

et nous donne un message d'erreur incluant

note: an implementation of `PartialEq<_>` might be missing for `SomethingOrNothing<i32>`

Ainsi, on doit implémenter le trait PartialEq pour le type SomethingOrNothing<T>, qui permet de tester l'égalité entre deux instances d'un type

/// An generic enumerated type that has two variants.
///
/// - Nothing
/// - Something
#[derive(Clone, Copy)]
pub enum SomethingOrNothing<T> {
    /// A [SomethingOrNothing::Nothing]
    Nothing,
    /// A [SomethingOrNothing::Something] encapsulating a T
    Something(T),
}
/// Implementation of the [PartialEq] trait that is useful for tests.
impl<T: PartialEq> PartialEq for SomethingOrNothing<T> {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (SomethingOrNothing::Nothing, SomethingOrNothing::Nothing) => true,
            (SomethingOrNothing::Something(lhs), SomethingOrNothing::Something(rhs)) => {
                *lhs == *rhs
            }
            _ => false,
        }
    }
}
fn main() {
    let n1 = SomethingOrNothing::<i32>::Nothing;
    assert!(n1 == SomethingOrNothing::<i32>::Nothing);
}

On constate que dans notre implémentation, il est nécessaire que T implémente également le trait PartialEq. Ainsi, deux Nothing sont égaux, un Nothing et un Something sont différents, et seulement quand les deux valeurs encapsulées dans deux Something sont égales alors nous avons égalité.

On peut également vouloir construire des tests qui échouent comme dans l'exemple ci-dessous

    #[test]
    #[should_panic]
    fn test_failure_creation() {
        let n2: SomethingOrNothing<i32> = SomethingOrNothing::Something(1);
        assert!(n2 == SomethingOrNothing::Nothing);
        assert!(n2 == SomethingOrNothing::Something(2));
    }

où on a annoté le test avec un #[should_panic]. Ce test, bien qu'il panique, sera considéré comme réussi. Il faut néanmoins rester prudent avec ce type de test. Rien ne garantit que la fonction de test a paniqué au moment espéré. Le code pourrait tout à fait paniquer pour une raison autre que celle attendue. Cela est particulièrement vrai si le test est complexe.

Finalement, il y a également la possibilité de regrouper les tests comme ci-dessous (dans le fichier lib.rs dans cet exemple)

#[cfg(test)]
mod tests {
    use crate::minimum::Minimum;
    use crate::something_or_nothing::{find_min, SomethingOrNothing};

    #[test]
    #[should_panic]
    fn test_failure_creation() {
        let n2: SomethingOrNothing<i32> = SomethingOrNothing::Something(1);
        assert!(n2 == SomethingOrNothing::Nothing);
        assert!(n2 == SomethingOrNothing::Something(2));
    }

    #[test]
    fn test_min() {
        let a = [1, 5, -1, 2, 0, 10, 11, 0, 3];
        let min = find_min(&a);
        assert!(min == SomethingOrNothing::Something(-1));
    }

    #[test]
    fn test_min_something_or_nothing() {
        let x = SomethingOrNothing::Something(5i32);
        let y = SomethingOrNothing::Something(10i32);
        let z = SomethingOrNothing::Nothing;
        assert!(x.min(y) == x);
        assert!(y.min(x) == x);
        assert!(z.min(y) == y);
        assert!(y.min(z) == y);
        assert!(z.min(z) == z);
    }
}

Pour ce faire, il faut créer un module, (ici mod tests) et l'annoter avec une configuration spéciale #[cfg(test)]. Cela permet de séparer les tests totalement du reste du code et devoir importer les différentes implémentations.

Il est également possible de répartir les tests dans différents modules comme dans minimum.rs par exemple en plus de lib.rs

#[cfg(test)]
mod tests {
    use crate::minimum::Minimum;

    #[test]
    fn test_min_i32() {
        let x = 5;
        let y = 10;
        assert_eq!(Minimum::min(x, y), x);
        assert_eq!(Minimum::min(y, x), x);
        assert_eq!(Minimum::min(x, x), x);
        assert_eq!(Minimum::min(y, y), y);
    }
}

Tests de documentation

Une bonne documentation inclut des exemples de code. Mais il n'y a rien de pire que des exemples faux ou qui ne marchent pas. Ainsi, Rust offre la possibilité de compiler et exécuter le code inclut dans la documentation: ces tests sont des tests de documentation. En effet, le code entre "code fences" markdown sera compilé et exécuté (à moins qu'il soit annoté ignore où il sera pas compilé et no_run où il sera pas exécuté). Il y a deux exemples de tests de documentation dans notre code. Le premier concerne l'implémentation du trait Default pour SomethingOrNothing qui permet de construire une instance par défaut qui sera la variante Nothing (voir something_or_nothing.rs).

/// Implementation of the [Default] trait that creates a [SomethingOrNothing]
/// that is a `Nothing` variant.
///
/// # Example
///
/// ```
/// # use tooling::something_or_nothing::SomethingOrNothing;
/// # fn main() {
/// let def: SomethingOrNothing<i32> = SomethingOrNothing::default();
/// assert!(def == SomethingOrNothing::Nothing);
/// # }
/// ```
impl<T> Default for SomethingOrNothing<T> {
    /// By Default a [SomethingOrNothing] is a nothing.
    fn default() -> Self {
        SomethingOrNothing::Nothing
    }
}

On voit que pour que le test puisse compiler et s'exécuter il est nécessaire d'importer les bons modules/fonctions. Ici on importe explicitement tooling::something_or_nothing::SomethingOrNothing et on met les fonctions à exécuter dans un main. Pour que ce "bruit" n'apparaisse pas dans la documentation, on préfixe les lignes par des #. Ainsi ces lignes sont lues par le compilateur pour les tests de documentation mais sont ignorées lors du rendu de la documentation.

Il y a également un exemple sur l'utilisation de la fonction find_min() (toujours dans something_or_nothing.rs)

/// Computes the minimum of an Array of a type T which implements the [Minimum] trait.
/// Returns a [SomethingOrNothing::Something] containing the the minimum value
/// or [SomethingOrNothing::Nothing] if no minimum value was found.
///
/// # Example
///
/// ```
/// # use tooling::something_or_nothing::{SomethingOrNothing, find_min};
/// # fn main() {
/// let tab = [10, 32, 12, 43, 52, 53, 83, 2, 9];
/// let min = find_min(&tab);
/// assert!(min == SomethingOrNothing::Something(2));
/// # }
/// ```
pub fn find_min<T: Minimum>(tab: &[T; crate::SIZE]) -> SomethingOrNothing<T> {
    let mut minimum = SomethingOrNothing::Nothing;
    // Here is T is Copyable. Which means that t is not moved in the loop
    for t in tab {
        minimum = minimum.min(SomethingOrNothing::Something(*t));
    }
    minimum
}

Finalement, les tests de documentation peuvent également être mis dans les documentation de module comme dans minimum.rs

//! Contains a generic trait implementation for computing the minimum between two
//! values. It is the equivalent of the `<` operator.
//!
//! # Examples
//!
//! For integers this would look like
//!
//! ```
//! # use tooling::minimum::Minimum;
//! let one = 1;
//! let two = 2;
//! assert!(Minimum::min(one, two) == one);
//! ```

Rapport sur l'exécution des tests

Lors de l'appel à cargo test tous les tests sont exécutés et un rapport est généré. Sur ce code, on obtient

$ cargo  est                                                                                               
   Compiling tooling v0.1.0 (/home/orestis/git/projects/rust-101/codes/rust_lang/tooling)
    Finished test [unoptimized + debuginfo] target(s) in 0.37s
     Running unittests src/lib.rs (target/debug/deps/tooling-f12750c4987ae624)

running 5 tests
test test_creation ... ok
test minimum::tests::test_min_i32 ... ok
test tests::test_min ... ok
test tests::test_failure_creation - should panic ... ok
test tests::test_min_something_or_nothing ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/tooling-b63e62707c6aab7d)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests tooling

running 3 tests
test src/minimum.rs - minimum (line 8) ... ok
test src/something_or_nothing.rs - something_or_nothing::SomethingOrNothing<T> (line 38) ... ok
test src/something_or_nothing.rs - something_or_nothing::find_min (line 95) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s

Les outils en plus du compilateur

La chaîne de compilation Rust vient avec deux autres outils très pratiques (en plus de cargo et rustdoc): rustfmt un formatteur de code, et clippy un linter.

rustfmt

Rust a fait le choix fasciste de définir des tas de conventions pour le nommage des variables, des types, etc. L'outil rustfmt permet de formatter automatiquement le code pour avoir un style uniforme au travers de tout votre code et ainsi améliorer sa lisibilité.

fn main() {
    const size: usize = 9;
    let tab: [i32; size] = [10, 32, 12, 
    43, 52, 53, 83, 2, 9];
    
    if (size == 0)
    {
        panic!("Size is of tab = 0.");
    }

    println!("Among the numbers in the list:");
    for i in 0..size {
        print!("{} ", tab[i]);
    }
    println!();

    let mut MinTab=tab[0];
    for i in 1..size 
    {
        if MinTab > tab[i] 
        {
            MinTab=tab[i];
        }
    }



    println!("The minimal value is: {}", MinTab);
}

Afin de le voir à l'oeuvre copier le code ci-dessus dans un src/main.rs et utilisez la commande rustfmt --check src/main.rs et observez le résultat. Par défaut rustfmt va modifier le code, ainsi l'option --check va uniquement montrer quelles sont les choses à corriger. En général, rustfmt est intégré avec les plugins dans des éditeurs de code tels que codium qui l'intègre dans le plugin rust-analyzer par exemple.

clippy

L'outil clippy est un détecteur automatique de mauvaise pratiques de codage, telles que définies par la communauté du Rust. Il permet d'écrire du code le plus idiomatique possible et en général d'éviter certaines mauvaises pratiques et/ou de simplifier le code avec des patterns connus.

Comme ci-dessus, prenez ce code et exécutez la commande cargo clippy pour voir les recommandations du linter.

Rustlings

Les rustlings à faire dans ce chapitre sont les suivants:

Les modules

$ rustlings run tests1
$ rustlings run tests2
$ rustlings run tests3
$ rustlings run tests4

Clippy

$ rustlings run clippy1
$ rustlings run clippy2
$ rustlings run clippy3