Carbon 工具链 file_test 测试编写完全指南:文件结构、命名约定与 autoupdate 工作流
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
本篇指南以 toolchain_tests 技能文档 为主体,结合 Carbon 语言仓库中 file_test 基础设施、自动更新脚本 以及
toolchain/*/testdata/下的真实测试用例,系统讲解如何为 Carbon 工具链编写、结构化和运行 file test。读完本文,你将掌握测试文件的头注释与AUTOUPDATE机制、最小化 prelude 的引入方式、split 测试与[[@TEST_NAME]]占位符、fail_/todo_前缀语义、常量求值的验证约定、SemIR 输出裁剪,以及如何用 autoupdater 一键生成CHECK校验行。
工具链测试概览:file_test 如何工作
Carbon 语言仓库中,工具链(toolchain)的绝大多数行为验证都依赖统一的file_test基础设施。测试数据以.carbon源码文件的形式存放在toolchain/*/testdata/目录中(例如 toolchain/check/testdata/),覆盖 lex、parse、check、lower 等各个编译阶段。
一条工具链测试的典型执行链路是:Lexing(词法)→ Parsing(语法)→ Checking(语义检查)→ 可选 Lowering(降级)。测试框架将 Carbon 源文件送入编译流水线,捕获输出(例如 SemIR 转储、Clang 报错等),再与测试文件内的行内CHECK记录进行比对校验。
从源码结构看,这套基础设施由三层组成:
- Bazel 规则层:testing/file_test/rules.bzl 中的
file_test()宏负责把testdata/**的 glob 转成 manifest 并生成cc_test,同时会额外生成name.file_path形式的 per-file 手动测试,方便单独跑某个文件; - 基类实现层:testing/file_test/file_test_base.h 提供
FileTestBase,子类通过重写Run()与GetDefaultArgs()接入自己的编译逻辑,最后用CARBON_FILE_TEST_FACTORY(MyFileTest)注册; - 命令行入口层:toolchain/testing/file_test 接收
--autoupdate、--file_tests=、--threads、--dump_output等参数。
文件布局与头注释规范
测试文件必须以标准 Carbon 许可证头开头,随后是配置注释,并用空注释行(//)分隔不同段落。SKILL.md 给出了标准模板:
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM // Exceptions. See /LICENSE for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // // INCLUDE-FILE: toolchain/testing/testdata/min_prelude/... // // AUTOUPDATE两条关键规则:
// AUTOUPDATE是强制性的:任何使用CHECK标记的测试文件都必须声明它。它告诉测试框架允许在运行--autoupdate时重写/插入CHECK行;若不想被自动更新,对应标记是// NOAUTOUPDATE(二者必须恰好出现一个,见 testing/file_test/README.md 的 Comment markers 一节)。使用 split 测试时,该标记当前必须位于任何 split 之前;// TIP:行由 autoupdater 自动生成:例如bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/alias/builtins.carbon。你不必手写 TIP(手写也无害,脚本会接管),它的存在只是提示开发者如何单独运行该测试,对校验结果没有影响。
真实示例可参考 toolchain/check/testdata/alias/builtins.carbon 的文件头:它同时展示了许可证头、INCLUDE-FILE、AUTOUPDATE与自动生成的TIP行。
最小化 prelude:INCLUDE-FILE与内置原语
当测试内容与Core包完全无关时,SKILL.md 建议通过// INCLUDE-FILE引入一个最小化 prelude,通常选择toolchain/testing/testdata/min_prelude/下的脚本,如int.carbon或primitives.carbon。这样做能显著加快执行速度、最小化 STDOUT 噪音。
以 toolchain/testing/testdata/min_prelude/primitives.carbon 为例,它是一个只含原始类型的极简 prelude:通过INCLUDE-FILE组合bool/char/copy/float/int/string/uint等parts/分片,并用EXTRA-ARGS: --custom-core --exclude-dump-file-prefix=min_prelude/告诉测试框架使用自定义 Core 且不在输出中混入 prelude 内容;而 toolchain/testing/testdata/min_prelude/int.carbon 则只引入Int/i32(数组测试所必需)。这些分片本身也是用INCLUDE-FILE层层嵌套的,例如 parts/float.carbon 内部又引入了as、copy、float_literal、int_literal。
关键约束——内置原语测试:在最小化 prelude 下,标准运算符(+、-、/、<等)不会被导入,也不可用。若要以最小的 prelude 足迹编写测试,需要直接在测试代码中调用原始内置函数来构造表达式,例如用float.negate、float.div等= "float.make_type"/"primitive_*"形式暴露的内置函数,而不是依赖运算符语法糖。这一点在parts/float.carbon的实现中体现得很直观:FloatLiteral as ImplicitAs(Float(To))的转换直接绑定到"float.convert_checked"这样的原始内置名称。
split 测试与[[@TEST_NAME]]
一个物理文件可以通过 split 标记// --- <filename>拆成多个逻辑文件,从而在一个文件里测试多种场景:
// --- passing_case.carbon library "[[@TEST_NAME]]"; // ... // --- fail_bad_case.carbon library "[[@TEST_NAME]]"; // ...注意事项(SKILL.md 中列为强制约定):
- 每个 split 内一律使用
library "[[@TEST_NAME]]";,而不是硬编码库名。这能防止命名冲突、避免重复定义默认库,并让测试代码保持干净、可模板化; - 必须精确书写
[[@TEST_NAME]](含方括号)。测试基础设施会自动把它替换为 split 的文件名去掉todo_和fail_前缀后的结果。其替换语义在 testing/file_test/README.md 的 Content replacement 一节有完整定义:替换为“去掉扩展名及fail_/todo_前缀后的文件名”,对 split 文件则基于 split 文件名计算; - 严禁把“预期通过”与“预期失败”的代码放进同一个 split。校验机制依赖:非失败 split 必须产生零错误,失败 split 必须独立地产生正确的编译器错误。混放会破坏这种独立验证的前提。
文件命名前缀:fail_与todo_
预期失败必须与意外失败(以及 bug)区分开,命名前缀是唯一的沟通载体。四种语义如下:
| 前缀 | 含义 | 当前状态 |
|---|---|---|
fail_... | 测试应当产生编译器错误,也确实产生了 | 正确行为 |
todo_fail_... | 测试应当产生错误,但当前没有产生 | 待实现(有 bug) |
fail_todo_... | 测试确实产生错误或崩溃,但不应该(或错误内容不对、伴随错误时行为异常) | 待修复(有 bug) |
todo_... | 测试存在某些错误行为,但当前不产生错误,且也不应该产生错误 | 待修复(有 bug) |
主文件命名规则:主测试文件(以及任何 split 文件)只要有关联错误,就必须带fail_前缀。例外:如果主文件内至少有一个带fail_前缀的 split,主文件可以省略fail_。testing/file_test/README.md 同样确认了这条规则:主文件与 split 文件的fail_前缀“当且仅当”其有关联错误时是必需的。
另外,fail_和todo_前缀都会从[[@TEST_NAME]]等文件名属性中被剥离。
常量求值验证的六项约定
SKILL.md 专门为语义检查(check)测试中的常量求值验证列出了整套约定,目的是保证诊断输出稳定、准确。这些约定在编写涉及浮点转换、舍入、泛型参数等场景的测试时务必遵守:
- 字面量拼写规范化(Literal Spelling Canonicalization):在 SemIR 中,数学值相同但源码拼写不同的实数字面量可能被分配到不同的内部表示 ID。要彻底杜绝预期输出中因拼写差异导致的不匹配,验证测试必须使用规范化的比较手段,例如把转换后的值传入
Expect(X as f64)这样的函数; - 泛型参数验证:本地运行时变量作为泛型实参会被编译期约束拒绝,因此要绕过这种约束测试泛型类型转换,可以把静态字面量直接传给原始内置调用,来验证编译期转换;
- 穷举边界情况验证:对复杂的数学算法(如浮点转整数的截断与舍入),要映射并执行覆盖每条代码分支、条件出口与回退求值路径的测试约束;
- 舍入阈值边界:测试紧贴数学边界的用例,例如表示恰好大于 1.0 微小增量的浮点字面量,如 $2^{30} \times 2^{-30}$ 或 $10^{10} \times 10^{-10}$,验证能精确截断到 1 或 0;
- 精确浮点字面量拼写:针对目标阈值用精确的数学精度书写浮点字面量。例如测试 1.0 之上最小分数增量时,用精确的十六进制小数
0x1.0000000000001p0或高精度十进制小数1.0000000000000001,而不是1.1这种粗粒度分数,以保证边界断言正确; - 表示容量边界与零值尺寸边界:显式瞄准目标类型表示能力的极限(如
i32/u32的 mantissa 与 exponent 组合恰好等于、略低于、略高于容量上限);同时对0与0.0边界输入做显式断言,确保零输入能被正确“定尺寸并简化”,不会触发下溢、除零错误或低估所需位分配。
测试代码注释:只写“验证什么”,不写“思考过程”
SKILL.md 明确禁止在测试文件中出现“agent 思考痕迹”——例如“Wait, but...”这类描述推理过程的注释。留在测试里的注释应当简洁地描述该测试本身在验证什么,供人类读者理解意图,而不是记录 AI 的思维链。这一点也是工具链测试作为回归资产长期可维护的基础。
SemIR 转储与输出最小化
测试应把 STDOUT 校验范围限制在被测逻辑上。SKILL.md 要求:
- 始终使用
//@dump-sem-ir-begin与//@dump-sem-ir-end包裹希望转储 SemIR 的具体声明/代码块; - 只使用这一对标记,不要再叠加
--dump-sem-ir-ranges=if-present之类的额外参数——新测试通过默认行为让//@dump-sem-ir...把输出自然过滤到被高亮的片段。
//@dump-sem-ir-begin fn F(x:? form(ref i32)); //@dump-sem-ir-end真实示例见 toolchain/check/testdata/alias/builtins.carbon:每个 split 的声明前后都用//@dump-sem-ir-begin/end圈定,随后紧跟对应的// CHECK:STDOUT:常量表与文件表断言。
生成与更新输出:autoupdate 工作流
AI 工具永远不要手写或手动触碰// CHECK:STDOUT:/// CHECK:STDERR:注释。正确流程是:
- 写好 Carbon 测试代码、文件头与
// AUTOUPDATE; - 运行测试更新器:
./toolchain/autoupdate_testdata.py toolchain/PATH/TO/YOUR/TEST.carbon- 用
git diff审查更新后的测试输出,确认逻辑路径被正确覆盖,而不是产生大段样板代码。
该脚本(toolchain/autoupdate_testdata.py)本质上是bazel run -c <build_mode> //toolchain/testing:file_test -- --autoupdate ...的封装:它通过bazel info workspace探测构建模式(默认fastbuild),仅接受位于testdata/下的.carbon文件参数,并以--file_tests=逗号列表的形式传给 file_test。它还支持--threads、--print_slowest_tests以及--non-fatal-checks(后者与-c optimize不兼容)等透传参数。
autoupdate 的插入策略(见 testing/file_test/README.md):CHECK从AUTOUPDATE标记下方开始插入;带行信息的CHECK会被尽量插到关联行的旁边(stderr 的在前、stdout 的在后);若测试中没有任何STDOUT检查关联到具体行,则所有STDOUT检查行会统一放到文件末尾。对于 split 测试,若最后一个 split 命名为// --- AUTOUPDATE-SPLIT,则所有CHECK都会集中写进该 split,不做行关联。
常用的注释标记速查
在 testing/file_test/README.md 中可以查到file_test支持的全部行内配置标记,它们是 SKILL.md 之上更细的可用选项:
| 标记 | 作用 |
|---|---|
// AUTOUPDATE/// NOAUTOUPDATE | 控制该文件是否参与--autoupdate,二者必须恰好出现一个 |
// ARGS: <arguments> | 空格分隔的命令行参数,支持%s(替换为文件列表,仅允许独立成参)、%t(替换为${TEST_TMPDIR}/temp_file)、%{identifier}(替换为实现相关标识符);至多指定一次 |
// EXTRA-ARGS: <arguments> | 追加式参数,语义同ARGS,可重复、可与ARGS并存 |
// INCLUDE-FILE: <path/from/repository/root> | 把指定文件纳入测试的虚拟文件系统并拼入当前文件的 split;被包含文件还可继续使用ARGS/EXTRA-ARGS/INCLUDE-FILE/--- <filename>,未给 split 名时以include_files/<filename>命名。最小化Core包就是靠它实现的 |
// SET-CAPTURE-CONSOLE-OUTPUT | 把测试自身(而非传入流)的 stdout/stderr 也捕获进输出;应尽量避免使用,仅在包装 Clang(直接写 stderr)时有用 |
// SET-CHECK-SUBSET | 默认输出每一行都必须有CHECK匹配;加上它后未匹配行被忽略,但已有的CHECK:STDOUT:/CHECK:STDERR:仍须全部命中 |
// --- <filename> | 把物理文件切成多个逻辑文件;文件不落盘,而是经fs传入Run;文件名若为STDIN则改走input_stream |
// CHECK:STDOUT:/// CHECK:STDERR: | 对命令输出的匹配行,支持[[@LINE+offset]]与{{regex}}语法(类似 FileCheck) |
// TIP: <tip> | 由 autoupdate 生成的提示(如单测运行命令),不影响校验,autoupdate 可按需更新或删除 |
如何注册一个新的 file_test
若要在仓库中为新的测试程序接入这套框架,Bazel 侧与 C++ 侧的最小骨架如下(均来自 testing/file_test/README.md):
load("rules.bzl", "file_test") file_test( name = "my_file_test", srcs = ["my_file_test.cpp"], tests = glob(["testdata/**"]), deps = [ ":my_lib", "//testing/file_test:file_test_base", "@googletest//:gtest", "@llvm-project//llvm:Support", ], )#include "my_library.h" #include "testing/file_test/file_test_base.h" namespace Carbon::Testing { namespace { class MyFileTest : public FileTestBase { public: using FileTestBase::FileTestBase; auto Run(const llvm::SmallVector<llvm::StringRef>& test_args, const llvm::SmallVector<TestFile>& test_files, FILE* input_stream, llvm::raw_pwrite_stream& output_stream, llvm::raw_pwrite_stream& error_stream) -> ErrorOr<RunResult> override { return MyFunctionality(test_args, input_stream, output_stream, error_stream); } auto GetDefaultArgs() -> llvm::SmallVector<std::string> override { return {"default_args", "%s"}; } }; } // namespace CARBON_FILE_TEST_FACTORY(MyFileTest); } // namespace Carbon::Testing其中Run()负责在单条测试执行时接收参数与文件并产出输出流,GetDefaultArgs()为未提供ARGS的测试提供默认参数(典型值为%s文件占位),最后通过CARBON_FILE_TEST_FACTORY注册进框架。rules.bzl的实现还会额外生成 per-file 手动测试,便于开发者针对单个 testdata 文件调试。
小结
Carbon 工具链的 file_test 体系把“写测试”变成了一件高度模板化、可自动维护的工作:用AUTOUPDATE声明可更新性,用INCLUDE-FILE裁剪 prelude 以提速,用 split +[[@TEST_NAME]]在单文件内组织多场景,用fail_/todo_前缀区分预期与非预期行为,用dump-sem-ir-begin/end裁剪输出,最后交给 autoupdater 生成权威的CHECK行。遵循这套约定,既能保证测试输出的稳定性与可审查性,也让 Carbon 语言本身的每次演进都有可信的回归防线。
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考