Trybuild
Trybuild 是一个测试工具,用于在一组测试用例上调用 rustc 并断言产生的任何错误消息是预期的。
这种测试通常用于测试涉及过程宏的错误报告。我们会编写触发宏检测到的错误或 Rust 编译器在生成的展开代码中检测到的错误的测试用例,并与预期的错误进行比较,以确保它们保持用户友好。
这种测试风格有时被称为 ui 测试,因为它们测试了用户与库交互的方面,这些方面不会被普通的 API 测试所覆盖。
这里没有什么是特定于宏的;trybuild 同样适用于测试非宏 API 的误用。
[dev-dependencies]
trybuild = "1.0"
编译失败测试
一个最小的 trybuild 设置如下所示:
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}
可以使用 cargo test
运行测试。它会单独编译匹配全局模式的每个源文件,期望它们编译失败,并断言编译器的错误消息与相邻的 *.stderr 文件中包含的预期输出相匹配(与测试文件同名但扩展名不同)。如果匹配,则认为测试用例成功。
项目 Cargo.toml 中 [dev-dependencies]
下列出的依赖项可以在测试用例中访问。
失败的测试会内联显示预期与实际的编译器输出对比。
未能编译失败的 compile_fail 测试也是一种失败。
要测试单个源文件,请使用:
cargo test -- ui trybuild=example.rs
其中 ui
是调用 trybuild
的 #[test]
函数的名称,example.rs
是要测试的文件名。
通过测试
同样的测试工具也能运行预期通过的测试。通常你会让 Cargo 直接运行这些测试,但能够像这样组合模式可能对工作坊很有用,参与者可以一次启用一个测试用例。Trybuild 最初是为我在 Rust Latam 举办的过程宏工作坊开发的。
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/01-parse-header.rs");
t.pass("tests/02-parse-body.rs");
t.compile_fail("tests/03-expand-four-errors.rs");
t.pass("tests/04-paste-ident.rs");
t.pass("tests/05-repeat-section.rs");
//t.pass("tests/06-make-work-in-function.rs");
//t.pass("tests/07-init-array.rs");
//t.compile_fail("tests/08-ident-span.rs");
}
如果通过测试成功编译并具有在执行编译后的二进制文件时不会崩溃的 main
函数,则认为测试通过。
详情
这就是整个 API。
工作流程
更新 *.stderr 文件有两种方法,在您迭代测试用例或库时不建议手动编写。
首先,如果一个测试用例作为 compile_fail 运行,但相应的 *.stderr 文件不存在,测试运行器会将实际的编译器输出保存到包含 Cargo.toml 的目录中名为 wip 的子目录中,并使用正确的文件名。因此,您可以通过删除这些文件,运行 cargo test
,然后将 wip 中的所有文件移动到您的测试用例目录来更新这些文件。
或者,使用环境变量 TRYBUILD=overwrite
运行 cargo test
,跳过 wip 目录,直接将所有编译器输出写入原位置。之后您需要检查 git diff
以确保编译器的输出符合您的预期。
测试内容
对于编译失败测试,为任何您关心的、可能因用户面向的编译器输出变化而发生变化的内容编写测试。作为反面例子,请不要仅仅为了使用错误类型的参数调用所有公共 API 而编写编译失败测试;这样做没有任何好处。
一个常见的用途是测试过程宏发出的特定目标错误消息。例如,ref-cast
crate 中的派生宏要求放置在具有 #[repr(C)]
或 #[repr(transparent)]
的类型上,以确保展开不会产生未定义行为,这在编译时强制执行:
error: RefCast trait requires #[repr(C)] or #[repr(transparent)]
--> $DIR/missing-repr.rs:3:10
|
3 | #[derive(RefCast)]
| ^^^^^^^
使用辅助属性的宏需要检查这些属性中无法识别的内容是否正确地指示给调用者。错误消息是否正确放置在错误的标记下,而不是放在无用的 call_site span 上?
error: unknown serde field attribute `qqq`
--> $DIR/unknown-attribute.rs:5:13
|
5 | #[serde(qqq = "...")]
| ^^^
声明性宏也可以从编译失败测试中受益。serde_json 中的 json!
宏只是一个大型的 macro_rules 宏,但它努力确保输入中的错误 JSON 始终在最合适的标记上显示错误消息:
error: no rules expected the token `,`
--> $DIR/double-comma.rs:4:38
|
4 | println!("{}", json!({ "k": null,, }));
| ^ no rules expected this token in macro call
有时我们可能有一个成功展开的宏,但我们指望它在宏展开之后的某个点触发特定的编译器错误。例如,readonly
crate 引入了公开但只可读的结构体字段,即使调用者有对周围结构体的 &mut 引用。如果有人写入只读字段,我们需要确保它不会编译:
error[E0594]: cannot assign to data in a `&` reference
--> $DIR/write-a-readonly.rs:17:26
|
17 | println!("{}", s.n); s.n += 1;
| ^^^^^^^^ cannot assign
在所有这些情况下,编译器的输出可能会因为我们的 crate 或其中一个依赖项破坏了某些内容,或者由于 Rust 编译器的变化而改变。这两种情况都是拥有精心设计的编译失败测试的好理由。如果我们重构并不小心导致原本正确的错误不再被发出或在错误的地方发出,测试套件捕捉到这一点很重要。如果编译器的变化使我们关心的错误消息大幅恶化,捕捉到这一点并报告为编译器问题也很重要。
许可证
根据 Apache License, Version 2.0 或 MIT license 两者之一授权,由您选择。除非您另有明确说明,否则您有意提交以包含在本 crate 中的任何贡献(如 Apache-2.0 许可证中所定义)均应按上述方式双重许可,无任何附加条款或条件。