Ajouter des fonctionnalités à l’aide du développement piloté par les tests
Maintenant que la logique de recherche est dans src/lib.rs, à part de la fonction main, il est bien plus facile d’écrire les tests pour les fonctionnalités de base de notre code. Nous pouvons appeler les fonctions directement avec différents arguments et vérifier les valeurs de retour sans avoir à appeler notre binaire dans la ligne de commande.
Dans cette section, nous allons ajouter la logique de recherche au programme minigrep en utilisant le processus de développement piloté par les tests (c’est le TDD : Test-Driven Development) avec les étapes suivantes :
- Écrivez un test qui échoue et lancez-le pour vous assurer qu’il va échouer pour la raison que vous attendiez.
- Écrivez ou modifiez juste assez de code pour faire réussir ce nouveau test.
- Remaniez le code que vous venez d’ajouter ou de changer pour vous assurer que les tests continuent à réussir.
- Recommencez à l’étape 1 !
Bien que ce processus n’est qu’une des différentes manières d’écrire des programmes, le TDD peut aussi aider à piloter sa conception. Écrire les tests avant d’écrire le code qui fait réussir les tests aide à maintenir une haute couverture de tests tout le long du processus.
Nous allons expérimenter cela avec l’implémentation de la fonctionnalité qui va rechercher la chaîne de caractères demandée dans le contenu du fichier et générer une liste de lignes qui correspond à cette recherche. Nous ajouterons cette fonctionnalité dans une fonction rechercher.
Écrire un test qui échoue
Comme nous n’en avons plus besoin, enlevons les instructions println! de src/lib.rs et src/main.rs que nous avions utilisé pour vérifier le bon comportement du programme. Ensuite, Dans src/lib.rs, nous allons ajouter un module tests avec une fonction de test, comme nous l’avions fait dans le chapitre 11. La fonction de test définit le comportement que nous voulons qu’ait la fonction rechercher : elle va prendre en arguments une recherche et le texte dans lequel rechercher, et elle va retourner seulement les lignes du texte qui correspondent à la recherche. L’encart 12-15 montre ce test.
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// -- partie masquée ici --
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_resultat() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
}
search function for the functionality we wish we hadCe test recherche la chaîne de caractères "duct". Le texte dans lequel nous recherchons fait trois lignes, et seulement une d’entre elles contient "duct" (remarquez que la barre oblique inverse après le double guillemet ouvrant indique à Rust de ne pas insérer un caractère de nouvelle ligne au début du contenu de ce littéral de chaîne de caractère). Nous vérifions que la valeur retournée par la fonction rechercher contient seulement la ligne que nous avions prévue.
Si nous exécutons ce test, il va échouer pour le moment car la macro unimplemented! panique avec le message “not implemented”. En suivant les principes TDD, nous allons ajouter juste assez de code pour que le test puisse ne pas paniquer lors de l’appel de la fonction, en définissant la fonction rechercher de manière à ce qu’elle renvoie toujours un vecteur vide, comme dans l’encart 12-16. Ensuite, le test devrait compiler et échouer car un vecteur vide ne correspond pas au vecteur qui contient la ligne "sécurité, rapidité, productivité."
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_resultat() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
}
search function so that calling it won’t panicVoyons maintenant pourquoi nous avons besoin de définir explicitement une durée de vie 'a dans la signature de rechercher et de l’utiliser sur l’argument contenu ainsi que la valeur de retour. Rappelez-vous que dans le chapitre 10 nous avions vu que le paramètre de durée de vie indique quelle durée de vie d’argument est connectée à la durée de vie de la valeur de retour. Dans notre cas, nous indiquons que le vecteur retourné devrait contenir des slices de chaînes de caractères qui proviennent des slices de l’argument contenu (et pas de l’argument recherche).
Autrement dit, nous disons à Rust que les données retournées par la fonction rechercher vont vivre aussi longtemps que la donnée dans l’argument contenu de la fonction rechercher. C’est très important ! Les données sur lesquelles pointent les slices doivent toujours être en vigueur pour que la référence reste valide ; si le compilateur croit que nous créons des slices de recherche plutôt que de contenu, ses vérifications de sécurité seront incorrectes.
Si nous oublions les annotations de durée de vie et que nous essayons de compiler cette fonction, nous allons obtenir cette erreur :
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | pub fn rechercher(recherche: &str, contenu: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
1 | pub fn rechercher<'a>(recherche: &'a str, contenu: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust ne peut pas deviner lequel des deux paramètres nous allons utiliser, donc nous devons lui dire explicitement. Notez que le texte d’aide suggère de préciser la durée de vie pour tous les paramètres et le type de retour, ce qui est incorrect ! Comme contenu est le paramètre qui contient tout notre texte et que nous voulons retourner des extraits de ce texte qui correspondent à la recherche, nous savons que contenu est le seul paramètre qui doit être connecté à la valeur de retour, en utilisant la syntaxe de durée de vie.
Les autres langages de programmation n’ont pas besoin que vous connectiez les arguments aux valeurs de retour dans la signature, mais cette pratique deviendra plus facile au fil du temps. Vous pouvez comparer cet exemple aux exemples de la section “La conformité des références avec les durées de vies” du chapitre 10.
Écrire du code pour réussir au test
Pour le moment, notre test échoue car nous retournons toujours un vecteur vide. Pour corriger cela et implémenter rechercher, notre programme doit suivre les étapes suivantes :
- Itérer sur chacune des lignes de
contenu. - Vérifier si la ligne contient la chaîne de caractères recherchée.
- Si c’est le cas, l’ajouter à la liste des valeurs que nous retournerons.
- Si ce n’est pas le cas, ne rien faire.
- Retourner la liste des résultats qui ont été trouvés.
Travaillons sur chacune de ces étapes, en commençant par l’itération sur les lignes.
Itérer sur chacune des lignes avec la méthode lines
Rust a une méthode très pratique pour gérer l’itération ligne-par-ligne des chaînes de caractères, judicieusement appelée lines, qui fonctionne comme dans l’encart 12-17. Notez que cela ne se compile pas encore.
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
for ligne in contenu.lines() {
// faire quelquechose avec ligne ici
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_resultat() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
}
contentsLa méthode lines retourne un itérateur. Nous verrons plus tard les itérateurs dans le chapitre 13, mais souvenez-vous que vous avez vu cette façon d’utiliser un itérateur dans l’encart 3-5, dans lequel nous avions utilisé une boucle for sur un itérateur pour exécuter du code sur chaque élément d’une collection.
Trouver chaque ligne correspondant à la recherche
Ensuite, nous allons vérifier que la ligne courante contient la chaîne de caractères que nous recherchons. Heureusement, les chaînes de caractères ont une méthode contains assez pratique qui fait cela pour nous ! Ajoutez l’appel à la méthode contains dans la fonction rechercher, comme dans l’encart 12-18. Notez qu’ici non plus nous ne pouvons pas encore compiler.
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
for ligne in contenu.lines() {
if ligne.contains(recherche) {
// faire quelquechose avec la ligne ici
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_resultat() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
}
queryPour le moment, nous construisions des fonctionnalités. Pour que le code puisse se compiler, nous devons retourner une valeur depuis le corps de la fonction, comme nous l’avons indiqué dans la signature de la fonction.
Stocker les lignes trouvées
Pour terminer cette fonction, nous avons aussi besoin d’un moyen de stocker les lignes qui contiennent la chaîne de caractères que nous recherchons. Pour cela, nous pouvons créer un vecteur mutable avant la boucle for et appeler la méthode push pour enregistrer la ligne dans le vecteur. Après la boucle for, nous retournons le vecteur, comme dans l’encart 12-19 :
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
let mut resultats = Vec::new();
for ligne in contenu.lines() {
if ligne.contains(recherche) {
resultats.push(ligne);
}
}
resultats
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_resultat() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
}
Maintenant, notre fonction rechercher retourne uniquement les lignes qui contiennent recherche, et notre test devrait réussir. Exécutons le test :
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Notre test a réussi, donc nous savons que cela fonctionne !
Arrivés à ce stade, nous pourrions envisager des pistes de remaniement pour l’implémentation de la fonction de recherche tout en faisant en sorte que les tests réussissent toujours afin de conserver les mêmes fonctionnalités. Le code de la fonction de recherche n’est pas mauvais, mais il ne profite pas de quelques fonctionnalités utiles des itérateurs. Nous retrouverons cet exemple dans le chapitre 13, dans lequel nous explorerons les itérateurs en détail, et ainsi découvrir comment nous pourrions l’améliorer.
Maintenant, l’intégralité du programme devrait fonctionner ! Essayons-le, pour commencer avec un mot qui devrait retourner exactement une seule ligne du poème d’Emily Dickinson, “frog” :
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
Super ! Maintenant, essayons un mot qui devrait retourner plusieurs lignes, comme “body” :
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
Et enfin, assurons-nous que nous n’obtenons aucune ligne lorsque nous cherchons un mot qui n’est nulle part dans le poème, comme “monomorphization” :
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
Très bien ! Nous avons construit notre propre mini-version d’un outil classique et nous avons beaucoup appris sur la façon de structurer nos applications. Nous en avons aussi appris un peu sur les entrées et sorties des fichiers, les durées de vie, les tests et l’interprétation de la ligne de commande.
Pour clôturer ce projet, nous allons brièvement voir comment travailler avec les variables d’environnement et comment écrire sur la sortie standard des erreurs, ce qui peut s’avérer utile lorsque vous écrivez des programmes en ligne de commande.