- 语言运行时
- 编译器
- 移动开发
【免费下载链接】hermes
A JavaScript engine optimized for running React Native.
本篇技术指南聚焦 Hermes JavaScript 引擎仓库中的utils/testsuite/测试运行器(Test Runner),它负责将 test262、mjsunit、esprima、flow 与 CVE 等外部/内部测试套件统一接入 Hermes,通过异步并发方式批量编译并执行每个测试用例、汇总通过率并输出失败明细。读完本文,你将掌握该运行器的完整命令行用法、编译/VM 参数注入方式、九个 Skiplist 分类的语义与维护流程,以及从源码层面理解其并发调度、测试预处理与结果判定的实现原理。
一、为什么需要一个统一的 Test Runner
Hermes 作为面向 React Native 的 JavaScript 引擎,需要持续验证两件事:语言规范符合度(与 ECMAScript 规范的一致性)与AST/语法兼容性(对 esprima、flow 生态语法树的还原能力)。这两类验证分别依赖不同的测试资产:
- test262:ECMAScript 官方一致性测试套件,用例带有 frontmatter 元数据(
flags、negative、features、includes),需要预处理后才能运行; - mjsunit:V8 的回归测试风格套件,要求直接运行源码并校验编译/运行时错误;
- esprima / flow:仅做 AST 校验,需要把源码交给 Hermes 解析后与预期的
*.tree.json/*.failure.json结果比对; - CVEs:与 test262 同风格 frontmatter 的安全回归用例。
如果为每一套件各写一套脚本,参数、统计口径与跳过逻辑会严重重复。Hermes 的做法是提供一个统一入口 utils/test_runner.py,它只是薄薄一层包装:导入testsuite.cli并执行asyncio.run(main()),真正的逻辑全部落在utils/testsuite/包内。README(即 utils/testsuite/README.md)给出了该运行器的设计定位与用法概要,本文以下内容将结合仓库源码逐层展开。
二、快速开始:运行一个测试套件
运行器需要两类输入路径(见 utils/testsuite/README.md 的 "How to run"):
- 测试套件目录:当前支持的套件根目录,包括 test262、mjsunit、esprima、flow 以及 CVE 套件;多个套件路径可用空格分隔一次传入,同时运行多套测试;
- Hermes 二进制目录:包含
hermes可执行文件(若启用--shermes还需shermes,若启用--bytecode-compat-check还需b_hermes)的构建输出目录,通常是 CMake 构建目录下的bin/。
README 中的示例用法如下(假设./build是 cmake 构建目录):
# flow 套件 ./utils/test_runner.py external/flowtest/test/flow -b ./build/bin # esprima 套件 ./utils/test_runner.py external/esprima/test_fixtures -b ./build/bin其中external/flowtest/test/flow与external/esprima/test_fixtures均位于本仓库的external/目录下(见 external/flowtest/test 与 external/esprima/test_fixtures),可直接使用。test262、mjsunit 与 CVE 套件则需要自行准备对应目录(仓库内test/下存放的是 Hermes 自身的回归用例,与上述外部套件不同)。
运行前,utils/testsuite/utils.py 中的check_hermes_exe()会校验-b指定的目录中是否存在所需可执行文件,缺失时直接报错退出;路径参数既可以是目录(递归收集其中所有.js文件,test262目录下以_FIXTURE.js结尾的 fixture 文件会被排除),也可以是单个文件。
三、支持的测试套件与自动识别机制
运行器如何知道一个测试文件属于哪套测试?答案在 utils/testsuite/test_run_defs.py 的Suite.create():它按路径子串识别,从路径末尾往前查找第一个匹配的目录名,避免路径中出现重复目录名时误判:
| 路径子串 | Suite 类 | 套件名 |
|---|---|---|
test262/ | Test262Suite | test262 |
mjsunit/ | MjsunitSuite | mjsunit |
CVEs/ | CvesSuite(继承Test262Suite) | CVEs |
esprima/ | EsprimaAstCheckSuite | esprima |
flow/ | FlowAstCheckSuite(注意用flow/前缀避免误匹配flowtest/) | flow |
每个用例的完整测试名格式为{suite_name} :: {相对路径},例如mjsunit :: regress-011.js,这个名称会出现在输出、Skiplist 更新交互与最终统计中。
不同的 Suite 决定不同的运行策略:
- Test262Suite / CvesSuite:先解析 test262 风格 frontmatter 做预处理,再执行"编译 + 运行"两阶段流程(详见第四节);
- MjsunitSuite:同样走"编译 + 运行",但无需 test262 harness 逻辑;
- AstCheckSuite(esprima/flow 基类):调用
generate_ast()生成 AST,与期望文件比对。期望文件按优先级查找*.tree.json→*.failure.json→*.tokens.json(后者不被支持、直接 SKIP),见 utils/testsuite/test_run_defs.py。
四、命令行参数详解
运行器的全部参数在 utils/testsuite/cli.py 的create_parser()中定义。README 只点出了其中最关键的几个,下表基于源码补齐了完整参数集、默认值与作用:
| 参数 | 默认值 | 说明 |
|---|---|---|
paths(位置参数,nargs="+") | 必填 | 测试套件路径,可传目录或文件、可传多个(空格分隔) |
-b, --binary-dir | 包根目录 | Hermes 构建产物目录,需含hermes(必要时含shermes/b_hermes) |
-j, --jobs | 逻辑 CPU 数(兜底 10) | 同时运行的测试任务数,用于限制资源争用 |
--compile-args | 空 | 追加的编译器参数,多个参数用空格分隔(nargs="+") |
--vm-args | 空 | 追加的 VM(运行字节码)参数,多个参数用空格分隔 |
--test-skiplist | 关闭 | 强制运行 Skiplist 配置中的测试,通过后询问是否从配置移除 |
--test-intl | 关闭 | 运行需要 INTL 的测试(对应intl_tests分类) |
--lazy | 关闭 | 强制惰性求值(lazy evaluation)模式,与--shermes互斥 |
--shermes | 关闭 | 使用shermes二进制测试,与--lazy互斥 |
--opt | 关闭 | 编译时启用优化器,用-O而非-O0 |
--bytecode-compat-check | 关闭 | 用hermes编译、b_hermes运行,校验字节码兼容性(对 shermes/lazy 模式无效果) |
-v, --verbose | 关闭 | 输出中间过程 |
-d, --dump-source | 关闭 | 不运行测试,仅把预处理后的源码打印到 stdout(此时只允许传一个路径) |
--work-dir | 自动临时目录 | 生成预处理测试文件的工作目录;若已存在会先删除 |
--timeout | 200(秒) | 单个测试编译与运行的超时上限 |
--show-slowest-tests N | 0(关闭) | 结束时打印耗时最长的 N 个测试 |
其中--compile-args/--vm-args支持一种实用的小技巧:由于 argparse 会把以-开头的值误判为标志,源码在 utils/testsuite/cli.py 提供了strip_nargs(),允许在参数值前后补空格传入,例如--compile-args ' -Xes6-block-scoping'。
五、默认编译与 VM 标志
README 指出:默认情况下,test262 与 CVE 套件会使用以下编译器/VM 标志。这些标志在 utils/testsuite/hermes.py 中定义为常量:
# 编译器(hermes 编译字节码时追加) -test262 -fno-static-builtins -Xes6-block-scoping -Xenable-tdz -Xasync-generators # VM(运行字节码时追加) -Xes6-proxy -Xhermes-internal-test-methods -Xmicrotask-queue各标志的作用从名称与上下文可推断:
-test262:启用 test262 兼容的宿主环境(如提供$262对象);-fno-static-builtins:关闭静态内建函数优化,保证内建函数可被测试覆写;-Xes6-block-scoping/-Xenable-tdz:开启 ES6 块级作用域与暂时性死区(TDZ)检查;-Xasync-generators:开启 async generator 支持;-Xes6-proxy:开启 ES6 Proxy;-Xhermes-internal-test-methods:暴露内部测试方法;-Xmicrotask-queue:启用微任务队列(async 测试依赖 Promise 微任务调度)。
运行字节码时,utils/testsuite/hermes.py 还会附加-b(以字节码方式运行.out文件)与-Xes6-proxy、-Xhermes-internal-test-methods、-Xmicrotask-queue;在 lazy 模式下则改为直接运行源码并追加-lazy与上述 COMPILE_ARGS。
由于 esprima 套件只做 AST 校验,运行器对 esprima 直接使用-dump-ast(见 utils/testsuite/hermes.py 的generate_ast());而 flow 套件在此基础上追加:
-parse-flow -Xparse-component-syntax -parse-jsx -Xinclude-empty-ast-nodes -Xparse-flow-matchgenerate_ast()中还会根据测试文件路径是否含JSX追加--parse-jsx,并支持-dump-transformed-ast(当首次解析未报错、但期望文件要求失败时,用语义校验模式重跑一次,见 utils/testsuite/test_run_defs.py)。对于 esprima 套件中形如var source = "..."的*.source.js文件,会先用 Hermes 求值出source变量的内容,再交给解析器处理(见 utils/testsuite/hermes.py)。
六、Skiplist 配置机制
skiplist.json(即 utils/testsuite/skiplist.json)是控制"哪些测试默认跳过"的配置文件。README 列出的九个分类在 utils/testsuite/skiplist.py 中被定义为SkipCategory枚举,与 README 一一对应:
| 分类键 | 语义 | 判定结果码 |
|---|---|---|
manual_skip_list | 人工维护:通常是"计划近期支持、支持后手动移除"的测试,也可附注释说明原因 | SKIPPED |
skip_list | 因规范不完全符合或特性未支持而当前失败的测试(可被--test-skiplist反跑移除) | SKIPPED |
lazy_skip_list | 传入--lazy时需要跳过的测试 | SKIPPED |
permanent_skip_list | 不计划修复/支持的测试 | PERMANENTLY_SKIPPED |
handlesan_skip_list | 需以-gc-sanitize-handles=0运行的测试(关闭句柄 sanitizer 以提速) | —(运行时附加参数) |
unsupported_features | test262 中暂不支持的特性(未来可能支持),按 feature 名匹配 | SKIPPED |
permanent_unsupported_features | test262 中不支持且无支持计划的特性,按 feature 名匹配 | PERMANENTLY_SKIPPED |
intl_tests | 需要启用 INTL 才能运行的测试,配合--test-intl使用 | SKIPPED |
platform_skip_list | 按平台跳过的测试,JSON 内按linux/darwin/win32分组 | SKIPPED |
从当前 utils/testsuite/skiplist.json 的实际规模看:skip_list约 1600+ 条、permanent_skip_list约 430 条、handlesan_skip_list约 86 条、unsupported_features22 条、permanent_unsupported_features4 条(含Atomics、SharedArrayBuffer、IsHTMLDDA、cross-realm)、lazy_skip_list1 条、intl_tests指向test262/test/intl402目录、manual_skip_list8 组、platform_skip_list覆盖 3 个平台。
配置项的值有两种形式:
- 纯字符串路径:可以是完整文件路径、目录路径(
.../formatRange/)或部分路径前缀(如mjsunit/mul-exhaustive-匹配多个文件); - 带注释的对象:形如
{"paths": [...], "comment": "..."},comment记录跳过原因(如 "Timeout on Sandcastle"、跟踪 issue 编号等),manual_skip_list中的条目几乎都带注释。
匹配逻辑在 utils/testsuite/skiplist.py 的should_skip_cat()中:字符串做子串包含匹配(所以目录路径与部分前缀都能命中),同时也支持正则 Pattern 匹配(供 test262 的 feature 匹配使用)。
运行时的跳过决策
主循环中(utils/testsuite/cli.py),每个测试文件依次检查:
- 默认跳过分类:
skip_list、permanent_skip_list、manual_skip_list、platform_skip_list(lazy 模式追加lazy_skip_list); - Intl 分类:
intl_tests单独检查,只有传--test-intl才运行; - test262 特性分类:对每个
features元数据,先查构建出的hermes --version输出中动态支持的特性(见 utils/testsuite/utils.py 的get_hermes_supported_test262_features(),当前映射如Unicode RegExp Property Escapes→regexp-unicode-property-escapes),支持则跳过 Skiplist 检查,否则再查unsupported_features/permanent_unsupported_features。
跳过结果映射到SKIPPED或PERMANENTLY_SKIPPED两种结果码(见 utils/testsuite/skiplist.py),并计入最终统计。
七、用--test-skiplist反跑并自动清理 Skiplist
这是 Skiplist 机制中最有价值的工作流,README 对其进行了重点描述:
# 强制运行 skiplist.json 中列出的所有测试 ./utils/test_runner.py external/flowtest/test/flow -b ./build/bin --test-skiplist行为细节如下:
- 加了
--test-skiplist后,原本会被跳过的测试照常执行(--lazy时额外反跑lazy_skip_list,--test-intl时额外反跑intl_tests); - 结束后,运行器打印所有已通过的 Skiplist 测试("Passed tests in skiplist:");
- 然后通过
input()提示Remove these passed tests from skiplist? [y]es/[n]o:,确认后把这些测试从配置中移除(见 utils/testsuite/cli.py 的remove_tests_from_skiplist()); - 目前只有
skip_list与permanent_skip_list两个分类会被自动更新(见 utils/testsuite/skiplist.py 的remove_tests()),其余分类不会被触碰。
针对"配置中写的是目录/部分路径"的情况,运行器会先执行unfold(展开):把目录路径递归展开为其中所有测试文件、把部分路径展开为所有匹配文件,剔除已通过的用例后,将剩余文件以文件粒度写回配置(utils/testsuite/skiplist.py)。这一设计的价值在于:skip_list中常见的整目录跳过项(如test262/test/intl402/NumberFormat/)在个别用例通过后,无需人工去拆目录。README 特别提醒:如果你不希望某个目录被自动展开,请把它放进manual_skip_list并附注释说明原因,因为manual_skip_list不会被自动更新。
八、结果统计与运行输出解读
每次运行结束,utils/testsuite/cli.py 的print_stats()会输出一张 ASCII 统计表,字段与 utils/testsuite/utils.py 中的TestResultCode一一对应:
----------------------------------- | Results | PASS | |----------------------+----------| | Total | 10000 | | Passes | 9850 | | Failures | 80 | | Skipped | 60 | | Permanently Skipped | 10 | | Pass Rate | 99.19% | ------------------------------------ Failures= Compile fail + Compile timeout + Execute fail + Execute timeout + Other(即
TEST_FAILED,用于 JSON 错误等未分类失败); - Pass Rate= Passes /(Total − Skipped − Permanently Skipped);
- 失败用例会在 "Details:" 段按 Compile failed / Compile timeout / Execute failed / Execute timeout / Other test failure 分组打印完整路径与输出;
- 配合
--show-slowest-tests N可输出按子进程墙钟耗时排序的 Top-N 慢测试。
TestResultCode的is_failure属性(utils/testsuite/utils.py)定义很关键:只有TEST_PASSED、TEST_SKIPPED、TEST_PERMANENTLY_SKIPPED不算失败,其余全部计为失败,这也决定了进程退出码(存在失败返回 1,否则返回 0)。
九、并发调度与测试执行的源码原理
运行器采用asyncio + 子进程 + 信号量的并发模型,核心逻辑在 utils/testsuite/cli.py 的run()中:
- 先递归收集所有测试文件(
list_all_files()),为每个文件构造TestRunArgs; - 用
asyncio.Semaphore(n_jobs)限制存活任务数,每个任务包裹一个子进程调用; - 用
asyncio.as_completed()按完成顺序消费结果,任务完成后释放信号量、让新任务进入; - 进度通过
ProgressBar/SimpleProgressBar显示(-v时输出每条用例的中间结果)。
对每个 test262 用例,Test262Suite.run_test()(utils/testsuite/test_run_defs.py)的完整流程是:
- 读取源码,交给 utils/testsuite/preprocess.py 的
generate_source()解析 frontmatter:确定flags(onlyStrict/noStrict/raw/async/module等)、strict_mode、negative(期望失败的phase与type)、features,并把includes指定的 harness 文件(如sta.js、assert.js,async 用例还会附加doneprintHandle.js)内容拼接到源码前; module标志的用例直接 SKIP;包含testIntl.jsharness 的用例 SKIP(当前不支持多 Intl 构造器);async标志的用例在结果判定时额外检查 stdout 中的Test262:AsyncTestComplete/Test262:AsyncTestFailure标记(见 utils/testsuite/hermes.py);- 根据
strict_mode生成一个或两个变体文件(严格模式变体文件名带.strict,且会在开头插入'use strict';); - 按
negative.phase校验期望失败阶段:parse期望编译失败,runtime期望运行抛错——预期与实际不符都判 FAIL(utils/testsuite/hermes.py 的compile_with_args()与run()); - 若命中
handlesan_skip_list,追加-gc-sanitize-handles=0运行参数; - 最后统计各变体的"编译 + 运行"总耗时并返回
TestCaseResult。
调试小技巧:-d, --dump-source可以把某个用例预处理后的源码(含插入的 harness 代码与'use strict';指令)直接打印到终端,方便确认预处理是否正确,此时只允许传入一个路径(utils/testsuite/cli.py)。
十、环境与兼容性注意事项
- 运行器为每个子进程传递当前环境变量(便于设置 shermes / ASAN 等选项),并强制
LC_ALL=en_US.UTF-8;Linux 下还会设置ICU_DATA指向二进制目录(utils/testsuite/hermes.py); progress.py模块源自 Hermes 所携带的 LLVM 工具external/llvh/utils/lit,经简化改造后使用(见 utils/testsuite/README.facebook);- 字节码兼容性检查(
--bytecode-compat-check)要求-b目录中同时存在hermes与b_hermes,且该模式在 shermes 与 lazy 模式下不生效; - 若要调试预处理逻辑,可直接阅读 utils/testsuite/preprocess.py(test262 元数据解析)与 utils/testsuite/external/parse_test262.py(frontmatter 语法解析,来自 test262 官方工具)。
通过上述机制,Hermes 团队得以在单一命令下持续跟踪数十万级测试用例的通过率,并把"新修复的用例从 Skiplist 中毕业"这一日常维护动作自动化——这正是 utils/testsuite/README.md 所描述的核心工作流,也是本文所有实操命令与配置的最终落点。
- 语言运行时
- 编译器
- 移动开发
【免费下载链接】hermes
A JavaScript engine optimized for running React Native.
相关推荐
curl 测试套件(Test Suite)完全指南:运行、调试与编写测试用例
curl 测试套件(Test Suite)完全指南:运行、调试与编写测试用例 本文围绕 curl 仓库中的测试套件展开,系统讲解如何从零构建并运行 curl 的
CLI网络通信vim-plug 测试套件完全指南:用 Vader.vim 运行与扩展插件管理器测试
vim plug 测试套件完全指南:用 Vader.vim 运行与扩展插件管理器测试 本篇指南围绕 vim plug 仓库的 test 测试目录 https:/
开发工具插件系统Flow 项目测试框架指南:使用 `./tool test` 编写与运行 newtests 集成测试
Flow 项目测试框架指南:使用 ./tool test 编写与运行 newtests 集成测试 导读 本指南基于 Flow 仓库中的 newtests/REA
开发工具静态分析代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考