在测试代码中允许使用assert_*,但在生产代码中应禁止所有可能导致程序崩溃的构造

编程语言 2026-07-08

我有一个包含多个Rust包的项目(这是对这个术语的正确叫法吗?):

  • Cargo.toml - Rust库
  • ffi/Cargo.toml - C库
  • ffi/common/Cargo.toml
  • ffi/{python,ruby}/Cargo.toml

所有测试代码都在标记为 #cfg(test) 的模块下。

下面是我的要求:

  • 禁止生产代码中的panic(以及其他可能panic的函数)
  • 允许测试代码中使用断言(assert_*)
  • 不要在Rust代码中乱放 clippy 属性
  • 将所有lint规则放在 Cargo.toml(理想情况下只用一个 Cargo.toml,不在所有的 Cargo.toml 中重复)和/或一个 clippy.toml
  • lint应该在整个代码库运行,包括生产代码和测试代码,通过一个简单的命令,例如 cargo clippy 之类的

我已经努力实现这一点两天了。下面是我遇到的一些问题:

  • 如何在整个代码库上运行lint,而不仅仅是某一个包?
  • 如何在测试代码中允许断言?

解决方案

让panic容易防止的简单方法(Clippy)

你可以通过使用一个工作区(workspace)来把你的Clippy配置(以及很多其他配置)应用到你所有的crates(Rust术语中的包)。具体来说,可以把工作区范围内的Clippy lint放在顶层工作区的 Cargo.toml 下的 [workspace.lints.clippy]

博客文章 Your Clippy Config Should Be Stricter 将给你一个合理的严格Clippy配置,你可以将其应用到你的工作区,从而捕获你在自己的代码中很可能写出的绝大多数panic(以及许多与panic无关的其他反模式)。相关片段如下(已编辑以包含 expect_used):

# Don't Panic - prevent panics from unwraps and unsafe slicing or indexing
string_slice = "warn"
indexing_slicing = "warn"
unwrap_used = "warn"
expect_used = "warn"
panic = "warn"
todo = "warn"
unimplemented = "warn"
unreachable = "warn"
get_unwrap = "warn"
unwrap_in_result = "warn"
unchecked_time_subtraction = "warn"
panic_in_result_fn = "warn"

arithmetic_side_effects 这个lint也可能对你有帮助,但遵守它往往让人非常烦恼。

Clippy没有专门用于禁止断言的lint,但你可以在 clippy.toml 中使用 disallowed_macros 这个lint进行手动配置:

disallowed-macros = [
    "std::assert",
    "std::assert_eq",
    # etc
]

话虽如此,你绝对应该在代码中随处放上 debug assertions,只要你想到某个不应失败的不变量即可。之所以看起来矛盾,是因为调试断言本应触发panic,但在防止生产环境中的panic时,它们其实是你最好的朋友。

Clippy默认不对测试代码进行检查。你可以在测试中使用 cargo clippy --all-targets 来运行它,但由于你明确不想把这些lint应用到测试中,我不推荐这样做。如果你仍然想对测试进行lint,可以通过在工作区放置 clippy.toml 来允许测试中的unwrap与数组下标等行为(这不能放在 Cargo.toml 中):

allow-indexing-slicing-in-tests = true
allow-panic-in-tests = true
allow-unwrap-in-tests = true
allow-expect-in-tests = true

不过,在测试中没有禁用 disallowed-macros 的选项,因此如果你在非测试代码中禁止了 assert_eq!,你将在所有crates的 lib.rsmain.rs 顶部重新启用它:

#![cfg_attr(test, allow(clippy::disallowed_macros))]

让panic变得更难的做法(链接器花招)

如果绝对关键、在任何情况下都不能panic,Clippy只是个很好的起点,但不足以解决问题,因为你不能把Clippy配置应用到标准库或任何第三方依赖项。如果你真的找到办法,我猜会遇到大量警告,你可能不得不走 no_std 且放弃依赖,那么“对整个代码库进行lint”就成了无意义的讨论。

不过,如果你确实必须在依赖项中阻止panic,你可以尝试dtolnay的 no_panic 这样的crate。它会在链接时强制编译器证明被注解的函数不会panic。如果它不能证明它不会panic(要么因为它确实会panic,要么因为编译器不足以看出它不会panic),就会给出一个链接错误。这确实对第三方依赖和标准库提供了一种检查的方式。然而,这可能如此困难以至于你仍然得走 no_std 且放弃依赖。很多(很多)情况下的边缘情况在不被Clippy检查的情况下也会panic。println! 在无法正确写入 stdout 时可能会panic。Vec::insert 在下标越界时也可能会panic。有时这些边缘情况可以通过编译器优化被证明不可能发生,有时则不能。

最后,如果你曾经想过通过替换为不安全代码(unwrap_uncheckedassert_uncheckedunreachable_unchecked 等)来“修复”可能会panic的代码,请不要这样做。即使在生产环境中, panic也总是比未定义行为好。

此外,在你的代码中发生panic时,使用panic永远比进入未定义行为更好。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章