首页
/ Flutter android_hardware_smoke_test:参考黄金图目录与本地基线、Skia Gold 双轨管理模型

Flutter android_hardware_smoke_test:参考黄金图目录与本地基线、Skia Gold 双轨管理模型

2026-09-04 20:57:48作者:尤峻淳Whitney

本文以 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.md file exists to ensure Git preserves this directory, preventing Flutter asset packaging warnings during CI presubmit checks.

从源码看,这个“占位”角色有两条硬性技术依据:

  1. Git 不跟踪空目录。Git 只跟踪文件,若目录真的为空,test_driver/goldens/ 路径就会从仓库中消失,README.md 是保住目录存在的最小手段。

  2. 这个目录同时是 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” 的机制来源。

基线文件的命名规则。 从断言路径可见,基线文件由三部分拼成:

  • 测试场景名:blueRectangleTesttrianglePathTesttextTestimageTestadvancedBlendTestbackdropFilterBlurTest,以及三个 platformView 前缀的平台视图场景;
  • 图形变体后缀 .$activeGoldenVariant。variant 值由驱动脚本在套件启动时通过 get_golden_variant 命令(commandGetGoldenVariant)向应用查询而来,见 lib/constants.dartintegration_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.dartcompareGoldenOnDevice 正是利用这一点:

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,核心信息可归纳为三点:

  1. 目录职责:test_driver/goldens/ 是测试套件参考黄金图的存放位置;README.md 的存在让 Git 保留该目录,同时防止 CI presubmit 检查中 Flutter asset 打包告警。
  2. 双轨模型:本地运行与 Standalone OEM 模式下,基线存放在本地目录,用 UPDATE_GOLDENS=1 flutter drive ... 在主机 PC 捕获/更新,并被编译为 instrumented 测试 APK 的只读资源、经 asset:// scheme 在设备上加载比对;CI 运行时,主机侧比对一律路由至 Skia Gold 后端,禁止把生成的参考 PNG 提交进仓库,runner 每次运行后清空目录中除 README.md 外的全部内容。
  3. 文件命名约定:基线文件名为 $testName.$variant.png,variant 后缀由应用根据 Manifest 编译进来的 ImpellerBackend 元数据自报(如 .vulkan.opengles),捕获基线前先切换 Manifest 中的后端值。

遵循这一模型,本地基线维护可复现,CI 流水线不会被本地生成图污染,而真机上的 standalone instrumented 测试始终拥有一套完整的只读基线资源作为比对依据。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341