首页
/ Ladybird 测试体系实战指南:从 test-web 四类测试到 Sanitizer 与 WPT 全流程

Ladybird 测试体系实战指南:从 test-web 四类测试到 Sanitizer 与 WPT 全流程

2026-09-05 15:40:38作者:戚魁泉Nursing

本篇指南基于 Ladybird 仓库的官方测试文档 Documentation/Testing.md 展开,并结合 Tests/LibWeb/test-web 测试运行器、Meta/ladybird.pyCMakePresets.json 的源码实现进行纵深讲解。读完后你将能够:独立跑通 Ladybird 的全部单测与 LibWeb 网页测试,复现 CI 的 Sanitizer 构建与失败场景,编写 Text / Layout / Ref / Screenshot 四类 LibWeb 测试并正确完成 rebaseline,以及用 Meta/WPT.sh 运行、对比与导入 Web Platform Tests。

测试体系总览:Tests/ 目录与每库一目录

Ladybird 的测试统一放在 Tests/ 目录下,每个被测试的库对应一个子目录,例如 Tests/AK 测试 AK 基础库,Tests/LibCore 测试 LibCore,Tests/LibJS 测试 JavaScript 引擎(含 1400 多个 JS 测试文件),Tests/LibWeb 则是网页引擎测试的核心阵地。

文档明确要求:为 LibWeb 新增的每个特性或 bug 修复,都应在 Tests/LibWeb 中配套一个测试,并按特性选择 Text、Layout、Ref 或 Screenshot 四种类型之一;而针对内部 C++ 代码的测试则以独立的 TestFoo.cpp 文件形式放在 Tests/LibWeb 下。这一点可以从源码结构直接得到印证——Tests/LibWeb 目录中既有 Text/Layout/Ref/Screenshot/ 四个按类型划分的测试根目录,也有一批 C++ 单元测试文件,如 TestHTMLTokenizer.cppTestCSSPixels.cppTestContentBlocker.cppTestStructuredSerializeCorpus.cpp 等。

运行测试的三种方式

提示:若要复现 CI 上的失败,应参考后文的 Sanitizer 构建运行 一节。

文档给出的第一种(也是最简单的方式)是使用仓库自带的 Meta/ladybird.py 脚本。它的 test 子命令先执行 configure 与 build,再运行测试。从源码看(Meta/ladybird.pytest_main 函数),该脚本实际做的事情就是调用 ctest:

test_args = [
    "ctest",
    "--preset",
    preset,
    "--output-on-failure",
    "--test-dir",
    str(build_dir),
]
if pattern:
    test_args.extend(["-R", pattern])

即:Meta/ladybird.py test 运行所有测试;Meta/ladybird.py test LibWeb 通过 ctest 的 -R 正则过滤只跑名称匹配 LibWeb 的测试。preset 默认取环境变量 BUILD_PRESET,否则为 Release,构建目录随之映射到 Build/release(见 Meta/ladybird.pyknown_presets 表:Debug → Build/debug、Sanitizer → Build/sanitizers 等)。

LibWeb 网页测试是如何注册为 CMake 测试的?在 Tests/LibWeb/test-web/CMakeLists.txt 中可以看到,test-web 可执行文件被注册为名为 LibWeb 的 ctest 测试,并固定附带 --python-executable--per-test-timeout 120 -v -v 参数:

if (BUILD_TESTING)
    add_test(
        NAME LibWeb
        COMMAND $<TARGET_FILE:test-web> --python-executable ${Python3_EXECUTABLE} --per-test-timeout 120 -v -v
    )
    ...
    set_tests_properties(LibWeb PROPERTIES
        ENVIRONMENT LADYBIRD_SOURCE_DIR=${LADYBIRD_SOURCE_DIR}
        TIMEOUT_SIGNAL_NAME SIGTERM)

第二方式是直接调用 test-web 测试运行器,绕过 CTest 一层:

Meta/ladybird.py run test-web

Tests/LibWeb/test-web/Application.cpp 的参数定义中,可以确认 test-web 支持的完整命令行参数,比文档描述更加丰富:

