Bonjour, Habr! Je vous présente la traduction de l'entrée "# [test] en 2018" sur le blog de John Renner, qui se trouve 
ici .
Récemment, j'ai travaillé sur l'implémentation d' 
eRFC pour des cadres de test personnalisés pour Rust. En étudiant la base de code du compilateur, j'ai étudié les principes internes des tests dans Rust et j'ai réalisé qu'il serait intéressant de partager cela.
Attribut # [test]
Aujourd'hui, les programmeurs Rust comptent sur l'attribut intégré 
#[test] . Tout ce que vous avez à faire est de marquer la fonction comme test et d'activer certaines vérifications:
 #[test] fn my_test() { assert!(2+2 == 4); } 
Lorsque ce programme est compilé à l'aide des commandes 
rustc --test ou 
cargo test , il crée un fichier exécutable qui peut exécuter cette fonction et toute autre fonction de test. Cette méthode de test vous permet de garder organiquement des tests proches du code. Vous pouvez même mettre des tests à l'intérieur de modules privés:
 mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } } 
Ainsi, les entités privées peuvent être facilement testées sans utiliser d'outils de test externes. C'est la clé des tests ergonomiques de Rust. Sémantiquement, cependant, c'est plutôt étrange. Comment la fonction 
main appelle-t-elle ces tests s'ils ne sont pas visibles ( 
note du traducteur : je vous le rappelle, privés - déclarés sans utiliser le mot clé 
pub - sont protégés par encapsulation depuis un accès extérieur)? Que fait exactement 
rustc --test ?
#[test] implémenté comme une conversion de syntaxe dans la 
libsyntax compilateur libsyntax. Il s'agit essentiellement d'une macro fantaisie qui réécrit notre caisse en 3 étapes:
Étape 1: réexporter
Comme mentionné précédemment, les tests peuvent exister à l'intérieur des modules privés, nous avons donc besoin d'un moyen de les exposer à la fonction 
main sans casser le code existant. À cette fin, 
libsyntax crée des modules locaux appelés __test_reexports qui __test_reexports récursivement __test_reexports tests . Cette divulgation traduit l'exemple ci-dessus en:
 mod my_priv_mod { fn my_priv_func() -> bool {} fn test_priv_func() { assert!(my_priv_func()); } pub mod __test_reexports { pub use super::test_priv_func; } } 
Notre test est maintenant disponible sous le nom 
my_priv_mod::__test_reexports::test_priv_func . Pour les modules imbriqués, 
__test_reexports modules contenant les tests, donc le test 
a::b::my_test devient 
a::__test_reexports::b::__test_reexports::my_test . Ce processus semble assez sûr jusqu'à présent, mais que se passe-t-il s'il existe un module 
__test_reexports existant? Réponse: 
rien .
Pour expliquer, nous devons comprendre 
comment l'AST représente les identificateurs . Le nom de chaque fonction, variable, module, etc. stocké non pas comme une chaîne, mais plutôt comme un 
symbole opaque, qui est essentiellement un numéro d'identification pour chaque identifiant. Le compilateur stocke une table de hachage distincte, ce qui nous permet de restaurer le nom lisible du symbole si nécessaire (par exemple, lors de l'impression d'une erreur de syntaxe). Lorsque le compilateur crée le module 
__test_reexports , il génère un nouveau symbole pour l'identificateur.Par conséquent, bien que les 
__test_reexports générés par le compilateur puissent être du même nom avec votre module générique, il n'utilisera pas son symbole. Cette technique empêche les collisions de noms lors de la génération de code et est la base de l'hygiène du système macro Rust.
Étape 2: génération du cerclage
Maintenant que nos tests sont accessibles depuis la racine de notre caisse, nous devons faire quelque chose avec eux. 
libsyntax génère un tel module:
 pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[]; #[main] pub fn main() { self::test::test_static_main(TESTS); } } 
Bien que cette conversion soit simple, elle nous donne beaucoup d'informations sur la façon dont les tests sont réellement effectués. Les tests sont collectés dans un tableau et transmis au 
test_static_main test, appelé 
test_static_main . Nous reviendrons sur ce qu'est 
TestDescAndFn , mais pour le moment, la conclusion clé est qu'il existe une caisse appelée 
test , qui fait partie du noyau Rust et implémente l'intégralité du runtime pour les tests. L'interface de 
test est instable, donc la seule façon stable d'interagir avec elle est la macro 
#[test] .
Étape 3: génération d'un objet de test
Si vous avez précédemment écrit des tests dans Rust, vous connaissez peut-être certains des attributs facultatifs disponibles pour les fonctions de test. Par exemple, un test peut être annoté avec 
#[should_panic] si nous nous attendons Ă  ce que le test provoque une panique. Cela ressemble Ă  ceci:
 #[test] #[should_panic] fn foo() { panic!("intentional"); } 
Cela signifie que nos tests sont plus que de simples fonctions et contiennent des informations de configuration. 
test code ces données de configuration dans une structure appelée 
TestDesc . Pour chaque fonction de test dans la caisse, 
libsyntax analysera ses attributs et générera une instance de 
TestDesc . Il combine ensuite 
TestDesc et la fonction de test dans la structure logique 
TestDescAndFn , avec laquelle 
test_static_main fonctionne. Pour ce test, l'instance générée de 
TestDescAndFn ressemble Ă  ceci:
 self::test::TestDescAndFn { desc: self::test::TestDesc { name: self::test::StaticTestName("foo"), ignore: false, should_panic: self::test::ShouldPanic::Yes, allow_fail: false, }, testfn: self::test::StaticTestFn(|| self::test::assert_test_result(::crate::__test_reexports::foo())), } 
Une fois que nous avons construit un tableau de ces objets de test, ils sont transmis au lanceur de test via la liaison générée à l'étape 2. Bien que cette étape puisse être considérée comme faisant partie de la deuxième étape, je veux attirer l'attention sur elle en tant que concept distinct, car ce sera la clé de la mise en œuvre d'un test personnalisé. cadres, mais ce sera un autre article de blog.
Postface: Méthodes de recherche
Bien que j'ai obtenu beaucoup d'informations directement des sources du compilateur, j'ai pu découvrir qu'il existe un moyen très simple de voir ce que fait le compilateur. La compilation nocturne du compilateur a un indicateur instable appelé 
unpretty , que vous pouvez utiliser pour imprimer le code source du module après avoir développé les macros:
 $ rustc my_mod.rs -Z unpretty=hir 
Note du traducteur
Intéressant pour le plaisir, j'illustrerai le code du cas de test après macro-divulgation:
Code source personnalisé:
 #[test] fn my_test() { assert!(2+2 == 4); } fn main() {} 
Code après expansion des macros:
 #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; #[test] pub fn my_test() { if !(2 + 2 == 4) { { ::rt::begin_panic("assertion failed: 2 + 2 == 4", &("test_test.rs", 3u32, 3u32)) } }; } #[allow(dead_code)] fn main() { } pub mod __test_reexports { pub use super::my_test; } pub mod __test { extern crate test; #[main] pub fn main() -> () { test::test_main_static(TESTS) } const TESTS: &'static [self::test::TestDescAndFn] = &[self::test::TestDescAndFn { desc: self::test::TestDesc { name: self::test::StaticTestName("my_test"), ignore: false, should_panic: self::test::ShouldPanic::No, allow_fail: false, }, testfn: self::test::StaticTestFn(::__test_reexports::my_test), }]; }