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