#macro #dev-dependencies #testing #public-api #compiler-error #error-message

trybuild-internals-api

将 trybuild 的内部结构暴露为公共 API 的分支

1 个稳定版本

新版本 1.0.99 2024 年 8 月 21 日

#379Rust 模式

MIT/Apache

145KB
3.5K SLoC

Trybuild

github crates.io docs.rs build status

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] 下的依赖项可以在测试用例中访问。

失败的测试会显示预期的与实际的编译器输出。

未能编译失败的编译失败测试也是失败。

要测试单个源文件,请使用

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函数,在执行编译后的二进制文件时不会panic。


详情

这是整个API。


工作流程

有两种方法可以更新您在测试用例或库迭代过程中的*.stderr文件;不建议手动编写。

首先,如果一个测试用例正在运行为compile_fail,但相应的*.stderr文件不存在,测试运行器将实际编译器输出保存到Cargo.toml所在目录中的名为wip的目录中。因此,您可以通过删除这些文件,运行cargo test,并将所有文件从wip移动到您的测试用例目录来更新这些文件。

或者,使用环境变量TRYBUILD=overwrite运行cargo test,以跳过wip目录,并直接将所有编译器输出写入原处。您可能需要运行git diff来确保编译器的输出符合您的预期。


要测试什么

当涉及到compile-fail测试时,为任何您希望了解用户界面编译器输出变化的更改编写测试。作为一个负面示例,请不要编写仅用错误类型的参数调用所有公共API的compile-fail测试;这没有任何好处。

一个常见用途是测试过程宏发出的特定错误消息。例如,来自ref-cast包的derive宏需要放在具有#[repr(C)]#[repr(transparent)]的类型上,以使展开无未定义行为,它在编译时强制执行

error: RefCast trait requires #[repr(C)] or #[repr(transparent)]
 --> $DIR/missing-repr.rs:3:10
  |
3 | #[derive(RefCast)]
  |          ^^^^^^^

消费辅助属性的宏希望检查那些属性中未识别的内容是否已正确指示给调用者。错误消息是否正确地放在错误的标记下,而不是放在无用的call_site范围内?

error: unknown serde field attribute `qqq`
 --> $DIR/unknown-attribute.rs:5:13
  |
5 |     #[serde(qqq = "...")]
  |             ^^^

声明式宏也可以从compile-fail测试中受益。来自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 包引入了公共但只读的struct字段,即使调用者拥有对周围struct的 &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

在这些所有情况下,编译器的输出可能会改变,因为我们的包或我们的依赖之一破坏了某些东西,或者是由于Rust编译器的变化所致。这两者都是拥有良好构思的编译失败测试的好理由。如果我们重构并错误地导致一个原本正确的错误现在不再被发出或被发现在错误的位置,这对于测试套件捕获来说很重要。如果编译器改变了一些东西,使得我们关心的错误信息变得严重恶化,那么捕获并报告作为编译器问题也同样重要。


许可证

根据您选择的任何一项,在Apache许可证,版本2.0MIT许可证下获得许可。
除非您明确声明,否则您根据Apache-2.0许可证定义的任何有意提交以包含在此包中的贡献,将按照上述方式双重许可,不附加任何额外的条款或条件。

依赖项

~1–9MB
~79K SLoC