ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深度解读 Bevy bevy_reflect compile-fail 测试:用编译错误断言锁定反射系统的编译期契约

深度解读 Bevy bevy_reflect compile-fail 测试:用编译错误断言锁定反射系统的编译期契约 深度解读 Bevy bevy_reflect compile-fail 测试用编译错误断言锁定反射系统的编译期契约【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy本文围绕 Bevy 仓库中crates/bevy_reflect/compile_fail/README.md所述的compile-fail编译失败测试机制展开它是为 Bevy 反射系统定制的反向 UI 测试基础设施通过断言编译器产出的精确错误消息与 span防止Reflect派生宏、reflect_remote、IntoFunction等宏 API 在不经意间改变其错误诊断契约。读完本文你将理解为何要把它做成独立 crate、如何阅读与编写带//~注解的测试用例、底层ui_test引擎如何驱动它们以及 CI 如何用 stable Rust 工具链守护这套错误输出即回归测试的防线。compile-fail 测试是什么bevy_reflect 为什么需要它常规单元测试验证的是代码能跑出正确结果而 compile-fail也叫 UI 测试验证的是代码应该编译失败并且失败得符合预期。对bevy_reflect这样大量使用过程宏derive macros的 crate 而言宏在类型检查期生成的约束与诊断就是面向用户的公共 API字段类型写错、反射属性自相矛盾、外部类型包装不一致时用户看到的应当是清晰、稳定、可解释的rustc错误而不是一团混乱的E0277。这正是crates/bevy_reflect/compile_fail/README.md说明的核心设计动机该测试包是一个独立于bevy_reflect的单独 crate其测试会断言编译器的精确错误输出由于错误消息中的spans 等细节会随 rustc 版本变化这类测试很容易因为新版 Rust 而失败Bevy 仓库的CI 在 stable Rust 工具链上执行这些测试参见 tools/ci/src/main.rs编写测试用例的具体规范指向配套工具 tools/compile_fail_utils/README.md。把测试从bevy_reflect本体中剥离成独立包是为了避免影响 Crater 等针对主 crate 的构建/测试任务——因为这些用例刻意制造编译错误且断言的是会随工具链漂移的诊断文本不适合混入常规cargo test的语义中。仓库根 Cargo.toml 的 workspace members 注释也印证了这一点这些带宏的 crate 内部嵌套的 compile failUI测试专门用于验证诊断输出不会在不知不觉中改变。目录布局与包结构整个测试工程位于crates/bevy_reflect/compile_fail/crates/bevy_reflect/compile_fail/ ├── Cargo.toml # 独立的 bevy_reflect_compile_fail 包 ├── README.md # 本文主体文档 ├── src/lib.rs # 空壳实际逻辑都在 tests 里 └── tests/ ├── derive.rs # [[test]] deriveharness false ├── func.rs # [[test]] funcharness false ├── remote.rs # [[test]] remoteharness false ├── reflect_derive/ # Reflect / FromReflect 派生宏用例 ├── reflect_remote/ # reflect_remote 用例 └── into_function/ # IntoFunction函数反射用例Cargo.toml 的关键配置从 Cargo.toml 可以看到若干关键信息包名bevy_reflect_compile_failpublish falseedition 2024且只把bevy_reflect开启functionsfeature作为普通依赖——因为要测函数反射见into_function用例中对bevy_reflect::func::IntoFunction的引用compile_fail_utils作为 dev-dependency路径指回仓库的 tools/compile_fail_utils声明了三个[[test]]derive、func、remote每个都带harness false——即不套用 Rust 默认测试框架而是由每个 runner 的main()直接驱动ui_test引擎。三个 runner 的写法derive、func、remote三个测试入口文件内部完全一致地薄——它们只负责把测试目录交给compile_fail_utils::testtests/derive.rscompile_fail_utils::test(reflect_derive, tests/reflect_derive)tests/func.rscompile_fail_utils::test(reflect_into_function, tests/into_function)tests/remote.rscompile_fail_utils::test(reflect_remote, tests/reflect_remote)其中 src/lib.rs 只有一句注释Nothing here, check out the integration tests说明真正的用例全部以被编译的.rs文件形态存在于各子目录中。测试文件按*_pass.rs与*_fail.rs成对命名例如bounds_pass.rs与bounds_fail对应场景、from_reflect_fail.rs、custom_where_fail.rs、type_data_fail.rs、generics_fail.rs等正面用例验证能编译负面用例验证报错且报错内容匹配。测试注解语言//与//~速查根据 tools/compile_fail_utils/README.md被测试的.rs文件通过注释注解描述测试应该如何运行、预期哪里出错全局注解////check-pass是最常用的全局注解标记该文件必须编译通过任何编译错误都会让测试失败。仓库里的 bounds_pass.rs 首行即写//check-pass专门用来确认带泛型、where子句、#[reflect(ignore)]字段等复杂形态的Reflect/FromReflect派生是合法的。错误注解//~错误注解由两部分组成可选的错误位置指示符错误匹配器。位置指示符含义^错误发生在上一行v错误发生在下一行\|该注解与另一条注解关联/延续多行连续断言缺省错误发生在注解所在行错误匹配器含义示例E####期待出现指定 rustc 错误码E0499、E0599lint_name触发指定的编译器 lintdead_codeLEVEL: substring出现指定级别ERROR/HELP/WARN/NOTE且包含子串的编译器消息//~v ERROR: missing traitLEVEL: /regex/同上但用正则匹配消息内容//~ ERROR: /.*mismatched.*/README 给出的典型示例是//~v ERROR: missing trait它要求紧邻的下一行产生一条包含子串missing trait的 ERROR。子串可含空格正则形式则用/.../包裹。多个//~|可以横向串联表达同一处错误会连带产生多条消息。仓库真实用例解读反例冲突的from_reflect属性from_reflect_fail.rsuse bevy_reflect::{FromReflect, Reflect}; // Reason: Cannot have conflicting from_reflect attributes #[derive(Reflect)] #[reflect(from_reflect false)] #[reflect(from_reflect true)] //~^ ERROR: already set to false struct Foo { value: String, }这里//~^表示错误发生在上一行即最后一条冲突属性#[reflect(from_reflect true)]所在行且消息须包含already set to false。文件中还覆盖了镜像场景true后跟false断言already set to true以及同时派生Reflect, FromReflect时冲突实现的问题//~ ERROR: conflicting implementation。这些错误消息产自bevy_reflect的派生宏代码宏层面的属性参数校验失败正是通过 compile-fail 用例被钉死的。反例IntoFunction 的约束arguments_fail.rsuse bevy_reflect::func::IntoFunction; use bevy_reflect::Reflect; fn pass(_: i32) {} fn main() { let _ pass.into_function(); let _ too_many_arguments.into_function(); //~^ E0599 let _ argument_not_reflect(foo).into_function(); // 参数为非反射类型 //~^ E0599 }此用例断言参数个数超过上限、参数类型未实现反射时.into_function()应产生E0599方法不存在因为IntoFunctiontrait 只为满足约束的函数签名实现。反例reflect_remote 定义校验invalid_definition_fail.rsreflect_remote允许为外部 crate 的类型建立镜像反射定义因此镜像类型与目标类型长得不一样时必须报错。仓库用例用//~^与//~|组合断言多行#[reflect_remote(external_crate::TheirStruct)] //~^ ERROR: ? operator has incompatible types //~| ERROR: mismatched types struct MyStruct { // Reason: Should be u32 pub value: bool, //~^ ERROR: mismatched types }对枚举还断言了变体形态不一致的诊断如variant ... does not have a field named0、has no field named 0、多条?operator has incompatible types。配合同目录的invalid_definition_pass.rs、type_mismatch_fail.rs/type_mismatch_pass.rs 等正反对照可见该目录刻意用pass/fail 成对的方式把宏的接受面与拒绝面都固化为回归测试。正例//check-pass 锁定合法形态bounds_pass.rs该文件大量出现//check-pass覆盖 struct / tuple struct / enum 三种容器 × 泛型、带T: Clone约束、where T: Clone、无尾逗号的where等组合并借助#[reflect(ignore)]隔离不可反射字段、用#[reflect(Default)] 手写impl Default让FromReflect需要的边界成立。它证明复杂但合法的泛型反射不能因为测试套件过于激进而被误伤。底层引擎compile_fail_utils 如何驱动这些测试配套工具 tools/compile_fail_utils/src/lib.rs 做了几件关键的事统一ui_test版本pub use ui_test;把所有 runner 引用的 oli-obk/ui_test 收敛到同一版本避免各 crate 各自锁依赖默认忽略.stderr全文比对output_conflict_handling在未设置BLESS时使用ignore_output_conflict注释点明原因——stderr output changes between rust versions so we just rely on annotations。也就是说日常跑测试以//~注解为断言标准而不是逐字节比对错误输出路径脱敏用path_stderr_filter把错误消息里的本地目录代码中相对当前 crate 目录的..、RUSTUP_HOME等替换成$BEVY_ROOT、$RUSTUP_HOME占位符再用正则把形如/home/...、C:\users\...的用户目录统一替换为$HOME防止把贡献者的文件系统路径写进快照或 CI 日志依赖注入通过comment_defaults向测试文件注入aux-build风格的依赖构建逻辑DependencyBuilder保证测试文件能真正链接到bevy_reflect等依赖CI 友好的输出test_multiple/test_with_multiple_configs检测到CI环境变量时启用Text::verbose() GitHub Actions group 输出本地则用Text::quiet()。.stderr快照与 BLESS何时生成、为何默认不比对虽然默认断言只看注解ui_test仍会为每个用例维护.stderr快照文件仓库中已存在例如 from_reflect_fail.stderr、generics_fail.stderr。需要重新生成或刷新这些文件时只需给cargo test设置任意非空的环境变量BLESS1 cargo test -p bevy_reflect_compile_failcompile_fail_utils 的源码 表明设置BLESS后output_conflict_handling会切换到bless_output_files把实际编译器输出写回.stderr文件。之所以默认容忍快照与实测不一致是因为 proc-macro 产生的错误消息里包含当前工具链标准库的绝对路径仅靠路径替换难以彻底消除差异——这正是用注解做第一断言、用 BLESS 做快照维护双层设计的由来。CI 集成与本地运行CI 如何执行README 声明这些测试由 CI 在stable Rust 工具链上运行。对应实现是 tools/ci/src/commands/compile_fail.rs 中的compile-fail子命令它依次对bevy_derive_compile_fail、bevy_ecs_compile_fail、bevy_reflect_compile_fail三个包发起独立的cargo test -p ...调用并透传--no-fail-fast、-j与测试线程数参数。针对 reflect 包的失败提示语也呼应了本主题的痛点Compiler errors of the Reflect compile fail tests seem to be different than expected! Check locally and compare rust versions.即 CI 失败通常意味着你本地与 CI 的 rustc 版本不一致导致诊断输出漂移。此外 tools/ci/src/commands/compile.rs 将compile-fail与bench-check、example-check、compile-check、test-check、integration-test-check一起聚合为compile别名供一键式 CI 使用。本地运行在 Bevy 仓库根目录直接运行该包已注册在根 workspace members 中见 Cargo.tomlcargo test -p bevy_reflect_compile_fail注意三个[[test]]均设harness false实际入口是tests/{derive,func,remote}.rs中的main()任何一个//~断言未被满足、或//check-pass文件意外编译失败runner 都会返回非零退出码从而让整条cargo test失败。小结错误输出也是一种需要锁定的公共接口从crates/bevy_reflect/compile_fail/README.md出发可以看到Bevy 对反射系统的质量保障并不仅限于能编译、能运行还包括错误的形态与措辞稳定可预期独立成包的bevy_reflect_compile_fail、基于ui_test的//~注解语言、与用例一一对应的.stderr快照以及 CI 在 stable 工具链上的专项子命令共同构成了一套对编译器诊断漂移高度敏感、却也因此极具回归价值的测试体系。对于任何重度使用过程宏、以编译期诊断作为 DX 一部分的 Rust 项目这套实践都值得借鉴当你改动派生宏的生成代码或错误路径时cargo test -p crate_compile_fail会第一时间告诉你——你刚刚改变了用户会看到的报错。【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表