参数 短选项 作用
--test-path <path> 指定测试根目录(默认由 LADYBIRD_SOURCE_DIR 推导为 Tests/LibWeb
--results-dir <path> -R 测试结果的输出目录
--test-concurrency <jobs> -j 并发测试数,默认取 CPU 硬件并发数
--filter <glob> -f 只跑匹配 glob 的测试,如 -f Text/input/your-test.html
--rebaseline 重新生成被执行的 Layout/Text 测试的期望文件
--per-test-timeout <sec> -t 单测试超时秒数,默认 30
--fail-fast 首个失败/超时/崩溃即中止,超时时可挂接调试器
--dry-run 仅列出将要运行的测试
--repeat <n> 将匹配的测试重复运行 N 次
--shuffle -s 打乱测试执行顺序
--verbose -v 可叠加使用,提升日志详细度
--dump-gc-graph -G 输出 GC 图(会自动强制串行执行)

第三方式是直接调用 ctest,最简单的是复用 CMakePresets.json 中的 Release 预设:

cmake --preset Release
cmake --build --preset Release
ctest --preset Release

LADYBIRD_SOURCE_DIR 环境变量

部分测试要求 LADYBIRD_SOURCE_DIR 指向 Ladybird 源码树根目录。手动构建时需要自行设置:

# /path/to/ladybird repository
export LADYBIRD_SOURCE_DIR=${PWD}

这里有两处源码佐证可以说明该变量的重要性:

  • Meta/ladybird.pyensure_ladybird_source_dir 会在未设置时通过 git rev-parse --show-toplevel 自动推导并写入该环境变量;
  • Tests/LibWeb/test-web/Application.cpp 中,test-web 启动时若检测到 LADYBIRD_SOURCE_DIR,会将测试根路径设为 <该变量>/Tests/LibWeb
  • CMakePresets.jsonroot_base 测试预设也通过 "LADYBIRD_SOURCE_DIR": "${fileDir}" 自动注入该变量,因此使用 ctest --preset 时无需手动 export。

使用 ninja 与失败输出

构建完成后也可以直接用 ninja 驱动测试:

cd Build/release
ninja
ninja test

查看失败测试的 stdout/stderr 时,推荐设置 CTEST_OUTPUT_ON_FAILURE 环境变量为 1:

CTEST_OUTPUT_ON_FAILURE=1 ninja test

# 或者直接使用 ctest...
ctest --output-on-failure

结果产物:如何读懂一次 test-web 运行

Tests/LibWeb/test-web/main.cpp 的实现看,一次运行结束后会在结果目录(--results-dir 指定)中生成一份可浏览的 HTML 报告:results.js + 由源码树 Tests/LibWeb/test-web/results-index.html 模板复制出的 index.html,其中记录了 total/fail/timeout/crashed/skipped 汇总与每个未通过测试的模式(Text/Layout/Ref/Screenshot/Crash)、是否存在日志、Ref 与 Screenshot 测试的 pixelErrorsmaxChannelDiff 像素差异统计。每个失败测试还会落盘 .expected.txt/.actual.txt/.diff.txt/.diff.html(文本类差异)或 .actual.png/.expected.png/.diff.png(像素类差异,差异像素标红)。运行期间则持续写出 harness-status.txt 供排查挂起的 harness。这些细节对定位"为什么挂"非常有用。

另一个值得一提的配置是 Tests/LibWeb/TestConfig.initest-web 会解析其中的两个分组(见 main.cpp 中 load_test_config):

  • [LoadFromHttpServer]:列出必须经由内置 HTTP echo server 加载的测试,原因包括 cookie 行为、跨源 Worker fetch、pushState 需要 HTTP(s) scheme、Service Worker Cache API 仅对 HTTP(S) URL 生效等;
  • [Skipped]:当前被禁用的测试清单,每条通常附带原因注释(flaky、CI 超时、尚未实现的功能等),是理解当前已知问题的"活文档"。

使用 Sanitizer 运行测试,复现 CI 失败

CI 以 Address Sanitizer(ASan)与 Undefined Behavior Sanitizer(UBSan)插桩运行 host 测试。这两类工具能够捕获大量常见 C++ 错误,包括内存泄漏、堆栈越界访问、有符号整数溢出等。Sanitizer 构建会显著变慢,并且会使 ccache 之类的缓存失效。

最简单的启用方式是使用 Sanitizer 预设:

cmake --preset Sanitizer
cmake --build --preset Sanitizer
ctest --preset Sanitizer

若不想走预设而手动开启,则使用 -DENABLE_FOO_SANITIZER 系列开关。为保证行为与 CI 一致,需要按文档设置 ASAN_OPTIONSUBSAN_OPTIONSSanitizer 测试预设已在 CMakePresets.json 中内置了同样的值):

export ASAN_OPTIONS='strict_string_checks=1:check_initialization_order=1:strict_init_order=1:detect_stack_use_after_return=1:allocator_may_return_null=1'
export UBSAN_OPTIONS='print_stacktrace=1:print_summary=1:halt_on_error=1'
cmake -GNinja -B Build/lagom -DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON
cd Build/lagom
ninja
CTEST_OUTPUT_ON_FAILURE=1 LADYBIRD_SOURCE_DIR=${PWD}/../.. ninja test

