news 2026/9/8 21:36:56

Testing `expect-panic`

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Testing `expect-panic`

Testingexpect-panic

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

[lint] preview = true select = ["RUF990"]

panic.py:

1
虽然只有 15 行,但每一行都有明确的测试意图,逐段拆解: | 元素 | 内容 | 作用 | |---|---|---| | H1 标题 | `# Testing \`expect-panic\`` | mdtest 以标题切分测试段(section),每个标题对应一个独立的子测试 | | HTML 注释指令 | `<!-- expect-panic: This is a fake panic for testing -->` | 声明“本子测试预期会 panic”,冒号后的文本是**必须出现在 panic 消息中的子串** | | TOML 配置块 | `preview = true` + `select = ["RUF990"]` | 为本次 Lint 启用 preview 模式,并只选择 RUF990 这一条测试规则 | | Python 代码块 | 标题为 `panic.py`,内容为 `1` | 声明一个嵌入式虚拟文件 `panic.py`;文件名是触发 panic 的关键,文件内容本身无关紧要 | 两个关键设计点值得单独说明: 1. **`preview = true` 不可省略**。RUF990 对应的规则元数据标注为 `preview_since = "0.0.0"`(见下文),意味着它处于 preview 状态、永远不属于 stable 集合;如果不开启 preview,`select = ["RUF990"]` 根本不会让该规则生效,panic 也就无从谈起。 2. **文件名 `panic.py` 就是测试的“开关”**。规则实现里通过判断被 Lint 的文件名是否以 `panic.py` 结尾来决定是否触发断言失败(即 panic),因此代码块标题写成 `panic.py` 是夹具生效的前提,而文件内容 `1` 只是一个合法的占位语句。 ## `expect-panic` 指令的解析:mdtest 的 HTML 注释指令系统 mdtest 是 Ruff 自研的 Markdown 测试框架,核心实现位于 [crates/mdtest/src/parser.rs](https://link.gitcode.com/i/6505347e46360239ba513dc1ec1101d3)。在该文件的 `parse_impl` 中([parser.rs#L591-L653](https://link.gitcode.com/i/6505347e46360239ba513dc1ec1101d3#L591-L653)),解析器识别以 `<!-- ... -->` 包裹的 HTML 注释,并将其中 `指令名: 值` 形式的片段映射为 `MdtestDirective`: ```rust const SECTION_CONFIG_SNAPSHOT: &str = "snapshot-diagnostics"; const SECTION_CONFIG_PULLTYPES: &str = "pull-types:skip"; const SECTION_CONFIG_EXPECT_PANIC: &str = "expect-panic"; match directive { SECTION_CONFIG_SNAPSHOT => { /* 必须不带值 */ } SECTION_CONFIG_PULLTYPES => { /* 必须不带值 */ } SECTION_CONFIG_EXPECT_PANIC => { self.process_mdtest_directive(MdtestDirective::ExpectPanic, value)?; } _ => { /* 非白名单注释直接报错,防止拼写错误 */ } }

可以看到与夹具配套的三条硬约束:

  • expect-panic允许携带值的指令(值就是预期 panic 消息子串),而snapshot-diagnosticspull-types:skip等指令必须不带值,否则解析阶段就报错;
  • 所有 HTML 注释都必须落在指令白名单或fmt:on/fmt:off之内,否则直接bail!,避免夹具里出现“拼错了的指令”被静默忽略;
  • 解析结果最终可通过 parser.rs#L162-L164 的should_expect_panic()读取,返回Option<&str>Some(子串)表示预期 panic 且需匹配子串,None表示预期 panic 但不校验消息。

指令语义的官方说明在 crates/ty_test/README.md 中(ruff_mdtest 与 ty_test 共用同一套 mdtest 内核):

冒号后的文本是必须出现在 panic 消息中的子串,该消息是可选的。

主角 RUF990:一条专门制造 panic 的测试规则

