📜 ⬆️ ⬇️

About the device built-in testing functionality in Rust (translation)

Hi, Habr! I present to you the translation of the record "# [test] in 2018" in the blog of John Renner (John Renner), which can be found here .

Recently, I have been working on the implementation of eRFC for custom test frameworks for Rust. Studying the code base of the compiler, I studied the insides of testing in Rust and realized that it would be interesting to share this.

Attribute # [test]


Today, Rust programmers rely on the built-in attribute #[test] . All you need to do is mark the function as a test and include some checks:

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

When this program is compiled using the rustc --test or cargo test commands, it will create an executable file that can run this and any other test function. This test method allows you to organically keep the tests next to the code. You can even put tests inside private modules:
')
 mod my_priv_mod { fn my_priv_func() -> bool {} #[test] fn test_priv_func() { assert!(my_priv_func()); } } 

Thus, private entities can be easily tested without using any external testing tools. This is the key to ergonomics tests in Rust. Semantically, however, this is rather strange. How does the main function call these tests if they are not visible ( note the translator : I remind you that private — declared without using the pub keyword — modules are protected by encapsulation from being accessed from outside)? What exactly does rustc --test ?

#[test] implemented as a syntax conversion within the libsyntax compiler libsyntax . In fact, this is a fancy macro that rewrites our crete in 3 steps:

Step 1: Re-export


As mentioned earlier, tests can exist inside private modules, so we need a way to expose them to the main function without disrupting existing code. To this end, libsyntax creates local modules, called __test_reexports , which recursively __test_reexports - __test_reexports tests . This disclosure translates the example above into:

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

Now our test is available as my_priv_mod::__test_reexports::test_priv_func . For nested modules, __test_reexports will __test_reexports modules containing tests, so the test a::b::my_test becomes a::__test_reexports::b::__test_reexports::my_test . So far this process seems to be pretty safe, but what happens if there is an existing __test_reexports module? Answer: nothing .

To explain, we need to understand how AST represents identifiers . The name of each function, variable, module, etc. not stored as a string, but rather as an opaque Symbol , which is essentially an identification number for each identifier. The compiler stores a separate hash table, which allows us to restore the readable name of the Character if necessary (for example, when printing a syntax error). When the compiler creates the __test_reexports module, it generates a new Symbol for the identifier, therefore, although the compiler-generated __test_reexports may be the same with your samopisny module, it will not use its Symbol. This technique prevents name collisions during code generation and is the basis for the hygiene of the Rust macrosystem.

Step 2: Bundle generation


Now that our tests are available from our root, we need to do something with them. libsyntax generates this module:

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

Although this conversion is simple, it gives us a lot of information about how tests are actually performed. Tests are collected into an array and passed to the test test_static_main , called test_static_main . We will come back to what TestDescAndFn , but at the moment the key conclusion is that there is a cache, called test , which is part of the Rust core and implements the entire runtime for testing. The test interface is not stable, so the #[test] macro is the only stable way to interact with it.

Step 3: Test Object Generation


If you have previously written tests in Rust, you may be familiar with some optional attributes that are available for test functions. For example, a test can be annotated with #[should_panic] if we expect the test to cause a panic. It looks like this:

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

This means that our tests are more than simple functions and have configuration information. test encodes this configuration data into a structure called TestDesc . For each test function in the cracks, libsyntax will analyze its attributes and generate an instance of TestDesc . It then combines TestDesc and the test function into a logical structure TestDescAndFn , with which test_static_main works. For this test, the generated instance of TestDescAndFn looks like this:

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

Once we have built an array of these test objects, they are passed to the test runner through the binding generated in step 2. Although this step can be considered part of the second step, I want to draw attention to it as a separate concept, because it will be the key to the implementation of custom test files. frameworks, but it will be another blog post.

Afterword: Research Methods


Although I learned a lot of information directly from the source code of the compiler, I managed to find out that there is a very simple way to see what the compiler does. The compiler's nightly build has an unstable flag called unpretty , which you can use to print the module source code after macros are opened:

 $ rustc my_mod.rs -Z unpretty=hir 

Translator's Note


For the sake of interest, I will illustrate the code of the test example after macro-opening:

Custom source code:

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

Code after macros expansion:

 #[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/418095/


All Articles