scriptc测试语料库设计解析:700多个差分测试用例如何维护
【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptc
scriptc 是一个将 TypeScript / JavaScript 直接编译为原生可执行文件的编译器(TypeScript-to-Native Compiler)。它的正确性主要靠一套差分测试(differential testing)语料库来保证:tests/corpus/下约有 1700 个程序文件、1700 多个测试入口,每一个程序都会被 Node 和 scriptc 编译出的原生二进制各跑一遍,然后逐字节比对 stdout、stderr 与退出码。下面解析这套 700 多个差分测试用例的组织方式,以及它们是如何被低成本地持续维护的。
差分测试核心原理:为什么用 Node 做"裁判"
传统编译器测试常采用"黄金文件"(golden file):预先存好期望输出,再与编译器产物对比。这种方式最大的痛点是期望文件会漂移——语言语义变了、环境变了,就得人工逐个修期望值。
scriptc 的做法完全不同。打开 differential.test.ts 就能看到它的核心信条:
没有黄金文件——Node 本身就是期望输出,所以测试不会漂移。
具体流程是:
- 同一个语料程序,Node 直接运行(它是"裁判",即 oracle);
- scriptc 把它编译成 LLVM 原生二进制并运行;
- 两边输出逐字节比对,退出码必须一致;
- 任一侧拒绝编译(refusal),测试直接失败。
🔑 这样期望值就永远跟着 Node 走,700 多个用例不需要单独维护任何期望文件——这是整个语料库"可长期维护"的第一块基石。
用例组织:编号命名约定,让 1700 多个文件保持秩序
语料库就放在tests/corpus/,支持.ts / .js / .mjs / .cjs四种入口(JS 一等公民,走 checkJs 类型推断)。命名遵循"编号-主题"约定,编号即功能域,一眼定位:
| 编号段 | 覆盖内容 | 示例 |
|---|---|---|
001–199 | 基础语法与语言特性 | 001-hello.ts |
1000–1099 | JSON、异步、Promise | 1020-async-basics.ts |
1200–1299 | 正则 | 1200-regex-test-basics.ts |
1440–1589 | 定时器、流、子进程、IO | 1685-stream-readable-basics.ts |
1830–1999 | enum、装饰器、命名空间等 TS 高级特性 | 1970-decorators-basics.ts |
2382–2663 | 模块循环、顶层 await 等疑难场景 | 2655-top-level-await-cycle/main.ts |
| 无编号目录 | 主题性集合(CJS 互操作、mustcall 等) | tests/corpus/2390-dot-requires/ |
新增用例的维护成本极低:往tests/corpus/里丢一个文件(或一个目录),测试框架 glob 到它就自动纳入差分对比,无需改任何测试代码——这是"700 多用例还能继续增长"的关键。
指令头:两行注释扩展整个测试框架
每个语料程序的文件头两行是"指令区"(directive head),用注释就能改变测试行为,框架侧零配置:
// @exit: 1—— 声明程序预期以非零码退出(如 1599-js-uncaught-throw.js)。这类用例的 stderr 不做字节比对(未捕获异常的报错格式是文档化的差异点),stdout 仍严格一致;// @dynamic—— 以嵌入 JS 引擎的动态岛模式编译,Node 侧由 island-shim.mjs 提供对应语义,仍是同一个裁判;// @transform-types—— 程序里用了 Node 无法直接执行的 TS 语法(如命名空间),Node 侧改用 transform 模式运行;// @no-deprecation—— 屏蔽 Node 弃用警告中的 pid,保证可字节比对。
指令只依赖"文件前两行",因此天然可进缓存键:同一份程序字节永远映射到同一套指令解释,测试框架不需要为每个用例写特殊分支。
多模块目录测试:入口 + 兄弟模块
单文件用例之外,语料库还支持目录形式:以<name>/main.<ext>为入口、同目录其他文件作为兄弟模块(共约 150 组)。
例如 tests/corpus/951-modules-diamond/ 就包含main.ts、a.ts、b.ts、shared.ts四个文件,专门覆盖"diamond"菱形依赖这种 import 解析场景。这类用例的缓存键会递归哈希目录内所有源文件,加上tsconfig.json/package.json等两侧都会读取的配置——任何一次 import 改动都会自动击穿缓存重新编译,杜绝"改了模块 A 却拿旧二进制比对"的假阴性。
维护策略:缓存、分片与多平台 lane
语料库规模扩大后,"跑得动、跑得便宜"和"写得对"同样重要。harness 的维护设计可以拆成四层。
1️⃣ 三层缓存,让 700 多用例只跑增量
- Oracle 缓存:Node 的判定是"程序字节 + shim 内容 + Node 版本 + 调用形态"的纯函数,结果按 SHA-256 缓存;
- 时序敏感用例强制实时:用到
setTimeout/Promise.race等交错行为的程序(约 18 个)以及依赖易变主机状态(网卡地址、系统 CA 证书)的程序永远实时跑 Node——这是从真实 flaky 案例中换来的教训; - 缓存身份验收测试:
pnpm test:cache-identity会让整套用例分别以"无缓存、填缓存、命中缓存"三种方式各跑一遍,diff 每个用例的结果,任何漂移直接失败。
2️⃣ 稳定哈希分片,并行 CI 不抖动
CI 通过SCRIPTC_TEST_SHARD=i/n把语料库切成矩阵分片。分片逻辑在 shard.ts:对用例名的 SHA-1 哈希取模,而不是列表下标。好处是——语料库在它旁边新增用例时,已有用例的分片归属不变,编译缓存跨增长保持温热;且哈希是全域函数,n 个分片的并集恰好覆盖每个用例一次(由 shard.test.ts 固定该性质)。
3️⃣ 五道 lane,同一套语料库多平台复用
- 净化 lane:
SCRIPTC_SAN=1下每个程序用 AddressSanitizer + 运行时引用计数审计重跑,整个语料库自动变成"泄漏 / use-after-free 测试集"; - Linux lane:Docker 内走 Zig 链接,与容器里的 Linux Node 字节比对(gnu / musl 双发行版);
- Windows lane:交叉编译
.exe送到 Windows 机器,双侧在真实 Windows Node 上跑,零归一化; - 库模式 lane:对 6 个目标三元组交叉构建并逐归档校验符号精确性。
新平台不是"再写一套用例",而是同语料库、新执行面,边际维护成本趋近于零。
4️⃣ Test262 回归:外部标准套件的第二道防线
除自研语料外,scriptc 还挂了一套固定版本的 Test262 回归档案:upstream.json 钉死上游 revision、档案校验和与完整快照摘要;expectations.json记录每个用例的预期结果,"记录的拒绝保持拒绝、结果变化即失败"。扩展规则也很克制:审查原始用例 → 原样拷贝源码与许可 → 登记路径与 SHA-256 → 在 Node 与 scriptc 两侧执行。细节见 tests/test262/README.md。
小结:700 多用例可维护的 5 条设计原则
- Node 即裁判:期望值不落地、不漂移,编译器只需"跟 Node 一致";
- 字节级契约:对比 stdout/stderr/退出码而非模糊断言,差异定位精确到字节;
- 指令头而非配置:两行注释扩展框架能力,用例自描述;
- 一切入哈希:缓存键、分片归属都是程序字节的纯函数,局部改动局部重跑;
- 同语料多执行面:净化构建、Linux/Windows/库模式 lane 与 Test262 回归复用同一份资产。
这套"差分 + 自描述用例 + 哈希化维护"的组合,是脚本编译器在 700 多个测试用例上保持长期绿色门禁的答案,也很值得其他需要大规模行为对比的编译器 / 运行时项目借鉴。
【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考