À propos de l'appareil de fonctionnalité de test intégrée dans Rust (traduction)

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), }]; } 

Source: https://habr.com/ru/post/fr418095/


All Articles