关于Rust中内置测试功能的设备(翻译)

哈Ha! 我将在John Renner的博客上为您提供“#[test] in 2018”条目的翻译,可在此处找到。

最近,我一直在为Rust的自定义测试框架进行eRFC的实现。 在研究了编译器的代码库之后,我研究了Rust的内部测试,并意识到分享这一点很有趣。

属性#[测试]


今天,Rust程序员依赖于内置属性#[test] 。 您所要做的就是将该功能标记为测试并启用一些检查:

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

当使用rustc --testcargo test命令编译该程序时,它将创建一个可执行文件,该文件可以运行此程序以及任何其他测试功能。 这种测试方法使您有机地使测试接近代码。 您甚至可以将测试放入专用模块中:

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

因此,无需使用任何外部测试工具即可轻松测试私有实体。 这是Rust进行人体工程学测试的关键。 但是,从语义上来说,这很奇怪。 如果main函数不可见, main函数如何调用这些测试( 译者注 :我提醒您,私有(未经声明使用pub关键字声明)受到封装的保护,以防止外部访问)? rustc --test到底做什么?

#[test]实现为libsyntax编译器libsyntax的语法转换。 本质上,这是一个花哨的宏,它通过3个步骤重写了我们的箱子:

步骤1:重新汇出


如前所述,测试可以存在于私有模块内部,因此我们需要一种在不破坏现有代码的情况下将其公开给main函数的方法。 为此, libsyntax 创建名为__test_reexports本地模块,该模块递归地__test_reexports测试 。 本公开将以上示例转换为:

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

现在我们的测试可以通过my_priv_mod::__test_reexports::test_priv_func 。 对于嵌套模块, __test_reexports__test_reexports包含测试__test_reexports模块,因此测试a::b::my_test变为a::__test_reexports::b::__test_reexports::my_test 。 到目前为止,此过程似乎非常安全,但是如果存在现有的__test_reexports模块,会发生什么? 答: 没有

为了解释,我们需要了解AST如何表示标识符 。 每个函数,变量,模块等的名称。 存储的不是字符串,而是不透明的Symbol ,它本质上是每个标识符的标识号。 编译器存储一个单独的哈希表,如果需要的话(例如,在打印语法错误时),它使我们能够还原Symbol的清晰名称。 当编译器创建__test_reexports模块时,它会为标识符生成一个新的Symbol,因此,尽管编译器生成的__test_reexports与您的通用模块同名,但它不会使用其Symbol。 此技术可防止在代码生成过程中发生名称冲突,并且是Rust宏系统卫生的基础。

步骤2:产生捆扎带


现在可以从包装箱的根部访问我们的测试了,我们需要对它们进行一些处理。 libsyntax生成这样的模块:

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

尽管这种转换很简单,但是它为我们提供了许多有关如何实际执行测试的信息。 将测试收集到一个数组中,然后传递给称为test_static_main的测试test_static_main 。 我们将返回到TestDescAndFn什么,但是目前的主要结论是有一个称为test的板条箱,它是Rust内核的一部分,并实现了整个测试运行时。 test接口是不稳定的,因此与之交互的唯一稳定方法是宏#[test]

步骤3:生成测试对象


如果您以前用Rust编写测试,则可能熟悉一些可用于测试功能的可选属性。 例如,如果我们希望测试引起恐慌,则可以使用#[should_panic]注释测试。 看起来像这样:

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

这意味着我们的测试不仅仅是简单的功能,而且还具有配置信息。 test将此配置数据编码为一个名为TestDesc的结构。 对于包装箱中的每个测试功能, libsyntax将分析其属性并生成TestDesc的实例。 然后将TestDesc和测试函数组合到逻辑结构TestDescAndFntest_static_main可以test_static_main该逻辑结构。 对于此测试,生成的TestDescAndFn实例如下所示:

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

一旦我们构建了这些测试对象的数组,它们就会通过步骤2中生成的绑定传递给测试运行器。尽管可以将此步骤视为第二步的一部分,但我还是要提请注意它是一个单独的概念,因为这将是实现自定义测试的关键框架,但这将是另一篇博客文章。

后记:研究方法


尽管我直接从编译器来源获得了很多信息,但我发现有一种非常简单的方法可以查看编译器的功能。 每晚编译器构建都有一个不稳定的标志,称为unpretty ,您可以在扩展宏后使用它来打印模块的源代码:

 $ rustc my_mod.rs -Z unpretty=hir 

译者注


为了有趣,我将在宏公开之后说明测试用例的代码:

自定义源代码:

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

扩展宏后的代码:

 #[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/zh-CN418095/


All Articles