Ajouter des fonctionnalités avec le développement piloté par les tests
Maintenant que la logique de recherche se trouve dans src/lib.rs, séparée de la fonction main, il est beaucoup plus facile d’écrire des tests pour les fonctionnalités principales de notre code. Nous pouvons appeler les fonctions directement avec divers arguments et vérifier les valeurs de retour sans avoir à appeler notre binaire depuis 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 (TDD) avec les étapes suivantes : 1. Écrire un test qui échoue et l’exécuter pour s’assurer qu’il échoue pour la raison attendue. 2. Écrire ou modifier juste assez de code pour faire passer le nouveau test. 3. Refactoriser le code que vous venez d’ajouter ou de modifier et vous assurer que les tests continuent de passer. 4. Recommencer à l’étape 1 !
- Écrire un test qui échoue et l’exécuter pour s’assurer qu’il échoue pour la raison attendue.
- Écrire ou modifier juste assez de code pour faire passer le nouveau test.
- Refactoriser le code que vous venez d’ajouter ou de modifier et vous assurer que les tests continuent de passer.
- Recommencer à l’étape 1 !
Bien que ce ne soit qu’une des nombreuses façons d’écrire du logiciel, le TDD peut aider à orienter la conception du code. Écrire le test avant d’écrire le code qui fait passer le test aide à maintenir une couverture de test élevée tout au long du processus.
Nous allons piloter par les tests l’implémentation de la fonctionnalité qui effectuera réellement la recherche de la chaîne de requête dans le contenu du fichier et produira une liste de lignes correspondant à la requête. Nous ajouterons cette fonctionnalité dans une fonction appelée search.
Écrire un test qui échoue
Dans src/lib.rs, nous ajouterons un module tests avec une fonction de test, comme nous l’avons fait dans le chapitre 11. La fonction de test spécifie le comportement que nous voulons que la fonction search ait : elle prendra une requête et le texte à rechercher, et ne renverra que les lignes du texte qui contiennent la requête. L’encart 12-15 montre ce test.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\nRust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search function for the functionality we wish we hadCe test recherche la chaîne "duct". Le texte dans lequel nous cherchons fait trois lignes, dont une seule contient "duct" (notez que la barre oblique inverse après le guillemet double ouvrant indique à Rust de ne pas mettre de caractère de nouvelle ligne au début du contenu de ce littéral de chaîne). Nous vérifions que la valeur renvoyée par la fonction search contient uniquement la ligne attendue.
Si nous exécutons ce test, il échouera actuellement car la macro unimplemented! panique avec le message “not implemented”. Conformément aux principes du TDD, nous ferons un petit pas en ajoutant juste assez de code pour que le test ne panique pas lors de l’appel de la fonction, en définissant la fonction search pour qu’elle renvoie toujours un vecteur vide, comme illustré dans l’encart 12-16. Ensuite, le test devrait compiler et échouer car un vecteur vide ne correspond pas à un vecteur contenant la ligne "safe, fast, productive.".
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search function so that calling it won’t panicVoyons maintenant pourquoi nous devons définir une durée de vie explicite 'a dans la signature de search et utiliser cette durée de vie avec l’argument contents et la valeur de retour. Rappelez-vous que dans le [Chapitre 10][ch10-lifetimes], les paramètres de durée de vie spécifient quelle durée de vie d’argument est connectée à la durée de vie de la valeur de retour. Dans ce cas, nous indiquons que le vecteur renvoyé doit contenir des tranches de chaîne qui référencent des tranches de l’argument contents (plutôt que de l’argument query).
Autrement dit, nous indiquons à Rust que les données renvoyées par la fonction search vivront aussi longtemps que les données passées à la fonction search dans l’argument contents. C’est important ! Les données référencées par une tranche doivent être valides pour que la référence soit valide ; si le compilateur suppose que nous créons des tranches de chaîne de query plutôt que de contents, il effectuera ses vérifications de sécurité de manière incorrecte.
Si nous oublions les annotations de durée de vie et essayons de compiler cette fonction, nous obtiendrons cette erreur : console {{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}}
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | pub fn search(query: &str, contents: &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 search<'a>(query: &'a str, contents: &'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 savoir lequel des deux paramètres nous avons besoin pour la sortie, nous devons donc le lui indiquer explicitement. Notez que le texte d’aide suggère de spécifier le même paramètre de durée de vie pour tous les paramètres et le type de sortie, ce qui est incorrect ! Comme contents est le paramètre qui contient tout notre texte et que nous voulons renvoyer les parties de ce texte qui correspondent, nous savons que contents est le seul paramètre qui doit être connecté à la valeur de retour en utilisant la syntaxe de durée de vie.
D’autres langages de programmation ne vous obligent pas à connecter les arguments aux valeurs de retour dans la signature, mais cette pratique deviendra plus facile avec le temps. Vous voudrez peut-être comparer cet exemple avec les exemples de la section [« Valider les références avec les durées de vie »][validating-references-with-lifetimes] du Chapitre 10.
Écrire du code pour faire passer le test
Actuellement, notre test échoue car nous renvoyons toujours un vecteur vide. Pour corriger cela et implémenter search, notre programme doit suivre ces étapes : 1. Itérer sur chaque ligne du contenu. 2. Vérifier si la ligne contient notre chaîne de requête. 3. Si c’est le cas, l’ajouter à la liste des valeurs que nous renvoyons. 4. Si ce n’est pas le cas, ne rien faire. 5. Renvoyer la liste des résultats correspondants.
- Itérer sur chaque ligne du contenu.
- Vérifier si la ligne contient notre chaîne de requête.
- Si c’est le cas, l’ajouter à la liste des valeurs que nous renvoyons.
- Si ce n’est pas le cas, ne rien faire.
- Renvoyer la liste des résultats correspondants.
Travaillons sur chaque étape, en commençant par l’itération sur les lignes.
Itérer sur les lignes avec la méthode lines
Rust dispose d’une méthode utile pour gérer l’itération ligne par ligne des chaînes de caractères, judicieusement nommée lines, qui fonctionne comme illustré dans l’encart 12-17. Notez que cela ne compilera pas encore.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
contentsLa méthode lines renvoie un itérateur. Nous parlerons des itérateurs en profondeur dans le chapitre 13. Mais rappelez-vous que vous avez vu cette façon d’utiliser un itérateur dans l’encart 3-5, où nous avons utilisé une boucle for avec un itérateur pour exécuter du code sur chaque élément d’une collection.
Rechercher la requête dans chaque ligne
Ensuite, nous vérifierons si la ligne courante contient notre chaîne de requête. Heureusement, les chaînes de caractères ont une méthode utile nommée contains qui fait cela pour nous ! Ajoutez un appel à la méthode contains dans la fonction search, comme illustré dans l’encart 12-18. Notez que cela ne compilera toujours pas encore.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
queryPour le moment, nous construisons la fonctionnalité progressivement. Pour que le code compilé, nous devons renvoyer une valeur depuis le corps de la fonction comme nous l’avons indiqué dans la signature de la fonction.
Stocker les lignes correspondantes
Pour terminer cette fonction, nous avons besoin d’un moyen de stocker les lignes correspondantes que nous voulons renvoyer. Pour cela, nous pouvons créer un vecteur mutable avant la boucle for et appeler la méthode push pour stocker une line dans le vecteur. Après la boucle for, nous renvoyons le vecteur, comme illustré dans l’encart 12-19.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Maintenant, la fonction search ne devrait renvoyer que les lignes qui contiennent query, et notre test devrait passer. Exécutons le test : console {{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}}
$ 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 est passé, donc nous savons que ça fonctionne !
À ce stade, nous pourrions envisager des opportunités de refactorisation de l’implémentation de la fonction de recherche tout en gardant les tests passants pour maintenir la même fonctionnalité. Le code de la fonction de recherche n’est pas trop mal, mais il ne tire pas parti de certaines fonctionnalités utiles des itérateurs. Nous reviendrons à cet exemple dans le [Chapitre 13][ch13-iterators], où nous explorerons les itérateurs en détail, et verrons comment l’améliorer.
Maintenant, le programme entier devrait fonctionner ! Essayons-le, d’abord avec un mot qui devrait renvoyer exactement une ligne du poème d’Emily Dickinson : frog. console {{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}}
$ 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 ! Essayons maintenant un mot qui correspondra à plusieurs lignes, comme body : console {{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}}
$ 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 recherchons un mot qui n’apparaît nulle part dans le poème, comme monomorphization : console {{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.txt}}
$ 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`
Excellent ! Nous avons construit notre propre mini version d’un outil classique et avons beaucoup appris sur la structuration des applications. Nous avons aussi appris un peu sur les entrées/sorties de fichiers, les durées de vie, les tests et l’analyse de la ligne de commande.
Pour compléter ce projet, nous montrerons brièvement comment travailler avec les variables d’environnement et comment écrire sur la sortie d’erreur standard, deux choses utiles lorsque vous écrivez des programmes en ligne de commande.