这些选项的含义值得留意:ASan 的 strict_string_checks 开启字符串函数边界检查,check_initialization_order/strict_init_order 用于揪出静态初始化顺序问题,detect_stack_use_after_return 检测栈上 use-after-return;UBSan 的 print_stacktrace=1 在报错时打印调用栈、halt_on_error=1 让首个未定义行为即终止进程,避免错误级联掩盖根因。另外从 Meta/ladybird.py 可以看到,ladybird.py run--preset Sanitizer 时也会自动注入同一组 ASAN_OPTIONS/UBSAN_OPTIONS 默认值——这保证了日常本地运行与 CI 的插桩口径一致。

运行与导入 Web Platform Tests

Web Platform Tests(WPT)是衡量浏览器规范符合度的行业标尺,Ladybird 通过 Meta/WPT.sh 驱动 wpt 工具链运行,该脚本还支持对比两次运行的结果。

基本用法:run 与 compare

# 先跑一遍 WPT 并落日志,随后切到你的 CSS 改动分支,再跑一次并与基线对比
./Meta/WPT.sh run --log expectations.log css
git checkout my-css-change
./Meta/WPT.sh compare --log results.log expectations.log css
# 从上游 WPT 仓库拉取最新测试
./Meta/WPT.sh update
# 运行全部 WPT 测试,结果写入 results.log
./Meta/WPT.sh run --log results.log

从脚本源码(Meta/WPT.sh)可以看到它支持的完整子命令:update(更新 WPT 仓库)、run(运行)、compare(与 LOG_FILE 中的期望对比)、import(把指定 wpt.live 路径的测试抓下来生成 in-tree 测试与期望文件)、list-testscleanbisect BAD_COMMIT GOOD_COMMIT(二分定位首次产生意外结果的提交)。脚本默认构建 test-web 二进制(Meta/WPT.sh 中调用 ./Meta/ladybird.py build test-web),通过 WebDriver 驱动浏览器,覆盖 testharnessreftestwdspeccrashtesttest262 五类测试类型。

导入 WPT 测试到本地仓库

当你改动的代码让 Ladybird 新通过了某个尚未被导入的 WPT 测试时,应把它导入仓库,把"通过状态"固化下来。文档给出的方式:

./Meta/WPT.sh import html/dom/aria-attribute-reflection.html

即把 http://wpt.live/ URL 的路径部分交给 import 子命令。脚本会同时下载该测试及其引用的 JavaScript 脚本,拷贝到 Tests/LibWeb/<test-type>/input/wpt-import 目录,运行该测试,然后在 Tests/LibWeb/<test-type>/expected/wpt-import 目录中生成期望结果文件。这一点在 Tests/LibWeb/TestConfig.ini 中随处可见 wpt-import/ 前缀的条目,正是导入机制的产物。

编写新测试

用脚本生成测试骨架

仓库提供了 Python 脚手架 Tests/LibWeb/add_libweb_test.py

./Tests/LibWeb/add_libweb_test.py your-new-test-name test_type

文档说明接受的 test_type 取值为 "Text"、"Layout"、"Ref" 与 "Screenshot";从脚本源码看,choices 实际还支持 "Crash" 一类(用于验证特定输入不会导致崩溃),且提供 --async 开关为 Text 测试生成异步骨架。脚本会:

  • Tests/LibWeb/<test_type>/input 下创建 your-new-test-name.html 输入文件;
  • Tests/LibWeb/<test_type>/expected 下创建对应的期望文件——Text/Layout 生成 .txt,Screenshot 生成 .png(留空待生成),Ref 生成 <test_name>-ref.html 参考页;
  • Text 测试骨架引用 include.js 并预填 test(() => { println("Expected println() output"); })--async 时预填 asyncTest(async (done) => { ... done(); }));Ref 骨架则自动写入 <link rel="match" href="../expected/<name>-ref.html" /> 标签。

生成后,把模板替换为你的真实测试内容,并重新生成期望文件:

# Text / Layout:rebaseline 重新落盘期望文件
./Meta/ladybird.py run test-web --rebaseline -f Text/input/your-new-test-name.html

Ref 与 Screenshot 测试需要手工提供等效渲染的参考内容;不过 Screenshot 测试的参考图可以用无头模式浏览器直接生成:

./Meta/ladybird.py run ladybird --headless --test-mode Tests/LibWeb/Screenshot/input/your-new-test-name.html

# 日志会输出类似:Saved screenshot to: ~/Downloads/screenshot-2025-06-07-08-37-45.png

mv ~/Downloads/screenshot-2025-06-07-08-37-45.png Tests/LibWeb/Screenshot/images/your-new-test-name.png

--rebaseline 的实际行为可在源码中印证:main.cpp 的 handle_completed_testrebaseline 模式下会把实际输出直接写回期望文件并返回 Pass;Screenshot 的 rebaseline 分支 则会把实际截图写为 PNG,并尝试调用系统的 optipng -strip all 压缩图片(调用失败仅告警,不影响流程)。

