news 2026/9/24 2:41:40

Skia C++ 单元测试实战指南:从 DEF_TEST 编写、GN 构建集成到 dm 运行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Skia C++ 单元测试实战指南:从 DEF_TEST 编写、GN 构建集成到 dm 运行
  • 图形学

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.

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

本文是一份基于 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*),通过TestRegistryTest::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 测试。若测试文件里用到SkCanvasSkBitmap等绘制 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),其中optionsconst 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 声明的IsGLContextTypeIsVulkanContextType等过滤器,把测试分发给匹配的后端执行。

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_TESTDEF_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还支持gmskpimage等其他源类型);
  • --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>:指定要运行的配置(如8888glvk),GPU 测试可用它限定后端。

例如,运行所有 Ganesh GPU 测试可以用:

out/Debug/dm --src tests --match Ganesh --config gl vk

dm对不支持的 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

运行后你会看到MyNewTestMyNewGaneshTestMyNewGraphiteTest三个测试的结果;如果当前构建没有编译 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.

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 2:14:43

【C++三方组件】Google Test:C++单元测试的事实标准

【C三方组件】Google Test&#xff1a;C单元测试的事实标准 【摘要】&#xff1a;main 里手写 if 断言再肉眼比对输出的年代&#xff0c;被 TEST() 宏终结——Google Test 用「宏注册 自动发现 独立运行」把测试变成一等代码。How 实测 TEST/EXPECT/ASSERT 断言语义、TEST_F …

作者头像 李华
网站建设 2026/9/24 2:13:10

示波器探头选型与接地实战:从衰减比到补偿校准的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 2:10:06

DDR内存时序调优:CL、tRCD、tRP、tRAS四大参数详解与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华