Testing `expect-panic` 📅 发布时间:2026/9/8 21:37:00 👁 浏览次数: 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 集合如果不开启 previewselect [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-diagnostics、pull-types:skip等指令必须不带值否则解析阶段就报错所有 HTML 注释都必须落在指令白名单或fmt:on/fmt:off之内否则直接bail!避免夹具里出现“拼错了的指令”被静默忽略解析结果最终可通过 parser.rs#L162-L164 的should_expect_panic()读取返回OptionstrSome(子串)表示预期 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 Ruffs 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. ); } }这里有四个与夹具严格咬合的细节触发条件diagnostic中唯一的逻辑是对被 Lint 文件名断言!ends_with(panic.py)。mdtest 会把嵌入式代码块物化进内存文件系统本夹具声明的文件恰好叫panic.py断言随即失败Rust 的assert!抛出 panicpanic 消息正是This is a fake panic for testing.—— 与夹具中expect-panic指令携带的子串逐字对应。preview 元数据preview_since 0.0.0表明该规则自始处于 preview 状态因此夹具必须写preview true否则规则不会进入启用集合。规则编码注册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。为什么不直接 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.mddatatest_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_test与check_panicmdtest 内核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_testa, T, F( test_fn: F, test_file: a TestFilea, ) - ResultT, AttemptTestErrora 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_panicC(test: MarkdownTest_, _, C, panic_info: OptionPanicError) { 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 didnt., test.name()); } } }结合本夹具可以读出三种失败路径panic 消息不匹配规则改了断言文案但夹具里的expect-panic子串没跟着改message.contains(expected_message)失败预期 panic 却没有 panic例如把代码块标题从panic.py改成别的名字assert!不再触发测试会以“expected to panic but it didnt”失败——这条约束保证了夹具不会“假绿”意外 panic没有expect-panic指令却 panicAttemptTestError::into_file_failureslib.rs#L612-L664会把 panic 位置、payload 以及 backtrace可用RUST_BACKTRACE1开启格式化为指向夹具对应行的诊断输出并在提示中说明如何获取回溯。运行与调试该测试由于每个.md夹具都是 datatest 注册的一个用例验证方式即常规cargo test# 运行整个 mdtest 套件 cargo test -p ruff_mdtest # 只跑本夹具MDTEST_TEST_FILTER 按测试名做子串过滤 MDTEST_TEST_FILTERpanicy-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),仅供参考