夹具里select = ["RUF990"]选中的规则定义在 crates/ruff_linter/src/rules/ruff/rules/test_rules.rs。文件头部注释说明了这一组“假规则”的定位:

/// Fake rules for testing Ruff's behavior /// /// All of these rules should be assigned to the RUF9XX codes.

PanicyTestRule位于 test_rules.rs#L495-L529:

#[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.0.0", category = Category::Testing)] pub(crate) struct PanicyTestRule; impl Violation for PanicyTestRule { const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; #[derive_message_formats] fn message(&self) -> String { "If you see this, maybe panic!".to_string() } } impl TestRule for PanicyTestRule { fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { assert!( !context.source_file().name().ends_with("panic.py"), "This is a fake panic for testing." ); } }

这里有四个与夹具严格咬合的细节:

  1. 触发条件diagnostic中唯一的逻辑是对被 Lint 文件名断言!ends_with("panic.py")。mdtest 会把嵌入式代码块物化进内存文件系统,本夹具声明的文件恰好叫panic.py,断言随即失败,Rust 的assert!抛出 panic,panic 消息正是"This is a fake panic for testing."—— 与夹具中expect-panic指令携带的子串逐字对应。
  2. preview 元数据preview_since = "0.0.0"表明该规则自始处于 preview 状态,因此夹具必须写preview = true,否则规则不会进入启用集合。
  3. 规则编码注册:RUF990 到PanicyTestRule的映射写在 crates/ruff_linter/src/codes.rs#L1253:(Ruff, "990") => rules::ruff::rules::PanicyTestRule;规则统一登记在TEST_RULES常量(test_rules.rs#L37-L51)中,并在 linter.rs#L325-L327 的测试规则分派处按Rule::PanicyTestRule调用diagnostic
  4. 为什么不直接 report 一个诊断:这条规则的目的不是报告违规,而是把“Lint 进程崩溃”本身当作被测行为——mdtest 需要一条可控、可复现、消息固定的 panic 源,来验证“预期 panic”断言机制本身是正确的。

运行链路:panic 如何被捕获并与预期消息比对

测试入口:datatest 扫描所有.md夹具

crates/ruff_mdtest/tests/mdtest.rs 用datatest_stable../ruff_linter/resources/mdtest目录下所有*.md文件各自注册成一个测试用例,本夹具对应的测试名即test-rules/panicy-test-rule.md

datatest_stable::harness! { { test = mdtest, root = "../ruff_linter/resources/mdtest", pattern = r"\.md$" }, }

执行:把 Markdown 变成一次 Lint 调用

crates/ruff_mdtest/src/lib.rs#L26-L51 的run函数负责单个夹具的执行:先调用 mdtest 解析器把 Markdown 切成MarkdownTestSuite,再基于ruff_db建立测试数据库;每个子测试在 ruff_mdtest/src/lib.rs#L53-L80 的run_test中,将嵌入式代码块(py/pyi/ipynb/toml语言块)物化到内存文件系统/src下,解析 TOML 配置块为Options,然后对目标文件执行 Lint。夹具中select = ["RUF990"]即在此处生效,最终走到PanicyTestRule::diagnostic触发 panic。

捕获与断言:attempt_testcheck_panic

mdtest 内核(crates/mdtest/src/lib.rs)把“执行测试函数”包裹在 panic 捕获里。lib.rs#L591-L605 的attempt_test借助ruff_db::panic模块的catch_unwind

/// Run a function over an embedded test file, catching any panics that occur in the process. pub fn attempt_test<'a, T, F>( test_fn: F, test_file: &'a TestFile<'a>, ) -> Result<T, AttemptTestError<'a>> where F: FnOnce(File) -> T + std::panic::UnwindSafe, { catch_unwind(|| test_fn(test_file.file)).map_err(|info| AttemptTestError { info, test_file }) }

随后 lib.rs#L673-L702 的check_panic完成双向断言,这正是expect-panic指令的完整语义:

