在测试代码中允许使用assert_*,但在生产代码中应禁止所有可能导致程序崩溃的构造
我有一个包含多个Rust包的项目(这是对这个术语的正确叫法吗?):
Cargo.toml- Rust库ffi/Cargo.toml- C库ffi/common/Cargo.tomlffi/{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.rs 或 main.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_unchecked、assert_unchecked、unreachable_unchecked 等)来“修复”可能会panic的代码,请不要这样做。即使在生产环境中, panic也总是比未定义行为好。
此外,在你的代码中发生panic时,使用panic永远比进入未定义行为更好。