Flutter android_hardware_smoke_test:参考黄金图目录与本地基线、Skia Gold 双轨管理模型
本文以 android_hardware_smoke_test 项目中的 test_driver/goldens/ 参考黄金图目录为讲解主体,说明这套 Android 硬件冒烟测试套件中 golden(黄金图)基线文件的设计与运维规则:为什么该目录在仓库里只有 README.md 一个文件、开发者如何用 UPDATE_GOLDENS=1 flutter drive 在本地捕获并更新 PNG 基线、以及 CI 流水线为何改由中央 Skia Gold 后端承接全部像素比对。读完后你可以独立完成本地基线更新,理解 instrumented APK 如何把基线打包成只读资源并在设备上加载,并清楚本地与 CI 两套 golden 管理方式的边界。
goldens 目录是什么:一个“空目录”的刻意设计
dev/integration_tests/android_hardware_smoke_test/test_driver/goldens/README.md 的第一句话就点明了核心定位:
This directory serves as the storage location for reference golden images used by this integration test suite.
也就是说,这个目录是整个 android_hardware_smoke_test 集成测试套件参考黄金图(reference golden images)的存放位置。但在当前仓库中,该目录下只有这一份 README.md,没有任何 PNG 文件。这不是疏忽,而是有意为之。README 的末尾也解释了这份文件自身的存在目的:
This
README.mdfile exists to ensure Git preserves this directory, preventing Flutter asset packaging warnings during CI presubmit checks.
从源码看,这个“占位”角色有两条硬性技术依据:
-
Git 不跟踪空目录。Git 只跟踪文件,若目录真的为空,
test_driver/goldens/路径就会从仓库中消失,README.md 是保住目录存在的最小手段。 -
这个目录同时是 Flutter 的 asset 目录。在 pubspec.yaml 中,
flutter.assets声明了test_driver/goldens/作为项目的资源目录:flutter: uses-material-design: true assets: - test_driver/goldens/一旦该目录在构建或 CI presubmit 检查时不存在,Flutter 的 asset 打包逻辑就会告警。README.md 既保住了目录本身,也充当了该目录使用规范的说明文档。
另一个关键事实:实际的基线 PNG 从不出现在仓库里——无论本地运行还是 CI 运行,“把生成的参考 PNG 提交进仓库”都是被明确禁止的做法。这就引出了文档的主体:golden 的“本地 vs CI”双轨管理模型。
双轨 Golden 管理模型:本地运行 vs CI 运行
文档以 “Golden Management Model: Local vs. CI” 为标题,分两条轨道给出规则。
轨道一:本地运行与 Standalone OEM 模式——基线存放在本地目录
针对本地运行(开发者 PC)与 Standalone OEM 模式(Android 硬件厂商在自有设备上跑预编译 APK),文档给出三条规则:
-
基线参考图存放在本地的本目录(
test_driver/goldens/)中; -
在主机 PC 上运行 Host-Driven Driver Mode 并激活
UPDATE_GOLDENS=1环境变量时,可以捕获或更新这些本地基线:UPDATE_GOLDENS=1 flutter drive -v \ --driver=test_driver/driver_test.dart \ --target=integration_test/integration_test_wrapper.dart -
这些参考 PNG 会在 standalone 真机执行期间,被编译为 instrumented 测试 APK 内的只读资源(read-only assets)。
这三条规则在源码中都能找到对应实现。
UPDATE_GOLDENS 如何改变比对行为。 Host-Driven 模式下,主机侧驱动脚本 test_driver/driver_test.dart 让设备上应用渲染每个测试场景,取回图像字节(或裁剪坐标)后,用 matchesGoldenFile 断言:
await expectLater(
imageBytes,
matchesGoldenFile('goldens/$testName$activeGoldenVariant.png'),
);
matchesGoldenFile 这一 matcher 在 UPDATE_GOLDENS 环境变量激活时(取值 =1 或 =true 均可,仓库文档中两种写法都出现过:本文档用 UPDATE_GOLDENS=1,项目 README 用 UPDATE_GOLDENS=true),会把断言语义从“比对”切换为“写入”:不再逐像素断言相等,而是把当前实际渲染结果写回 goldens/ 对应路径,即完成基线捕获/更新。这正是文档所说 “capture or update these local baseline images on your host PC” 的机制来源。
基线文件的命名规则。 从断言路径可见,基线文件由三部分拼成:
- 测试场景名:
blueRectangleTest、trianglePathTest、textTest、imageTest、advancedBlendTest、backdropFilterBlurTest,以及三个platformView前缀的平台视图场景; - 图形变体后缀
.$activeGoldenVariant。variant 值由驱动脚本在套件启动时通过get_golden_variant命令(commandGetGoldenVariant)向应用查询而来,见 lib/constants.dart 与 integration_test/integration_test_wrapper.dart 中的对应分支。也就是说,变体后缀不是主机侧硬编码,而是由应用按其实际编译的图形后端自报。
因此,当活动图形后端为 Vulkan 时,文件是 goldens/blueRectangleTest.vulkan.png;OpenGLES 下则是 goldens/blueRectangleTest.opengles.png。
基线如何进入 instrumented APK 成为只读资源。 pubspec 中的 assets: [test_driver/goldens/] 声明是关键:构建 instrumented 测试 APK 时,test_driver/goldens/ 下的 PNG 会被打包成 APK 内的只读资源。设备侧比对逻辑 lib/goldens.dart 的 compareGoldenOnDevice 正是利用这一点:
goldenFileComparator = const PixelExactLocalFileComparator();
...
final goldenAssetPath = 'test_driver/goldens/$fileName';
...
final dynamic comparisonResult = await matchesGoldenFile(
Uri(scheme: 'asset', path: goldenAssetPath),
).matchAsync(resultImageBytes);
即 Standalone OEM 模式下,设备既不依赖网络也不依赖主机文件系统,而是直接通过 asset:// scheme 从 APK 内加载基线 PNG。PixelExactLocalFileComparator 专门实现了 asset scheme 的加载分支——用 rootBundle.load() 从包资源取出基线字节,完全省去了把文件拷贝到设备临时目录的步骤。它的 compare 实现还把两张图解码后做原始像素级逐字节比较,而不是直接比较 PNG 压缩字节:因为 Android 原生 screencap 编码器与 Dart 编码器对同一像素网格会产出不同的 PNG 元数据、chunk 顺序与 zlib 压缩级别,字节级比较是脆弱的。这是支撑“设备内只读资源”方案的关键实现细节。
轨道二:CI 运行——走 Skia Gold,不提交参考图
文档第二轨对 CI 流水线是一条负面约束,措辞非常直接:
Do not commit your generated reference PNG images to the repository.
并且,CI 中所有主机侧视觉比对会自动路由到中央 Skia Gold 后端,由其负责像素比对、triage 处理与审批工作流。
源码里,这个路由由 driver_test.dart setUpAll 中几行代码完成:
if (isLuci) {
await enableSkiaGoldComparator(
namePrefix: 'android_hardware_smoke_test$activeGoldenVariant',
localOutputDir: 'goldens',
);
}
判定条件是 LUCI_CI == 'True' 环境变量。在 LUCI CI 上运行时,驱动脚本把本地文件比较器替换为 Skia Gold 比较器:实际渲染字节被送往 Skia Gold 后端比对与 triage,namePrefix 携带图形变体(如 android_hardware_smoke_test.vulkan),保证不同后端的基线在后端独立管理。这意味着 CI 场景下基线的“事实来源”在 Skia Gold 服务端,主机的 goldens/ 目录只是临时工作区。
“不提交”这条约束还不只是文档规范,它被 CI runner 强制执行。在 dev/bots/suite_runners/run_android_hardware_smoke_tests.dart 中,runner 在每次 shard 运行结束后于 finally 块统一清空 goldens 目录:
void _cleanGoldensDirectory(Directory directory) {
if (!directory.existsSync()) {
return;
}
for (final FileSystemEntity entity in directory.listSync()) {
if (path.basename(entity.path) != 'README.md') {
entity.deleteSync(recursive: true);
}
}
}
调用处的注释点明意图:“Clean up copied goldens to keep Git worktree completely clean”。逻辑是只保留 README.md,其余文件全部删除——即使 flutter drive 在 CI 运行中于主机落下了基线图,也不会污染 git worktree。这与 README.md “保住目录”的职责形成呼应:它是目录中唯一允许长期驻留的文件。
实操指南:本地捕获与更新基线
结合文档给出的命令与项目 README(dev/integration_tests/android_hardware_smoke_test/README.md)的补充说明,本地维护基线的完整流程如下。
前置条件
-
解锁并连接一台 Android 真机或模拟器,确保其处于活动状态;
-
本项目遵循 minimal-boilerplate 模式,未提交标准 Gradle wrapper。首次本地运行前,需在该目录执行标准重新生成命令以恢复 wrapper(不影响任何定制构建定义与源码):
flutter create --platform=android --no-overwrite .
切换图形变体(决定基线后缀)
编译进 AndroidManifest.xml 的 <meta-data> 标签是图形后端的唯一事实来源(对 OEM 模式与 Host-Driven 模式同时生效)。要捕获某个特定图形变体的基线,先修改 android/app/src/main/AndroidManifest.xml,设置期望的 ImpellerBackend 值:
<!-- Enable Vulkan: -->
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="vulkan" />
<!-- Enable OpenGLES: -->
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="opengles" />
应用运行时会通过自定义 MethodChannel 向原生 Android embedder 查询并自报该值(见 wrapper 中 commandGetGoldenVariant 分支),主机 PC 完全不需要传环境变量来指定变体。
捕获/更新基线
随后执行本文档给出的命令:
UPDATE_GOLDENS=1 flutter drive -v \
--driver=test_driver/driver_test.dart \
--target=integration_test/integration_test_wrapper.dart
运行结束后,主机文件系统上 test_driver/goldens/ 中的 PNG 被当前渲染结果覆盖,文件名为 $testName.$variant.png 模式。
校验基线
去掉 UPDATE_GOLDENS 直接执行同一命令:
flutter drive -v \
--driver=test_driver/driver_test.dart \
--target=integration_test/integration_test_wrapper.dart
此时 matchesGoldenFile 会对本地基线做真实像素比对,失败即测试失败。
对 Standalone OEM 模式还有一条资源打包前置条件(项目 README 中以 IMPORTANT 强调):instrumented 测试在设备上与打包在 APK 内的只读基线资源做像素比对,因此必须先用带 UPDATE_GOLDENS 的 Host-Driven Driver Mode 在 test_driver/goldens/ 下生成完整本地基线,再编译构建 instrumented APK;否则打进 APK 的 asset 中不会有完整的参考图,设备端比对必然失败。
与 CI shard 的衔接
CI shard 注册在中央测试编排器 dev/bots/test.dart,可本地以 dev-bot 方式执行,例如:
# Run the Vulkan graphics backend shard locally
SHARD=android_hardware_smoke_vulkan_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart
shard 级 runner 的执行链路可参见 run_android_hardware_smoke_tests.dart:按 shard 后端改写 Manifest 的 ImpellerBackend 值,先跑带重试的 flutter drive 回路(失败时检查 logcat 判断是否为瞬时 EGL 错误),可选再跑 instrumented Gradle 测试;finally 中恢复 Manifest 并清空 goldens 目录(仅保留 README.md)。
小结
围绕这份 goldens 目录 README,核心信息可归纳为三点:
- 目录职责:
test_driver/goldens/是测试套件参考黄金图的存放位置;README.md 的存在让 Git 保留该目录,同时防止 CI presubmit 检查中 Flutter asset 打包告警。 - 双轨模型:本地运行与 Standalone OEM 模式下,基线存放在本地目录,用
UPDATE_GOLDENS=1 flutter drive ...在主机 PC 捕获/更新,并被编译为 instrumented 测试 APK 的只读资源、经asset://scheme 在设备上加载比对;CI 运行时,主机侧比对一律路由至 Skia Gold 后端,禁止把生成的参考 PNG 提交进仓库,runner 每次运行后清空目录中除 README.md 外的全部内容。 - 文件命名约定:基线文件名为
$testName.$variant.png,variant 后缀由应用根据 Manifest 编译进来的ImpellerBackend元数据自报(如.vulkan、.opengles),捕获基线前先切换 Manifest 中的后端值。
遵循这一模型,本地基线维护可复现,CI 流水线不会被本地生成图污染,而真机上的 standalone instrumented 测试始终拥有一套完整的只读基线资源作为比对依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00