#[test] 属性

今天,Rust 程序员依赖于称为 #[test] 的内置属性。您需要做的只是将一个函数标记为测试(test),并包含一些 断言(asserts),如下所示:

#[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 中 工效(ergonomics)测试的关键。然而从语法上讲,这是相当奇怪的。如果这些函数是不可见的(private)的,主函数如何调用他们呢?rustc --test 是怎么做到的?

编译器中的 rustc_ast crate#[test] 实现了语法转译。本质上这是一个 fancy 的宏,它通过三个步骤重写了 crate。

Step 1: 重新导出(Re-Exporting)

如前所述,测试可以存在于私有模块内部,因此我们需要一种在不破坏现有代码的情况下将其暴露给主函数。因此 rustc_ast 将创建一个名为 __test_reexports 的本地模块,该模块递归地重复导出(Re-Exporting)测试。此扩展的代码示例转换为:

mod my_priv_mod {
    fn my_priv_func() -> bool {}

    pub 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 讲重新导出包含测试模块,因此位于 a::b::my_test 将变成 a::__test_reexports::b::__test_reexports::my_test。尽管此过程看起来很安全,但是如果当前已存在 __test_reexports 模块会怎么样?答案:并不要紧。

为了解释,我们需要了解 AST 如何表示标识符(how the AST represents identifiers)。每个函数,变量,模块的名称都不直接存储为 string,而是存储为不透明的 Symbol,它本质是每个标识符的 ID 号。编译器保留一个独立的哈希表,使我们可以在必要时(例如在打印语法错误时)恢复人类可读的 Symbol 名称。当编译器生成 __test_reexports 模块是,它会为标识符生成一个新的符号,因此尽管编译器生成的__test_reexports 可能与您创建的包共享一个名称,但不会共享一个 Symbol 。此技术可以防止在代码生成过程中发生名称冲突,这是 Rust 宏卫生(hygiene)的基础

Step 2: Harness Generation

现在我们可以从 crate 根目录访问我们的测试,我们需要对它们进行一些操作。 rustc_ast 生成如下模块:

#[main]
pub fn main() {
    extern crate test;
    test::test_main_static(&[&path::to::test1, /*...*/]);
}

其中 path::to::test1 是类型 test::TestDescAndFn 的常量。

尽管这种转换很简单,但它使我们对测试的实际运行方式有很多了解。将测试汇总到一个数组中,然后传递给名称为 test_main_static 的测试运行器。我们将返回到 TestDescAndFn 到底是什么,但是现在,关键点是有一个名为 test crate,它是 Rust Core 的一部分,他实现了测试所有运行时,test 接口是不稳定的,所以与它交互的唯一方式是通过 #[test] 宏。

Step 3: Test Object Generation

如果您以前用 Rust 编写过测试,那么您可能熟悉一些测试函数上可用的一些可选属性。例如,如果我们预测测试会 panic ,可以用 #[should_panic] 来注释测试。看起来是如下的:

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

这意味着我们的测试不仅仅是简单的函数,它们也有配置信息。test 将这个配置数据编码到一个名为 TestDesc 的结构体中。对于 crate 中的每一个测试函数,rustc_ast 将解析其属性并生成 TestDesc 实例。然后它将 TestDesc 和 test 函数组合到可预测名称的 TestDescAndFn 结构体中,test_main_static 对其进行操作。对于给定的测试,生成 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中生成的管理传递给测试运行器。

检查生成的代码

在 nightly rust 中,有一个不稳定的标签叫做 unpretty ,你可以使用它在宏展开后打印出模块的源代码:

$ rustc my_mod.rs -Z unpretty=hir