Deno tests/specs:用 JSONC 驱动的二进制集成测试及其源码级实现
Deno 仓库的 tests/specs/ 目录是整个 CLI 首选的集成测试体系:每个测试目录下的 __test__.jsonc 文件以声明式方式描述“要执行的 deno 命令 + 期望输出”,测试框架负责运行真实的 deno 二进制并比对 stdout/stderr。本文以 tests/specs/README.md 为主体,完整覆盖测试目录结构、清单文件格式、过滤运行方式与 .out 断言语法,并结合 tests/specs/mod.rs 的解析与执行代码,说明这些声明式字段在运行时如何被翻译为真实的进程调用与断言。读完后你将能够独立编写、运行和排查 Deno 的 spec 测试,并理解其多步骤、多变体、跨平台条件的底层机制。
Spec 测试的定位与目录结构
tests/specs/README.md 开宗明义:
These are integration tests that execute the
denobinary. They are the preferred way of writing tests that use thedenobinary.
也就是说,spec 测试直接拉起编译好的 deno 可执行文件、在隔离环境中执行子命令,再对输出做匹配——这与 tests/integration/ 下基于 Rust 编程式断言的测试互补,但对“命令 + 输出”这类场景,JSONC 清单比手写 Rust 断言表达力更直接、维护成本更低。
测试必须遵循如下目录结构:
tests/specs/<category_name>/<test_name>/__test__.json
即按“分类/测试名”两级目录组织,每个叶子目录放一个测试清单文件。当前仓库中这一结构实际规模可观:tests/specs/ 下约有 58 个分类目录(run、cache、check、fmt、lint、jsr、npm、node、publish 等),共 2000 余个清单文件与 2800 余个 .out 期望输出文件。从源码结构看,manifest 的实际文件名是 __test__.jsonc(支持注释的 JSONC),这一点由 tests/specs/mod.rs 中的常量确认:
const MANIFEST_FILE_NAME: &str = "__test__.jsonc";
因此 README 中写作 __test__.json 可理解为格式说明,新写测试时应使用 .jsonc 扩展名以享受 schema 校验与编辑器提示。
测试清单的三种组织形式
__test__.jsonc 文件描述“要执行哪些测试、按什么步骤执行”。README 给出了三种典型形态。
形式一:单步测试(最常用)
{
"args": "run main.js",
"output": "main.out"
}
语义是:执行 deno run main.js,随后断言实际输出与 main.out 文件中的文本匹配。这是最简单也最高频的形态。仓库真实示例(tests/specs/run/_001_hello/__test__.jsonc):
{
"args": "run --reload 001_hello.js",
"output": "001_hello.js.out"
}
形式二:多步骤测试(steps 数组)
{
"tempDir": true,
"steps": [{
"args": "cache main.ts",
"output": "cache.out"
}, {
"args": "run main.ts",
"output": "error.out",
"exitCode": 1
}]
}
步骤按顺序执行,任一步骤失败则测试失败。上例先 deno cache 预缓存模块,再验证 run 因权限等原因以退出码 1 结束。tempDir: true 表示先把测试目录内的非断言文件复制到临时目录、再在其中执行命令(原因与实现见下文“临时目录机制”)。
形式三:一个文件多个测试(tests 对象)
{
"tests": {
"ignore_dir": {
"args": "run script.ts",
"output": "script.out"
},
"some_other_test": {
"args": "run other.ts",
"output": "other.out"
}
}
}
同一目录下定义多个命名测试,各自独立执行、独立断言。从源码看(tests/specs/mod.rs 中 map_test_within_file 函数),框架在收集阶段就会检查文件是否含 tests 键:含则反序列化为 MultiTestMetaData 并展开为测试子项(每个子项命名为 父测试::子项名);否则按单/多步测试处理。
顶层属性
README 列出的顶层属性如下,可结合源码中的反序列化结构(SingleTestMetaData / MultiStepMetaData / MultiTestMetaData)理解其完整集:
| 属性 | 类型 | 说明 |
|---|---|---|
repeat |
number | 测试重复执行的次数,任一次失败即整体失败 |
tempDir |
boolean | 将所有非测试文件复制到临时目录并在其中执行。默认测试的当前工作目录就是测试目录本身;但对于会创建 node_modules 等目录的测试,直接污染源码树不可接受,需使用临时目录 |
此外,源码中还可识别以下顶层字段(README 未逐一展开,写复杂测试时有用):
envs(object):注入给所有步骤的环境变量;cwd(string):覆盖步骤的执行目录;ignore(boolean):跳过整个测试;timeout(number):每个步骤的超时秒数,源码注释标明默认 300 秒(5 分钟);variants(object):定义可复用变量集,每个变体展开为一个子测试(见下文“变体机制”);canonicalizedTempDir/symlinkedTempDir(boolean):对临时目录做规范化 / 软链处理,源码注释提醒应谨慎使用(后者仅在 debug 构建允许,用于模拟 CI 的软链环境)。
步骤(Step)属性
README 指出:当只写单步时,步骤属性可以直接放在顶层而不必嵌套进 steps 数组或 tests 对象。步骤支持的全部字段:
args:字符串(会按空白拆分为参数数组)或参数数组;output:期望输出。可以是路径(必须以.out结尾,指向期望文本文件),也可以是直接内联的文本(与输出做模式匹配);flaky:标记该步骤为 flaky,失败时最多重试 3 次;if("windows"/"linux"/"mac"/"unix"):控制该步骤是否在对应平台运行;exitCode(number):期望退出码。
源码(StepMetaData 结构)中另有若干实战字段:
cwd:仅覆盖本步骤的工作目录;commandName:替换被执行的命令名(默认是deno),可用于测试其它可执行程序;envs:仅本步骤的环境变量,会与顶层envs合并(步骤优先);input:喂给子进程 stdin 的文本;若以.in结尾,则视为同目录下的输入文件路径。
一个值得注意的实现约束:内联 output 文本长度不得超过 160 字符,否则测试会直接失败,报错信息为 The "output" property in your __test__.jsonc file is too long. Please extract this to an .out file to improve readability.——这是对可读性的硬性规约,长期望输出必须落成 .out 文件。
if 条件的完整取值
README 只列了四个平台条件,而 tests/specs/mod.rs 中 should_run 函数实际支持更多条件(作用于步骤或测试级):
| 条件 | 含义 |
|---|---|
windows / unix / mac / linux |
平台条件 |
notCI |
仅本地运行(未设置 CI 环境变量时) |
notMacIntel |
排除 macOS x86_64(Intel 虚拟机) |
notWindowsArm |
排除 Windows on ARM |
notWindowsArmOrMacIntel |
同时排除以上两类 CI 弱环境 |
notSlowCiShard |
在 CI 分片策略中避开慢速分片 |
写跨平台测试时,这些条件比在 CI 脚本里做过滤更内聚。
运行与过滤
README 给出的运行方式:
cargo test specs::category_name::test_name
或只做子串匹配(可能命中其它测试):
cargo test test_name
需要查看每个测试的实时输出时追加 -- --no-capture(注意:这会令测试串行执行而非并行):
cargo test test_name -- --no-capture
仓库还封装了更方便的入口 tools/x.ts 中的 test-spec 子命令,其帮助文本说明“每条 spec 测试定义了要执行的 CLI 命令,并对 stdout/stderr 做断言,通配符包括 [WILDCARD]、[WILDLINE] 等”:
./x test-spec <filter> Run tests matching the filter
./x test-spec --list List all available tests
./x test-spec fmt # 运行名字含 "fmt" 的 spec 测试
./x test-spec run # 运行名字含 "run" 的 spec 测试
底层等价于 cargo test -p specs_tests --test specs -- <filter>。另外源码 main() 中有一处防误操作保护:当检测到 CLAUDE_CODE_ENTRYPOINT 环境变量(即从 Claude Code 会话中触发)且没有提供过滤参数时,会直接 panic 拒绝全量跑 spec 套件,避免意外执行整个集成测试集。
.out 文件与通配断言语法
.out 文件是输出断言的载体。为容忍路径、内存地址、耗时等不确定内容,文件内支持如下匹配标记:
| 标记 | 语义 |
|---|---|
[WILDCARD] |
匹配该处的任意文本 |
[WILDLINE] |
匹配当前行的任意文本 |
[WILDCHAR] |
匹配下一个字符 |
[WILDCHARS(5)] |
匹配接下来任意 5 个字符 |
[UNORDERED_START] … [UNORDERED_END] |
区间内各行按任意顺序匹配(用于非确定性输出) |
[# example] |
行内注释,以 [# 开始、] 结束 |
配合步骤的 exitCode 断言(源码中通过 output.assert_exit_code(step.exit_code) 执行),可以同时锁定“退出状态 + 输出文本”两个维度。
IDE 自动补全(JSON Schema)
README 建议在本地 .vscode/settings.json 中登记 schema 以获得补全与校验。注意其中的 URL 是相对仓库根目录的路径,schema 文件实际位于 tests/specs/schema.json:
{
"json.schemas": [{
"fileMatch": [
"__test__.jsonc"
],
"url": "./tests/specs/schema.json"
}]
}
该 schema 为 draft-07 格式,将单步测试定义为 args 与 output 两个必填项,并对 cwd、commandName、envs、flaky、canonicalizedTempDir 等字段给出类型约束,与上文“步骤属性”一节相互印证。
源码级实现走读:从 JSONC 到进程调用
理解以下调用链能解释很多“为什么这样写”的疑问,全部位于 tests/specs/mod.rs:
1. 收集阶段(main / map_test_within_file)。框架用 TestPerDirectoryCollectionStrategy 以 __test__.jsonc 为锚点扫描 tests/specs/ 下每个目录,再调用 map_test_within_file 判定文件形态:含 tests 键 → 多测试分类;含非空 variants → 展开为变体子测试;否则作为单/多步测试登记。解析入口 deserialize_value 先看有无 steps 键来决定反序列化到 MultiStepMetaData 还是 SingleTestMetaData(后者经 into_multi() 归一化为单步),从而让三种写法在运行期收敛为统一的“步骤列表”模型。
2. 执行环境(test_context_from_metadata)。根据元数据构建 TestContext:tempDir 为真时改用 use_temp_cwd(),否则 cwd 就是测试目录本身;未指定 base 时,默认注入 JSR、npm 与 compile 相关的环境变量(add_jsr_env_vars 等)。
3. 临时目录的复制规则。tempDir: true 时,cwd.copy_to_recursive_with_exclusions 会把测试目录整体拷入临时目录,但通过 resolve_test_and_assertion_files 排除 manifest 本身和所有 .out 断言文件——所以断言文件在临时 cwd 中不存在,而 output 断言时又按原始测试目录拼接路径(cwd.join(&step_output)),保证期望值始终来自源码树。
4. 步骤执行(run_step)。按“合并环境变量 → 应用变体替换 → 拼接 args(VecOrString)→ 设置 cwd / commandName → stdin(input)→ 超时(timeout)→ 执行”的顺序组装子进程;输出断言按 output 是否以 .out 结尾分流到 assert_matches_file 或 assert_matches_text,最后校验 exitCode。--no-capture 由全局 NO_CAPTURE 开关映射为 command.show_output(),这就是它导致串行的原因。
5. 变体机制(variants)。map_variants 将每个变体展开为 父测试::变体名 子测试;variant_substitutions 把变体里的字符串值映射成 ${name} → 值 的替换对,随后在 args、output、envs 中统一替换(替换后变空的参数会被剔除)。这使得同一份步骤模板可以在不同参数(如不同权限标志、不同 registry 地址)下复用,而不用复制粘贴多个目录。
6. 稳定性设施。flaky 步骤经 run_maybe_flaky_test 重试;CI 环境下所有测试默认按 flaky 处理(metadata.flaky || *IS_CI);main 开头还会对 tests/specs、tests/util、tests/testdata、tests/registry 与相关二进制做 CI hash 校验,未变更时直接跳过整套 spec 测试,加速 CI 反馈。
编写与排查指南小结
综合 README 与源码,编写一个新 spec 测试的完整流程是:
- 在
tests/specs/<category>/<test_name>/下建目录; - 写
__test__.jsonc(单步写args+output即可);会污染 cwd 的测试加tempDir: true; - 长输出落成
.out文件,不确定内容用[WILDCARD]/[UNORDERED_START]等标记,内联断言控制在 160 字符内; - 用
cargo test specs::<category>::<test_name>(或./x test-spec <filter>)过滤运行;排障时加-- --no-capture看实时输出; - 编辑器按上文配置 tests/specs/schema.json 获得补全。
排查时可按失败形态对照:退出码不符看 exitCode 与实际 stderr;输出不符打开 .out 检查是否漏写 [WILDLINE] 或平台相关路径差异(必要时用 if 条件区分平台);临时目录类失败检查是否误把 .out 文件依赖当成了 cwd 内文件(它们不会被复制进临时目录);超时则显式设置 timeout。
这套机制的价值在于:它把“跑什么命令、期望什么输出”沉淀为可读、可 diff、可被 schema 校验的数据文件,让 Deno 两千多个 CLI 行为的回归验证不依赖大量易腐化的 Rust 断言代码——这正是它被定位为使用 deno 二进制的“preferred way”的原因。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00