- 图形学
【免费下载链接】skia
Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.
本文是一份基于 Skia 仓库 agents/skills/skia-gn-workflow/references/testing.md 的单元测试实战指南,面向需要在 Skia 上新增 C++ 单元测试的开发者。文章覆盖测试宏体系(DEF_TEST/DEF_GANESH_TEST/DEF_GRAPHITE_TEST)、断言宏、gn/tests.gni构建注册,以及使用dm工具过滤与运行测试的完整流程,读完后你可以独立完成"写一个测试 → 挂进构建 → 跑起来"的全链路工作。
Skia 自定义单元测试框架概览
Skia 没有采用 GTest 等第三方框架,而是自带一套轻量的 C++ 单元测试框架,核心头文件位于 tests/Test.h,框架主体由skiatest命名空间承载。绝大多数单元测试放在仓库根目录的tests/目录下(与 gm/ 中的图形测试 GM、bench/ 中的基准测试区分开),并使用DEF_TEST系列宏声明测试函数。
从 tests/Test.h 可以看到,每个测试最终都被包装为一个skiatest::Test结构体,它携带测试名称、测试类型(kCPU/kCPUSerial/kGanesh/kGraphite)、CTS 强制等级以及对应平台的执行函数指针。测试通过sk_tools::Registry<Test>全局注册表(见 tests/Test.h)完成注册,因此测试文件只需要被编译进目标,测试即可被发现,无需额外手工登记名称。
Test结构体按测试类型区分了三种执行入口,实现在 tests/Test.cpp:
cpu():执行普通 CPU 测试函数,签名为void (*)(Reporter*);ganesh():执行需要 Ganesh GPU 上下文的测试,签名带const GrContextOptions&;graphite():执行需要 Graphite 上下文的测试,签名带const skiatest::graphite::TestOptions&。
也就是说,"同一个测试要跑在哪种后端"在宏展开阶段就已经决定了,运行时由测试运行器(如dm)负责分发。
编写第一个测试
在tests/目录新建一个.cpp文件,包含tests/Test.h,然后用DEF_TEST宏声明测试函数。下面是最小示例(与原文档一致,可直接复制编译):
#include "tests/Test.h" DEF_TEST(MyNewTest, reporter) { REPORTER_ASSERT(reporter, 1 + 1 == 2); if (false) { ERRORF(reporter, "Something went wrong!"); } }各要素说明:
DEF_TEST(name, reporter):定义并注册一个 CPU 测试。从 tests/Test.h 的宏定义可以看出,它实际展开为:声明static void test_MyNewTest(skiatest::Reporter*),通过TestRegistry将Test::MakeCPU(#name, test_MyNewTest)注册进全局注册表,再给出函数体。#name会把宏参数字符串化,因此测试名在运行时就是一个字符串。reporter:指向skiatest::Reporter对象的指针,用于上报成功、失败与附加消息。Reporter的接口定义在 tests/Test.h,核心虚函数是reportFailed(const skiatest::Failure&);它还维护了一个上下文栈,配合push/pop可以把失败信息加上子测试等上下文。
skiatest::Failure结构(tests/Test.h)记录了出错的文件名、行号、条件表达式字符串与格式化消息,toString()在 tests/Test.cpp 中把三者拼成文件:行号 消息: 条件的格式,这就是你在测试输出里看到的失败信息格式。
编译前提
DEF_TEST只声明为 CPU 测试。若测试文件里用到SkCanvas、SkBitmap等绘制 API,还需要包含对应的 Skia 头文件;若只是纯逻辑断言,上面的最小示例已足够。
断言宏:REPORTER_ASSERT 与 ERRORF
框架提供以下核心断言宏(定义见 tests/Test.h):
| 宏 | 说明 |
|---|---|
REPORTER_ASSERT(reporter, cond) | 基础断言,条件不成立时上报失败,失败信息中自动带上条件的源码字符串 |
REPORTER_ASSERT(reporter, cond, "fmt", ...) | 带 printf 风格格式化消息的断言,失败时附加自定义说明 |
ERRORF(reporter, "msg", ...) | 无条件上报一条 printf 风格错误(例如你检测到某个异常分支时主动调用) |
INFOF(reporter, ...) | 仅在reporter->verbose()为真时打印调试信息(通过SkDebugf输出),适合做测试内日志 |
注意REPORTER_ASSERT失败后不会终止测试,而是记录失败并继续执行,这便于在一次运行中收集尽可能多的失败点。若某步失败后继续执行没有意义,请用ERRORF(...)后紧跟return;提前退出(这也是 tests/Test.h 官方注释示例的推荐写法)。
从宏实现看,REPORTER_ASSERT最终走REPORT_FAILURE,它会调用reporter->reportFailedWithContext(...)(见 tests/Test.cpp),把Reporter上下文栈中的消息以[subtest, ...]后缀附加到失败信息后输出。
控制台输出与失败格式
失败信息最终经由skiatest::Failure::toString()格式化,形如:
tests/MyNewTest.cpp:12 Something went wrong: x should be 15其中12是调用断言的文件行号,x should be 15是格式化消息,Something went wrong前面的条件字符串可能为空。
从 CPU 到 GPU:Ganesh 与 Graphite 测试宏
除了DEF_TEST,框架为 GPU 相关测试提供了两组宏,在 tests/Test.h 中成体系地定义:
DEF_GANESH_TEST
用于需要 Ganesh GPU 上下文(OpenGL / Vulkan / Metal / D3D / Dawn 等)的测试:
DEF_GANESH_TEST(MyGaneshTest, reporter, options, CtsEnforcement::kNever) { // options 是 const GrContextOptions&,可读取 GPU 配置 REPORTER_ASSERT(reporter, options.fUseShaderSwizzler == false); }- 签名:
DEF_GANESH_TEST(name, reporter, options, ctsEnforcement),其中options是const GrContextOptions&,ctsEnforcement是 CTS 强制等级(见下文)。 - 变体宏:
DEF_GANESH_TEST_FOR_ALL_CONTEXTS(跑遍所有编译进的后端)、DEF_GANESH_TEST_FOR_GL_CONTEXT/DEF_GANESH_TEST_FOR_VULKAN_CONTEXT/DEF_GANESH_TEST_FOR_METAL_CONTEXT/DEF_GANESH_TEST_FOR_D3D_CONTEXT/DEF_GANESH_TEST_FOR_DAWN_CONTEXT(限定特定后端)、DEF_GANESH_TEST_FOR_MOCK_CONTEXT(mock 上下文)、DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(仅渲染型上下文)。这些变体通过 tests/Test.h 声明的IsGLContextType、IsVulkanContextType等过滤器,把测试分发给匹配的后端执行。
DEF_GRAPHITE_TEST
用于需要 Graphite 渲染器上下文的测试:
DEF_GRAPHITE_TEST(MyGraphiteTest, reporter, CtsEnforcement::kNever) { // 执行普通 CPU 测试逻辑,但被标记为 Graphite 测试 REPORTER_ASSERT(reporter, true); }- 签名:
DEF_GRAPHITE_TEST(name, reporter, ctsEnforcement)。从 tests/Test.h 的实现看,它内部仍然调用一个纯Reporter*签名的函数体,但注册为TestType::kGraphite,运行时只在 Graphite 上下文存在时执行。 - 需要真正访问 Graphite 上下文的测试应使用
DEF_GRAPHITE_TEST_FOR_CONTEXTS/DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS/DEF_GRAPHITE_TEST_FOR_VULKAN_CONTEXT/DEF_GRAPHITE_TEST_FOR_METAL_CONTEXT/DEF_GRAPHITE_TEST_FOR_DAWN_CONTEXT等变体,这些宏会通过skiatest::graphite::RunWithGraphiteTestContexts把测试分发到各后端。
ctsEnforcement 参数是什么
DEF_GANESH_TEST与DEF_GRAPHITE_TEST都要求传入一个CtsEnforcement等级,定义在 tests/CtsEnforcement.h。它决定测试在 Android CTS/SkQP 中的执行策略:
CtsEnforcement::kNever:与 CTS 无关,永远不被强制(普通单元测试通常用它);CtsEnforcement::kNextRelease:新测试如果稳定且准备在下个 Android 版本被强制,应使用该占位值;CtsEnforcement::kApiLevel_*:对应 Android vendor API 级别(如kApiLevel_T= 33);CtsEnforcement::kToBeDetermined:不稳定或进行中的测试使用,表示还未决定是否强制。
eval(apiLevel)会在三种运行模式间切换:跳过、带 workaround 运行、严格运行。对于不关心 CTS 的本地测试,直接传CtsEnforcement::kNever即可。
其他常用变体
DEF_TEST_DISABLED(name, reporter):注册但默认不运行(condition = false),适合临时禁用某个测试(tests/Test.h)。DEF_SERIAL_TEST(name, reporter):注册为kCPUSerial类型,只串行执行,适合对线程安全有特殊要求或共享全局状态的测试(tests/Test.h)。UNIX_ONLY_TEST:在 Unix 构建下等价于DEF_TEST,其他平台等价于DEF_TEST_DISABLED(tests/Test.h)。
把新测试加入构建:编辑 gn/tests.gni
tests/下的测试文件并不会被自动编译,必须把新文件登记进 gn/tests.gni 的tests_sources列表。该列表从 gn/tests.gni 开始,是一长串以$_tests/为前缀的绝对路径,例如:
tests_sources = [ "$_tests/AAClipTest.cpp", "$_tests/AndroidCodecTest.cpp", ... "$_tests/MyNewTest.cpp", # 新增这一行 ... ]其中_tests在文件顶部通过get_path_info("../tests", "abspath")解析为tests/目录的绝对路径(见 gn/tests.gni),因此新增行只需写:
"$_tests/MyNewTest.cpp",同类列表还包含:
graphite_tests_sources(gn/tests.gni):Graphite 相关测试;ganesh_tests_sources(gn/tests.gni):Ganesh GPU 测试;ganesh_gl_tests_sources/ganesh_metal_tests_sources(gn/tests.gni):特定后端的 GPU 测试;pathops_tests_sources(gn/tests.gni):pathops 算法测试。
如果只用到DEF_TEST,加入tests_sources即可;使用了DEF_GANESH_TEST/DEF_GRAPHITE_TEST的测试,视具体宏可能还需确认对应后端列表(GN 构建会自动按SK_GANESH/SK_GRAPHITE宏条件引入)。若项目同时使用 Bazel 构建,还需要把新文件加入 BUILD.bazel 中对应的skia_test目标源列表。
用 dm 运行测试
Skia 的单元测试统一由正确性测试工具dm执行(其入口在 dm/DM.cpp,测试运行逻辑见 dm/DMGpuTestProcs.cpp)。先构建出dm可执行文件(假设构建输出目录为out/Debug),然后运行:
out/Debug/dm --src tests --match MyNewTest参数含义:
--src tests:只运行 C++ 单元测试源(dm还支持gm、skp、image等其他源类型);--match MyNewTest:按名字过滤,只跑匹配的测试。
常用过滤与运行参数
dm的--match支持丰富的匹配语法(详见 agents/skills/skia-gn-workflow/references/dm_usage.md):
| 语法 | 效果 |
|---|---|
substring | 匹配名字中包含该子串的测试 |
~exclude | 排除匹配名字的测试 |
^start | 匹配以该前缀开头的测试 |
$end | 匹配以该后缀结尾的测试 |
^exact$ | 精确匹配 |
其他实用参数:
--src tests --match MyNewTest:单跑某个测试(最常用);--src tests --match ^Text:跑所有名字以Text开头的测试;--src tests --match ~Slow:排除名字含Slow的测试;--threads <n>(或-j):设置工作线程数,默认等于 CPU 核数,单测调试时可设为1;--nogpu:跳过所有 GPU 相关测试(GPU 测试在无图形环境下会失败或跳过);-v/--verbose:输出更详细的日志;-i <dir>/--resourcePath <dir>:测试资源目录,若测试加载资源失败,先检查该路径是否指向正确的resources目录;--config <names>:指定要运行的配置(如8888、gl、vk),GPU 测试可用它限定后端。
例如,运行所有 Ganesh GPU 测试可以用:
out/Debug/dm --src tests --match Ganesh --config gl vkdm对不支持的 GPU 后端会自动跳过,因此跨平台 CI 上常见做法是不指定--config,让 dm 遍历所有编译进的后端。
关于构建输出目录
上面示例假设已经用 GN 生成了out/Debug构建目录并构建出dm。Skia 的 GN 工作流(生成、配置、编译)可参考 agents/skills/skia-gn-workflow/SKILL.md;dm目标名即为dm,构建命令形如:
bin/gn gen out/Debug ninja -C out/Debug dm完整示例:一个带上下文的 GPU 测试
把以上要素串起来,一个同时覆盖 CPU 与 GPU 的完整示例文件tests/MyNewTest.cpp可以这样写:
#include "tests/Test.h" // 纯 CPU 测试:验证一个简单不变量 DEF_TEST(MyNewTest, reporter) { REPORTER_ASSERT(reporter, 1 + 1 == 2); REPORTER_ASSERT(reporter, 2 * 2 == 4, "multiplication works"); } // 需要 Ganesh GPU 上下文的测试,在所有编译进的 GPU 后端起效 DEF_GANESH_TEST_FOR_ALL_CONTEXTS(MyNewGaneshTest, reporter, context_info, CtsEnforcement::kNever) { // context_info 是 const sk_gpu_test::ContextInfo&,可用于获取 GPU 上下文 REPORTER_ASSERT(reporter, context_info.directContext() != nullptr); } // 需要 Graphite 上下文的测试 DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(MyNewGraphiteTest, reporter, graphite_ctx, CtsEnforcement::kNever) { REPORTER_ASSERT(reporter, graphite_ctx != nullptr); }对应地把该文件加入 gn/tests.gni 的tests_sources,重新生成构建文件并编译:
# 编辑 gn/tests.gni,追加 "$_tests/MyNewTest.cpp" bin/gn gen out/Debug ninja -C out/Debug dm out/Debug/dm --src tests --match MyNew运行后你会看到MyNewTest、MyNewGaneshTest、MyNewGraphiteTest三个测试的结果;如果当前构建没有编译 GPU 后端,后两个会被自动跳过。
小结
- 单元测试放在 tests/ 目录,用 tests/Test.h 中的宏声明;CPU 测试用
DEF_TEST,GPU 测试按后端选DEF_GANESH_TEST/DEF_GRAPHITE_TEST及其_FOR_*_CONTEXT变体。 - 断言用
REPORTER_ASSERT/ERRORF,失败不会中断测试,必要时手动return。 - 新测试文件必须登记进 gn/tests.gni 的
tests_sources才会被编译。 - 用
out/Debug/dm --src tests --match <pattern>运行与过滤测试,--match支持子串、排除、前缀、后缀、精确匹配五种模式。 - 关心 CTS 强制的测试需要正确选择
CtsEnforcement等级(参考 tests/CtsEnforcement.h)。
- 图形学
【免费下载链接】skia
Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.
相关推荐
ParlAI 测试实战指南:从 pytest 运行到 TorchGeneratorAgent 单元测试编写
ParlAI 测试实战指南:从 pytest 运行到 TorchGeneratorAgent 单元测试编写 ParlAI 是面向开放对话数据集训练与评估 AI
NLP人工智能深度学习Lynx 单元测试实战指南:从 C++/Android/iOS 测试编写到 RTF 自动化运行
Lynx 单元测试实战指南:从 C++/Android/iOS 测试编写到 RTF 自动化运行 Lynx 是面向跨端场景的高性能渲染引擎,其测试体系贯穿 C++
跨平台移动开发前端桌面应用Skia 测试工具集成 ANGLE:从 GN 构建配置到 `--config` 命令行的完整指南
Skia 测试工具集成 ANGLE:从 GN 构建配置到 config 命令行的完整指南 本文档对应 Skia 仓库中的 site/docs/user/spec
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考