Sobre o dispositivo da funcionalidade de teste integrada no Rust (tradução)

Olá Habr! Apresento a você a tradução da entrada "# [test] in 2018" no blog de John Renner, que pode ser encontrada aqui .

Recentemente, tenho trabalhado na implementação do eRFC para estruturas de teste personalizadas para o Rust. Estudando a base de código do compilador, estudei as partes internas dos testes em Rust e percebi que seria interessante compartilhar isso.

Atributo # [teste]


Hoje, os programadores da Rust contam com o atributo interno #[test] . Tudo que você precisa fazer é marcar a função como um teste e ativar algumas verificações:

 #[test] fn my_test() { assert!(2+2 == 4); } 

Quando esse programa é compilado usando os rustc --test ou cargo test , ele cria um arquivo executável que pode executar esta e qualquer outra função de teste. Esse método de teste permite manter organicamente os testes próximos ao código. Você pode até colocar testes dentro de módulos privados:

 mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } } 

Assim, entidades privadas podem ser facilmente testadas sem o uso de ferramentas de teste externas. Essa é a chave para testes ergonômicos no Rust. Semanticamente, no entanto, isso é bastante estranho. Como a função main chama esses testes se eles não são visíveis ( nota do tradutor : lembro que private - declarado sem usar a palavra-chave pub - é protegido por encapsulamento de acesso externo)? O que exatamente o rustc --test faz?

#[test] implementado como uma conversão de sintaxe dentro da libsyntax compilador libsyntax. Esta é essencialmente uma macro sofisticada que reescreve nossa caixa em 3 etapas:

Etapa 1: reexportar


Como mencionado anteriormente, os testes podem existir dentro de módulos privados, portanto, precisamos de uma maneira de expô-los à função main sem quebrar o código existente. Para esse fim, a libsyntax cria módulos locais chamados __test_reexports que __test_reexports recursivamente os testes . Esta divulgação traduz o exemplo acima em:

 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; } } 

Agora, nosso teste está disponível como my_priv_mod::__test_reexports::test_priv_func . Para módulos aninhados, __test_reexports módulos que contêm os testes; portanto, o teste a::b::my_test se torna a::__test_reexports::b::__test_reexports::my_test . Até agora, esse processo parece bastante seguro, mas o que acontece se houver um módulo __test_reexports existente? Resposta: nada .

Para explicar, precisamos entender como o AST representa identificadores . O nome de cada função, variável, módulo, etc. armazenado não como uma string, mas como um símbolo opaco, que é essencialmente um número de identificação para cada identificador. O compilador armazena uma tabela de hash separada, que permite restaurar o nome legível do símbolo, se necessário (por exemplo, ao imprimir um erro de sintaxe). Quando o compilador cria o módulo __test_reexports , ele gera um novo símbolo para o identificador, portanto, embora os __test_reexports gerados pelo compilador possam ter o mesmo nome do módulo genérico, ele não utilizará o símbolo. Essa técnica evita colisões de nomes durante a geração de código e é a base da higiene do sistema de macro Rust.

Etapa 2: Gerando cintas


Agora que nossos testes estão acessíveis a partir da raiz do nosso engradado, precisamos fazer algo com eles. libsyntax gera esse módulo:

 pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[/*...*/]; #[main] pub fn main() { self::test::test_static_main(TESTS); } } 

Embora essa conversão seja simples, ela nos fornece muitas informações sobre como os testes são realmente executados. Os testes são coletados em uma matriz e transmitidos ao test_static_main testes, chamado test_static_main . Voltaremos ao que TestDescAndFn , mas no momento a principal conclusão é que existe uma caixa chamada test , que faz parte do núcleo do Rust e implementa todo o tempo de execução para teste. A interface de test é instável, portanto, a única maneira estável de interagir com ela é a macro #[test] .

Etapa 3: Gerando um Objeto de Teste


Se você escreveu anteriormente testes no Rust, pode estar familiarizado com alguns dos atributos opcionais disponíveis para as funções de teste. Por exemplo, um teste pode ser anotado com #[should_panic] se esperamos que o teste cause pânico. Parece algo como isto:

 #[test] #[should_panic] fn foo() { panic!("intentional"); } 

Isso significa que nossos testes são mais do que simples funções e possuem informações de configuração. test codifica esses dados de configuração em uma estrutura chamada TestDesc . Para cada função de teste na caixa, a libsyntax analisará seus atributos e gerará uma instância do TestDesc . Em seguida, combina TestDesc e a função de teste na estrutura lógica TestDescAndFn , com a qual test_static_main trabalha. Para este teste, a instância gerada de TestDescAndFn parece com isso:

 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())), } 

Depois de criarmos uma matriz desses objetos de teste, eles são passados ​​para o executor de testes por meio da ligação gerada na etapa 2. Embora essa etapa possa ser considerada parte da segunda etapa, quero chamar a atenção para ela como um conceito separado, porque essa será a chave para implementar o teste personalizado estruturas, mas este será outro post do blog.

Posfácio: Métodos de Pesquisa


Embora eu tenha recebido muitas informações diretamente das fontes do compilador, fui capaz de descobrir que existe uma maneira muito simples de ver o que o compilador faz. A compilação noturna do compilador possui um sinalizador instável chamado unpretty , que você pode usar para imprimir o código-fonte do módulo após expandir as macros:

 $ rustc my_mod.rs -Z unpretty=hir 

Nota do tradutor


Interessante, ilustrarei o código do caso de teste após a divulgação macro:

Código-fonte personalizado:

 #[test] fn my_test() { assert!(2+2 == 4); } fn main() {} 

Código após a expansão de 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/pt418095/


All Articles