Acerca del dispositivo de funcionalidad de prueba incorporada en Rust (traducción)

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

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


All Articles