Hola Habr! Les presento la traducción de la entrada "# [prueba] en 2018" en el blog de John Renner, que se puede encontrar 
aquí .
Recientemente, he estado trabajando en la implementación de 
eRFC para marcos de prueba personalizados para Rust. Al estudiar la base del código del compilador, estudié los aspectos internos de las pruebas en Rust y me di cuenta de que sería interesante compartir esto.
Atributo # [prueba]
Hoy, los programadores de Rust confían en el atributo incorporado 
#[test] . Todo lo que tiene que hacer es marcar la función como prueba y habilitar algunas comprobaciones:
 #[test] fn my_test() { assert!(2+2 == 4); } 
Cuando este programa se compila utilizando los comandos de 
cargo test rustc --test o 
cargo test , creará un archivo ejecutable que puede ejecutar esta y cualquier otra función de prueba. Este método de prueba le permite mantener orgánicamente las pruebas cerca del código. Incluso puede poner pruebas dentro de módulos privados:
 mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } } 
Por lo tanto, las entidades privadas se pueden probar fácilmente sin usar herramientas de prueba externas. Esta es la clave para las pruebas ergonómicas en Rust. Semánticamente, sin embargo, esto es bastante extraño. ¿Cómo llama la función 
main a estas pruebas si no son visibles ( 
nota del traductor : le recuerdo que privado, declarado sin usar la palabra clave 
pub , está protegido por encapsulación del acceso externo)? ¿Qué hace exactamente 
rustc --test ?
#[test] implementa como una conversión de sintaxis dentro de la caja del compilador libsyntax. Esto es esencialmente una macro elegante que reescribe nuestra caja en 3 pasos:
Paso 1: reexportar
Como se mencionó anteriormente, las pruebas pueden existir dentro de módulos privados, por lo que necesitamos una forma de exponerlas a la función 
main sin romper el código existente. Con este fin, 
libsyntax crea módulos locales llamados __test_reexports que __test_reexports recursiva __test_reexports pruebas . Esta divulgación traduce el ejemplo anterior 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; } } 
Ahora nuestra prueba está disponible como 
my_priv_mod::__test_reexports::test_priv_func . Para los módulos anidados, 
__test_reexports volverá 
__test_reexports módulos que contienen las pruebas, por lo que la prueba 
a::b::my_test convierte 
a::__test_reexports::b::__test_reexports::my_test . Este proceso parece bastante seguro hasta ahora, pero ¿qué sucede si hay un módulo 
__test_reexports existente? Respuesta: 
nada .
Para explicar, necesitamos entender 
cómo AST representa los identificadores . El nombre de cada función, variable, módulo, etc. almacenado no como una cadena, sino como un 
símbolo opaco, que es esencialmente un número de identificación para cada identificador. El compilador almacena una tabla hash separada, que nos permite restaurar el nombre legible del símbolo si es necesario (por ejemplo, al imprimir un error de sintaxis). Cuando el compilador crea el módulo 
__test_reexports , genera un nuevo símbolo para el identificador, por lo tanto, aunque los 
__test_reexports generados por el compilador pueden ser del mismo nombre con su módulo genérico, no utilizará su símbolo. Esta técnica evita las colisiones de nombres durante la generación de código y es la base de la higiene del sistema macro Rust.
Paso 2: generar flejes
Ahora que se puede acceder a nuestras pruebas desde la raíz de nuestra caja, tenemos que hacer algo con ellas. 
libsyntax genera dicho módulo:
 pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[]; #[main] pub fn main() { self::test::test_static_main(TESTS); } } 
Aunque esta conversión es simple, nos brinda mucha información sobre cómo se realizan realmente las pruebas. Las pruebas se recopilan en una matriz y se pasan al 
test_static_main prueba, llamado 
test_static_main . Volveremos a lo que 
TestDescAndFn , pero en este momento la conclusión clave es que hay una caja llamada 
test , que es parte del núcleo Rust e implementa todo el tiempo de ejecución para las pruebas. La interfaz de 
test es inestable, por lo tanto, la única forma estable de interactuar con ella es la macro 
#[test] .
Paso 3: generar un objeto de prueba
Si anteriormente escribió pruebas en Rust, puede estar familiarizado con algunos de los atributos opcionales disponibles para las funciones de prueba. Por ejemplo, una prueba se puede anotar con 
#[should_panic] si esperamos que la prueba cause pánico. Se parece a esto:
 #[test] #[should_panic] fn foo() { panic!("intentional"); } 
Esto significa que nuestras pruebas son más que simples funciones y tienen información de configuración. 
test codifica estos datos de configuración en una estructura llamada 
TestDesc . Para cada función de prueba en la caja, 
libsyntax analizará sus atributos y generará una instancia de 
TestDesc . Luego combina 
TestDesc y la función de prueba en la estructura lógica 
TestDescAndFn , con la que funciona 
test_static_main . Para esta prueba, la instancia generada de 
TestDescAndFn ve así:
 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())), } 
Una vez que hemos creado una matriz de estos objetos de prueba, se pasan al corredor de prueba a través del enlace generado en el paso 2. Aunque este paso puede considerarse parte del segundo paso, quiero llamar la atención como un concepto separado, porque esta será la clave para implementar una prueba personalizada marcos, pero esta será otra publicación de blog.
Epílogo: Métodos de investigación
Aunque obtuve mucha información directamente de las fuentes del compilador, pude descubrir que hay una manera muy simple de ver qué hace el compilador. La compilación nocturna del compilador tiene un indicador inestable llamado 
unpretty , que puede usar para imprimir el código fuente del módulo después de expandir las macros:
 $ rustc my_mod.rs -Z unpretty=hir 
Nota del traductor
Interesante por el bien, ilustraré el código del caso de prueba después de la macro revelación:
Código fuente personalizado:
 #[test] fn my_test() { assert!(2+2 == 4); } fn main() {} 
Código después de expandir 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), }]; }