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