Hermes 测试运行器(Test Runner)完全指南:test262 / mjsunit / esprima / flow / CVE 测试套件集成运行与 Skiplist 管理
2026/9/24 17:23:03 网站建设 项目流程
  • 语言运行时
  • 编译器
  • 移动开发

【免费下载链接】hermes

A JavaScript engine optimized for running React Native.

项目地址:https://gitcode.com/gh_mirrors/hermes/hermes
点击查看免费下载

本篇技术指南聚焦 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 元数据(flagsnegativefeaturesincludes),需要预处理后才能运行;
  • 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"):

  1. 测试套件目录:当前支持的套件根目录,包括 test262、mjsunit、esprima、flow 以及 CVE 套件;多个套件路径可用空格分隔一次传入,同时运行多套测试;
  2. 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/flowexternal/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/Test262Suitetest262
mjsunit/MjsunitSuitemjsunit
CVEs/CvesSuite(继承Test262SuiteCVEs
esprima/EsprimaAstCheckSuiteesprima
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自动临时目录生成预处理测试文件的工作目录;若已存在会先删除
--timeout200(秒)单个测试编译与运行的超时上限
--show-slowest-tests N0(关闭)结束时打印耗时最长的 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-match

generate_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_featurestest262 中暂不支持的特性(未来可能支持),按 feature 名匹配SKIPPED
permanent_unsupported_featurestest262 中不支持且无支持计划的特性,按 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 条(含AtomicsSharedArrayBufferIsHTMLDDAcross-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),每个测试文件依次检查:

  1. 默认跳过分类:skip_listpermanent_skip_listmanual_skip_listplatform_skip_list(lazy 模式追加lazy_skip_list);
  2. Intl 分类:intl_tests单独检查,只有传--test-intl才运行;
  3. test262 特性分类:对每个features元数据,先查构建出的hermes --version输出中动态支持的特性(见 utils/testsuite/utils.py 的get_hermes_supported_test262_features(),当前映射如Unicode RegExp Property Escapesregexp-unicode-property-escapes),支持则跳过 Skiplist 检查,否则再查unsupported_features/permanent_unsupported_features

跳过结果映射到SKIPPEDPERMANENTLY_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

行为细节如下:

  1. 加了--test-skiplist后,原本会被跳过的测试照常执行--lazy时额外反跑lazy_skip_list--test-intl时额外反跑intl_tests);
  2. 结束后,运行器打印所有已通过的 Skiplist 测试("Passed tests in skiplist:");
  3. 然后通过input()提示Remove these passed tests from skiplist? [y]es/[n]o:,确认后把这些测试从配置中移除(见 utils/testsuite/cli.py 的remove_tests_from_skiplist());
  4. 目前只有skip_listpermanent_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 慢测试。

TestResultCodeis_failure属性(utils/testsuite/utils.py)定义很关键:只有TEST_PASSEDTEST_SKIPPEDTEST_PERMANENTLY_SKIPPED不算失败,其余全部计为失败,这也决定了进程退出码(存在失败返回 1,否则返回 0)。

九、并发调度与测试执行的源码原理

运行器采用asyncio + 子进程 + 信号量的并发模型,核心逻辑在 utils/testsuite/cli.py 的run()中:

  1. 先递归收集所有测试文件(list_all_files()),为每个文件构造TestRunArgs
  2. asyncio.Semaphore(n_jobs)限制存活任务数,每个任务包裹一个子进程调用;
  3. asyncio.as_completed()按完成顺序消费结果,任务完成后释放信号量、让新任务进入;
  4. 进度通过ProgressBar/SimpleProgressBar显示(-v时输出每条用例的中间结果)。

对每个 test262 用例,Test262Suite.run_test()(utils/testsuite/test_run_defs.py)的完整流程是:

  1. 读取源码,交给 utils/testsuite/preprocess.py 的generate_source()解析 frontmatter:确定flagsonlyStrict/noStrict/raw/async/module等)、strict_modenegative(期望失败的phasetype)、features,并把includes指定的 harness 文件(如sta.jsassert.js,async 用例还会附加doneprintHandle.js)内容拼接到源码前;
  2. module标志的用例直接 SKIP;包含testIntl.jsharness 的用例 SKIP(当前不支持多 Intl 构造器);async标志的用例在结果判定时额外检查 stdout 中的Test262:AsyncTestComplete/Test262:AsyncTestFailure标记(见 utils/testsuite/hermes.py);
  3. 根据strict_mode生成一个或两个变体文件(严格模式变体文件名带.strict,且会在开头插入'use strict';);
  4. negative.phase校验期望失败阶段:parse期望编译失败,runtime期望运行抛错——预期与实际不符都判 FAIL(utils/testsuite/hermes.py 的compile_with_args()run());
  5. 若命中handlesan_skip_list,追加-gc-sanitize-handles=0运行参数;
  6. 最后统计各变体的"编译 + 运行"总耗时并返回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目录中同时存在hermesb_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.

项目地址:https://gitcode.com/gh_mirrors/hermes/hermes
点击查看免费下载

相关推荐

上一篇:MLflow AI Gateway 集成 Hugging Face Text Generation Inference(TGI)实战指南
下一篇:WezTerm Lua API 详解:wezterm.procinfo.pid() 获取当前进程 ID

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询