Discussion du code tooling
Concepts
Les concepts abordés dans cet exemple sont:
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)
On peut écrire également des commentaires sur plusieurs lignes sans avoir à mettre des //
sur chacune. Pour ce faire on utilise la syntaxe /* ... */
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