pub fn check_panic<C>(test: &MarkdownTest<'_, '_, C>, panic_info: Option<PanicError>) { match panic_info { Some(panic_info) => { // 发生了 panic:若指令携带预期子串,则 panic 消息必须包含它 if let Some(expected_message) = expected_message { assert!(message.contains(expected_message), /* ... */); } } None => { // 未发生 panic:但指令声明了 expect-panic → 测试失败 panic!("Test `{}` is expected to panic but it didn't.", test.name()); } } }

结合本夹具可以读出三种失败路径:

  • panic 消息不匹配:规则改了断言文案,但夹具里的expect-panic子串没跟着改,message.contains(expected_message)失败;
  • 预期 panic 却没有 panic:例如把代码块标题从panic.py改成别的名字,assert!不再触发,测试会以“expected to panic but it didn't”失败——这条约束保证了夹具不会“假绿”;
  • 意外 panic(没有expect-panic指令却 panic):AttemptTestError::into_file_failures(lib.rs#L612-L664)会把 panic 位置、payload 以及 backtrace(可用RUST_BACKTRACE=1开启)格式化为指向夹具对应行的诊断输出,并在提示中说明如何获取回溯。

运行与调试该测试

由于每个.md夹具都是 datatest 注册的一个用例,验证方式即常规cargo test

# 运行整个 mdtest 套件 cargo test -p ruff_mdtest # 只跑本夹具(MDTEST_TEST_FILTER 按测试名做子串过滤) MDTEST_TEST_FILTER="panicy-test-rule.md" cargo test -p ruff_mdtest

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 21:32:21

CodeGraph:给AI编码代理一张代码地图,终结‘瞎改代码’

1. 这是什么东西&#xff0c;为什么你需要一张代码地图 如果你最近在用 AI 编码代理写代码&#xff0c;大概率会遇到一种很微妙的挫败感&#xff1a;你把它接进了 IDE&#xff0c;它确实能写函数、补测试、改 bug&#xff0c;但总感觉它“没来过这个项目”。你让它改某个模块的…

作者头像 李华
网站建设 2026/9/8 21:30:59

社交网络影响力最大化实战:贪心算法与PageRank深度对比

简介&#xff1a;一份围绕社交网络影响力最大化的Python实现资源&#xff0c;聚焦线性阈值&#xff08;LT&#xff09;模型及其贪心改进算法&#xff0c;适合从事社交网络分析、病毒营销和推荐系统方向的学生和研究者学习实验。代码均配有详细注释&#xff0c;同时附有Wiki-Vot…

作者头像 李华
网站建设 2026/9/8 21:30:14

CMSIS-5源码级解析:嵌入式MCU工程的分层设计与选型避坑指南

最近重新把 ARM CMSIS-5 整个拉下来做了一次断断续续的源码级梳理&#xff0c;边看边和手头几个量产项目的工程结构对照&#xff0c;发现不少以前“用了但没理解”的地方。网上聊 CMSIS-5 的文章并不少&#xff0c;但大多停留在“它有 Core、DSP、NN、RTOS 这几个组件”的层面&…

作者头像 李华
网站建设 2026/9/8 21:26:43

RPCS3自动更新3步配置指南:简单快速上手

RPCS3自动更新3步配置指南&#xff1a;简单快速上手 【免费下载链接】rpcs3 PlayStation 3 emulator and debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 RPCS3 更新大约每 1-2 周就一版&#xff0c;手动下载、解压、覆盖既耗时又容易把能跑的安装改…

作者头像 李华
网站建设 2026/9/8 21:23:37

Devika 实战指南:让 AI 软件工程师独立交付一个电商首页

Devika 实战指南&#xff1a;让 AI 软件工程师独立交付一个电商首页 【免费下载链接】devika Devika is the first open-source implementation of an Agentic Software Engineer. Initially started as an open-source alternative to Devin. 项目地址: https://gitcode.com…

作者头像 李华