测试

为 Clippy 开发 lint 规则是一个测试驱动开发 (TDD) 过程,因为在为新的 lint 实现任何逻辑之前,我们的首要任务是编写一些测试用例。

通过测试开发 Lint 规则

当我们开发 Clippy 时,我们会进入一个复杂而混乱的领域,其中充满了程序问题、风格错误、不合逻辑的代码以及不符合约定的情况。测试是我们用来定义何时以及在何处触发或不触发新的 lint 规则的第一层秩序。

此外,首先编写测试有助于 Clippy 开发人员为 lint 的首次迭代和进一步增强找到平衡。有了测试用例的支持,我们不必担心在 lint 的第一个版本中过度设计,也不必担心遗漏 lint 的一些明显的边缘情况。这种方法使我们能够迭代地增强每个 lint。

Clippy UI 测试

我们在 Clippy 中使用 UI 测试进行测试。这些 UI 测试检查 Clippy 的输出是否与我们期望的完全一致。每个测试都只是一个普通的 Rust 文件,其中包含我们要检查的代码。

Clippy 的输出会与一个 .stderr 文件进行比较。请注意,您不必自己创建此文件。我们将使用 cargo bless 命令(稍后会看到)来生成 .stderr 文件。

编写测试用例

现在让我们考虑一下我们虚构的 foo_functions lint 的一些测试。我们首先打开由 cargo dev new_lint 创建的测试文件 tests/ui/foo_functions.rs

使用一些示例更新文件以开始

#![warn(clippy::foo_functions)] // < Add this, so the lint is guaranteed to be enabled in this file

// Impl methods
struct A;
impl A {
    pub fn fo(&self) {}
    pub fn foo(&self) {} //~ ERROR: function called "foo"
    pub fn food(&self) {}
}

// Default trait methods
trait B {
    fn fo(&self) {}
    fn foo(&self) {} //~ ERROR: function called "foo"
    fn food(&self) {}
}

// Plain functions
fn fo() {}
fn foo() {} //~ ERROR: function called "foo"
fn food() {}

fn main() {
    // We also don't want to lint method calls
    foo();
    let a = A;
    a.foo();
}

如果没有实际的 lint 逻辑在看到 foo 函数名时发出 lint,此测试只会通过,因为不会发出任何 lint。但是,我们现在可以使用以下命令运行测试

$ TESTNAME=foo_functions cargo uitest

Clippy 将编译并得出测试结果为 ok 的结论

...Clippy warnings and test outputs...
test compile_test ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.48s

这是正常的。毕竟,我们编写了一堆 Rust 代码,但我们还没有真正实现任何让 Clippy 检测 foo 函数并发出 lint 的逻辑。

当我们逐步实现我们的 lint 逻辑时,我们将继续运行此 UI 测试命令。Clippy 将开始输出信息,使我们能够检查输出是否正在变成我们想要的样子。

示例输出

当我们的 foo_functions lint 被测试时,输出将如下所示

failures:
---- compile_test stdout ----
normalized stderr:
error: function called "foo"
  --> tests/ui/foo_functions.rs:6:12
   |
LL |     pub fn foo(&self) {}
   |            ^^^
   |
   = note: `-D clippy::foo-functions` implied by `-D warnings`
error: function called "foo"
  --> tests/ui/foo_functions.rs:13:8
   |
LL |     fn foo(&self) {}
   |        ^^^
error: function called "foo"
  --> tests/ui/foo_functions.rs:19:4
   |
LL | fn foo() {}
   |    ^^^
error: aborting due to 3 previous errors

请注意片段顶部的 failures 标签,我们将在下一节中删除它(保存此输出)。

注意:您可以通过指定逗号分隔的列表来运行多个测试文件:TESTNAME=foo_functions,bar_methods,baz_structs

cargo bless

一旦我们对输出感到满意,我们需要运行此命令来为我们的 lint 生成或更新 .stderr 文件

$ TESTNAME=foo_functions cargo uibless

这会将发出的 lint 建议和修复写入 .stderr 文件,其中包含 lint 的原因、建议的修复以及行号等。

然后,运行 TESTNAME=foo_functions cargo uitest 应该会通过。当我们提交我们的 lint 时,我们也需要提交生成的 .stderr 文件。

一般来说,您应该只提交由 cargo bless 更改的特定 lint 文件,您正在创建/编辑这些文件。

注意:如果生成的 .stderr.fixed 文件为空,则应将其删除。

toml 测试

某些 lint 可以通过 clippy.toml 文件进行配置。这些配置值在 tests/ui-toml 中进行测试。

要在此处添加新测试,请创建一个新目录并添加文件

  • clippy.toml:在此处放置您要测试的配置值。
  • lint_name.rs:一个测试文件,您可以在其中放置测试代码,该代码应根据 clippy.toml 文件中设置的配置看到不同的 lint 行为。

可以使用 cargo bless 再次生成潜在的 .stderr.fixed 文件。

Cargo Lints

Cargo lint 的测试过程有所不同,因为现在我们对 Cargo.toml 清单文件感兴趣。在这种情况下,我们还需要一个与该清单关联的最小 crate。这些测试在 tests/ui-cargo 中生成。

假设我们有一个名为 foo_categories 的新示例 lint,我们可以运行

$ cargo dev new_lint --name=foo_categories --pass=late --category=cargo

运行 cargo dev new_lint 后,我们将默认找到两个新的 crate,每个 crate 都有其清单文件

  • tests/ui-cargo/foo_categories/fail/Cargo.toml:此文件应导致新的 lint 引发错误。
  • tests/ui-cargo/foo_categories/pass/Cargo.toml:此文件不应触发 lint。

如果您需要更多的情况,您可以复制其中一个 crate(在 foo_categories 下)并重命名它。

生成 .stderr 文件的过程与其他 lint 相同,并且将 TESTNAME 变量前置到 cargo uitest 也适用于 Cargo lint。

Rustfix 测试

如果您正在处理的 lint 正在使用结构化建议,则 rustfix 将 lint 中的建议应用于测试文件代码,并将其与 .fixed 文件的内容进行比较。

结构化建议告诉用户如何修复或重写已使用 span_lint_and_sugg lint 的某些代码。

如果应使用 span_lint_and_sugg 生成建议,但并非所有建议都导致有效的代码,您可以使用测试文件顶部的 //@no-rustfix 注释,以不在该文件上运行 rustfix

我们将在 后续章节 中更深入地讨论建议。

运行测试后,使用 cargo bless 自动生成 .fixed 文件。

手动测试

如果您添加了一些 println! 并且测试套件输出变得难以阅读,则针对示例文件进行手动测试可能会很有用。

要使用您本地的修改尝试 Clippy,请从工作副本根目录运行。

$ cargo dev lint input.rs