哈Ha! 我将在John Renner的博客上为您提供“#[test] in 2018”条目的翻译,可在
此处找到。
最近,我一直在为Rust的
自定义测试框架进行
eRFC的实现。 在研究了编译器的代码库之后,我研究了Rust的内部测试,并意识到分享这一点很有趣。
属性#[测试]
今天,Rust程序员依赖于内置属性
#[test]
。 您所要做的就是将该功能标记为测试并启用一些检查:
#[test] fn my_test() { assert!(2+2 == 4); }
当使用
rustc --test
或
cargo 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
和测试函数组合到逻辑结构
TestDescAndFn
,
test_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), }]; }