四类测试逐一解析

Text 测试:验证无视觉表现的 Web API

Text 测试面向没有视觉表现的 Web API,用 JavaScript 编写并在无头浏览器中运行。每个测试在 script 标签中有一个测试函数来驱动 API,并用 println 输出期望结果;所有 println 调用被累积成输出文本,再由测试运行器与期望输出文件逐字节(忽略尾部换行)比较。

Text 测试可以是同步或异步的。异步测试应使用 done 回调信号完成——"异步"并不意味着一定运行在 async 上下文中,只是要求测试函数在结束时主动汇报;若测试 API 本身需要异步上下文,传给 test 的 lambda 可以直接写成 async。

从实现层面(main.cpp 中 Text 模式的处理)可以补充两条细节:test-web 通过 view.request_internal_page_info(WebView::PageInfoType::Text) 收集页面内累积的输出文本,测试完成由页面端调用 internals.signalTestIsDone("PASS") 上报(该注入脚本位于 main.cpp),页面加载完成与测试完成两个信号都到位后测试才算结束;单测试超时会由 --per-test-timeout 驱动的定时器触发 Timeout 结果。

Layout 测试:比对布局树

Layout 测试将页面的布局树与期望布局树做文本比对,最适合测试布局代码,也常用于验证其他对布局有可观测影响的功能。它不需要任何 JavaScript——页面加载完成后运行器会自动 dump 布局树。

源码中还有一个容易忽略的巧妙设计(run_dump_test 中 Layout 分支):dump 布局树前会先对页面截屏,注释说明这是为了强制触发"SVG as image"文档的惰性布局,同时让更多代码路径跑起来以暴露 bug。布局树的期望文件由 --rebaseline 生成(脚手架生成的 Layout 期望文件内容本身就是一句"run ./Meta/ladybird.py run test-web --rebaseline ..."的提示)。

Ref 测试:与参考页截图像素级对比

参考(ref)测试把测试页的截图与参考页的截图对比,两者完全一致才算通过。它们适合测试背景图、阴影这类视觉效果;如果难以构造等效参考页(例如 SVG 或 canvas 场景),文档建议改用 Screenshot 测试。

每个 Ref 测试都包含一个特殊的标签来指定参考页:

<link rel="match" href="../expected/my-test-ref.html" />

测试运行器据此定位参考页,从而允许多个测试共享同一参考页。实现上(run_ref_test),页面加载后 test-web 注入一段等待脚本(监听 reftest-wait class 移除,并等待 document.fonts.ready 与两帧 requestAnimationFrame 完成,参照 WPT reftest 的等待规范),确认渲染稳定后先截测试页、再通过 internals.loadReferenceTestMetadata() 读取 match/mismatch 引用与 fuzzy 配置,逐条加载参考页截图比对;失败时输出 actual/expected/diff 三张 PNG。

Screenshot 测试:与参考 PNG 对比

Screenshot 测试可视为 Ref 测试的子类型,其"参考页"是一个指向期望输出截图的 <img> 标签。文档建议:能用普通 Ref 测试就避免使用 Screenshot 测试,因为它们对细微的渲染差异敏感,且无法在所有平台上工作。与 Ref 一样,它需要 <link rel="match" href="..."> 标签(在 Screenshot 模式下该标签指向承载 <img> 的参考 HTML)。

结合 main.cpp 中 Screenshot 分支:期望 PNG 以 Gfx::ImageDecoder 解码后与实际截图做 fuzzy 匹配(支持页面内声明的 fuzzy 配置,允许局部区域的像素容差);失败时同样落盘 diff 图并统计 pixel_error_count 与最大通道差,这些数据会汇总进 HTML 报告供人工判断差异性质。

相关文档与延伸阅读

小结

Ladybird 的测试体系可以归纳为三层:Tests/<库>/ 下按库组织的单元测试(CTest 直接驱动)、test-web 驱动的四类网页级测试(Text/Layout/Ref/Screenshot + Crash),以及基于 wpt 工具链的 WPT 规范符合度测试。三条运行入口——Meta/ladybird.py test [pattern]ctest --preset <Release|Debug|Sanitizer>ninja test——殊途同归到 ctest 注册用例;Sanitizer 预设通过内置的 ASAN_OPTIONS/UBSAN_OPTIONS 保证本地与 CI 口径一致;新测试则用 add_libweb_test.py 生成骨架、--rebaseline 固化期望、WPT.sh import 把新通过的 WPT 测试收编进仓库。掌握这条链路后,无论是修 bug 还是加特性,都能把"改动 → 测试 → 期望固化"变成确定性的闭环。

登录后查看全文
热门项目推荐
相关项目推荐