在 Flutter Devicelab 中用 Android 模拟器测试 Android 变更:基于 .ci.yaml 的配置指南
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
Flutter 的 Devicelab 传统上运行在真实物理设备上,但为了降低对真机设备池的依赖,仓库已经通过 LUCI recipes 加入了对「Android 模拟器」的支持,开发者只需在仓库根目录的 .ci.yaml 中声明测试目标,基础设施即可自动完成模拟器的创建与测试驱动。本文以官方文档 Testing-Android-Changes-in-the-Devicelab-on-an-Emulator.md 为主线,结合仓库中真实的.ci.yaml配置与 Devicelab 框架源码,讲解如何从零新增一个跑在模拟器上的测试目标,以及如何把一个既有真机目标迁移到模拟器环境。
为什么需要「模拟器版」Devicelab
Devicelab(代码位于 dev/devicelab)是一套跑在真实设备上的测试体系,其任务(tasks)通常要求实验室中存在对应类型的物理设备(如linux_android表示 Linux 主机连接着 Android 真机)。物理设备池数量有限、维护成本高,且无法覆盖所有开发者本地场景。
因此 Flutter 为 Devicelab 增加了模拟器运行通道:在 LUCI recipe 层先启动一个 Android 模拟器(Android Virtual Device, AVD),再让测试任务在模拟器上执行。开发者只需在 CI 配置文件里新增一个 target,就能在任意 PR 或提交上自动验证 Android 侧的框架 / 引擎改动,无需手动申请真机。
在动手前,可以先了解两条相关联的资料路径:
- Flutter-Self-Service-Index.md 收录了本文档的索引入口;
- How-to-add-a-new-integration-test-to-Framework-CI.md 描述了向框架 CI 添加集成测试目标的通用流程,其中同样提及
devicelab_dronerecipe,可作为旁证。
前置认知:.ci.yaml 与 Devicelab 任务的协作关系
仓库根目录的 .ci.yaml 是 Flutter 持续集成目标(target)的清单文件,其头部注释说明了它的作用与约束:
enabled_branches:该文件只对master及flutter-\d+\.\d+-candidate\.\d+形式的发布候选分支生效;platform_properties:为若干「平台组」(如linux、linux_android_emu、mac、windows)预置公共属性(依赖、机器要求等);- 后续大量以
- name:开头的条目则是具体要执行的 CI 目标。
而每个 Devicelab 测试任务的「本体」是一个 Dart 程序,存放在 dev/devicelab/bin/tasks 下,通过package:flutter_devicelab/framework/framework.dart注册并执行(参考 dev/devicelab/README.md 的 "Writing tests" 一节)。目标名称与task_name共同决定最终执行哪一个任务文件。
此外,任务在 TESTOWNERS 中登记维护者,例如示例任务android_defines_test就登记在第 14 行,归属@jesswrd @flutter/android。
新增一个模拟器测试目标:完整 YAML 与字段逐项解析
原文档给出了一套「从零新增 Devicelab Android 模拟器测试」的完整目标定义。原文中的引号存在中文字符(“1”、"true"混用),在真实 YAML 解析中必须使用 ASCII 双引号,下面给出规范化后的可复制版本:
- name: Linux_android android_defines_test recipe: devicelab/devicelab_drone presubmit: true timeout: 60 dimensions: { kvm: "1", cores: "8", Machine_name: "n1-standard-8" } properties: device_type: "none" task_name: android_defines_test use_emulator: "true" dependencies: >- [ {"dependency": "android_virtual_device", "version": "31"} ] tags: > ["devicelab", "linux"] timeout: 300name:平台前缀与目标命名
name的前缀部分标明平台,可选值为Linux或Linux_Android。原文档建议优先选择Linux_Android,因为它会为任务提供更多 Android 相关的预装依赖(SDK、模拟器等),后续接模拟器所需的依赖更齐全。
需要注意:随着仓库 CI 配置演进,当前 .ci.yaml 中面向模拟器的目标实际以Linux_android_emu为前缀(例如第 2336 行的Linux_android_emu android_defines_test),该前缀与platform_properties中预置好的模拟器平台组一一对应。这一点在下一节详述。
recipe:固定为 devicelab/devicelab_drone
recipe: devicelab/devicelab_dronerecipe永远填写devicelab/devicelab_drone。这是唯一负责「启动模拟器并驱动测试」的 recipe:它会先按依赖声明拉起 Android 模拟器,再通过 Devicelab 的测试 runner 执行task_name指定的任务。这一约定同样体现在 .ci.yaml 的所有模拟器目标中。
presubmit:是否在 PR 中触发
presubmit: true取值为true或false。如果希望在任意打开的 PR 上都运行该测试,就设为true——这是把 Android 改动在镜像到 google3 之前提前暴露问题的最佳手段。若设false,则只在 CI 提交(post-submit)上运行。
顶层 timeout:任务超时整数
timeout: 60目标层级的timeout是整数,表示 LUCI 为该目标分配的整体超时预算(单位通常为分钟),用于兜底避免任务无限期悬挂。当前仓库内模拟器目标的顶层超时多为 60,例如Linux_android_emu android_defines_test(见 .ci.yaml)。
dimensions:要求嵌套虚拟化
dimensions: { kvm: "1", cores: "8", Machine_name: "n1-standard-8" }dimensions的作用是告知 LUCI 框架:该测试必须被调度到支持嵌套虚拟化的机器上,因为模拟器依赖 KVM 加速。因此必须按示例填写:
kvm: "1":机器必须启用 KVM(内核虚拟化);cores: "8":至少 8 核;Machine_name: "n1-standard-8":使用 GCEn1-standard-8型机器,该机型在 Flutter 基建中已被验证支持模拟器运行。
在当前的 .ci.yamlplatform_properties.linux_android_emu平台组中,kvm: "1"与cores: "8"同样作为公共属性预置(见 .ci.yaml),印证了该字段的必要性。
properties:任务级关键开关
properties是驱动 recipe 行为的核心,共包含六项:
a.device_type: "none"
必须设为none,表示使用一台未外接 Android 手机的机器。若不设置,recipe 会尝试寻找已连接的 Android 设备并可能报错,从而引发问题。
b.task_name
任务的名称,对应bin/tasks/{task_name}.dart中的任务文件名(去掉.dart后缀)。例如本示例的android_defines_test对应 dev/devicelab/bin/tasks/android_defines_test.dart,该任务会声明deviceOperatingSystem = DeviceOperatingSystem.android并执行dartDefinesTask()(见 dev/devicelab/lib/tasks/integration_tests.dart),用于验证 Android 上--dart-define是否生效。
c.use_emulator: "true"
告诉测试 recipe 需要为该测试创建一个模拟器。注意:这里是字符串"true"而非布尔值true。
d.dependencies:覆盖 AVD 版本
dependencies: >- [ {"dependency": "android_virtual_device", "version": "31"} ]dependencies可用于覆盖android_virtual_device的 API 版本。此处version: "31"表示希望使用 API 31 的系统镜像;若某平台组已默认声明依赖,则可省略这一项(详见下文“更新既有目标”)。
e.tags: ["devicelab", "linux"]
tags应设置为devicelab与linux,供仪表盘与调度逻辑归类任务。
f. properties 内层 timeout
timeout: 300这是给测试本身运行的时间预算,超时即被终止(与目标层timeout是两回事)。
为便于速查,将上述字段整理为对照表:
| 字段 | 位置 | 取值/写法 | 作用 |
|---|---|---|---|
name | 目标顶层 | Linux或Linux_Android(+ 空格 + 任务名) | 决定平台前缀与可读命名;Linux_Android携带更全的 Android 依赖 |
recipe | 目标顶层 | 恒为devicelab/devicelab_drone | 负责创建模拟器并驱动测试 |
presubmit | 目标顶层 | true/false | 是否在 PR 上运行 |
timeout | 目标顶层 | 整数(分钟) | LUCI 整体任务超时 |
dimensions | 目标顶层 | kvm:"1"、cores:"8"、Machine_name:"n1-standard-8" | 要求支持嵌套虚拟化的机器 |
device_type | properties | "none" | 使用无外接真机的机器,缺省会引发问题 |
task_name | properties | bin/tasks/*.dart去掉后缀的名字 | 指定要执行的 Devicelab 任务 |
use_emulator | properties | 字符串"true" | 要求 recipe 创建模拟器 |
dependencies | properties | android_virtual_device+ API 版本 | 覆盖 AVD 系统镜像版本 |
tags | properties | ["devicelab","linux"] | 任务归类标签 |
timeout | properties | 整数 | 测试运行时间预算,超时杀掉 |
当前仓库的真实落地形态:platform_properties 与 Linux_android_emu
原文档描述的机制(在目标内联声明dimensions、use_emulator、AVD 依赖等)在当前仓库中已进一步组织为platform_properties平台组,让同一平台的所有目标共享一套模拟器配置。
以 .ci.yaml 中的linux_android_emu为例,其公共属性包括:
contexts: ["android_virtual_device"]:为所有该平台目标注入 AVD 上下文;device_type: none:与文档要求一致;kvm: "1"、cores: "8":嵌套虚拟化与算力要求;dependencies:默认声明了android_sdk version:37v2、android_virtual_device version:android_36_google_apis_x64.textpb、avd_cipd_version build_id:...、open_jdk 21、clang、cmake、ninja、gradle_dists等完整工具链。
同时仓库还维护了两个变体平台组:
linux_android_emu_unstable(.ci.yaml):用于验证新的依赖组合组合的稳定性;linux_android_emu_vulkan_stable(.ci.yaml):Vulkan 稳定通道,其注释明确说明因 API 36 上虚拟显示存在潜在问题(对应 Flutter issue #170024),暂时改用 API 35 的 AVD(android_35_google_apis_x64.textpb)。
从 .ci.yaml 头部注释还可以看到 AVD 配置名的来源说明:合法的android_virtual_device版本名可在 Chromium 的tools/android/avd/proto目录下查找,而avd_cipd_version可通过 CIPD 页面最新实例中的build_id:<Identifier#>获取。
因此,在当前仓库中新增模拟器目标的实际写法更简洁——直接以Linux_android_emu为前缀声明目标即可,无需在目标内重复kvm、device_type与 AVD 依赖。真实例子可见 .ci.yaml:
- name: Linux_android_emu android_defines_test recipe: devicelab/devicelab_drone timeout: 60 properties: tags: > ["devicelab", "linux"] task_name: android_defines_test presubmit_max_attempts: "2"同类目标还有Linux_android_emu android views(.ci.yaml)、Linux_android_emu android_display_cutout、Linux_android_emu external_textures_integration_test以及引擎侧渲染测试Linux_android_emu android_engine_opengles_tests、Linux_android_emu_vulkan_stable android_hardware_smoke_vulkan_tests等,均可作为仿写模板。
把既有真机目标改造成模拟器目标:五个步骤
如果只是想更新一个既有目标(例如把某个跑在 Pixel 真机上的目标迁移到模拟器),只需做如下五处修改:
- 添加
dimensions字段:内容如上一节所示,声明kvm: "1"等嵌套虚拟化要求; - 设置
device_type: "none":确保任务被调度到无外接手机、可自建模拟器的机器; - 在 properties 中添加
use_emulator: "true":再次强调,它是字符串而非布尔值("true"而不是true); - 添加模拟器版本依赖:在 properties 的
dependencies中加入android_virtual_device条目;若目标原本没有dependencies字段,则按前文完整示例那样整体新增; - 移除任何标注 android 的 tags:如
["devicelab", "android", ...]。这类 tag 是为基准测试(benchmark)预留的,保留它们会导致部分设备检查(device checks)失败。
改造后的目标同样可以依赖当前仓库中预置的Linux_android_emu平台组,使第 1、2、4 步由平台公共属性代为提供,目标内只需声明task_name、tags等差异项。
框架源码视角:use_emulator 在 Devicelab 内部如何生效
了解 CI 配置后,再深入 Devicelab 框架源码,可以看到use_emulator这条链路在任务运行器中的具体处理。
在 dev/devicelab/lib/framework/runner.dart 的runTask中,存在一个名为useEmulator的参数(来自rerunTask/run一路透传)。当其为真时,运行器会在启动任务进程前向taskArgs追加两个参数:
if (useEmulator) { taskArgs ??= <String>[]; taskArgs ..add('--android-emulator') ..add('--browser-name=android-chrome'); }也就是说,模拟器模式会同时附加--android-emulator与--browser-name=android-chrome——后者服务于后续基于 Android Chrome 的 Web/集成测试运行。任务侧同样存在对应处理,例如 dev/devicelab/lib/tasks/android_views_test.dart 就显式接收这两个参数。
任务进程随后在独立的 Dart VM 中运行,执行体为bin/tasks/{task_name}.dart。以文档示例android_defines_test为例,其入口 dev/devicelab/bin/tasks/android_defines_test.dart 内容非常简短——设置deviceOperatingSystem = DeviceOperatingSystem.android后调用dartDefinesTask(),具体的构建、安装、运行与校验逻辑封装在 Devicelab 的 task 类库中。
设备抽象层位于 dev/devicelab/lib/framework/devices.dart:框架内部以AndroidDevice(第 607 行起)等类封装对 adb 设备的控制(唤醒、HOME、锁屏、性能模式等),并通过AndroidDeviceDiscovery发现可用设备。作为对照,iOS 的物理设备发现逻辑(IosDeviceDiscovery,第 1000 行附近)会调用flutter devices --machine,并在筛选中显式排除emulator属性为真的设备——这反过来说明「物理真机」与「模拟器」在 Devicelab 中是两条不同的设备通道,Android 模拟器通道正是由devicelab_dronerecipe 在任务运行前提前拉起。
本地验证与排障建议
虽然模拟器目标最终在 LUCI 上执行,但 Devicelab 任务本身完全可以在本地复现(详见 dev/devicelab/README.md):
# 在 dev/devicelab 目录下运行单个任务 ../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t {TASK_NAME} # 关闭默认的自动重试,便于定位真实失败原因 ../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test --exit -t {TASK_NAME} # 针对本地 engine 构建做 A/B 或对照验证 ../../bin/cache/dart-sdk/bin/dart bin/run.dart --task={TASK_NAME} \ --local-engine-src-path=[路径]/engine/src \ --local-engine=[本地引擎架构,如 android_debug_unopt_x86] \ --local-engine-host=[宿主架构,如 host_debug_unopt]运行 Android 相关任务前需要保证ANDROID_SDK_ROOT已正确设置(可通过flutter doctor -v确认 SDK 位置)。Devicelab 的默认失败重试逻辑会自动重跑 2 次,若想快速暴露偶发问题请加--exit。
针对模拟器目标,排查时可重点核对本文列出的五个要点:device_type是否为none、use_emulator是否为字符串"true"、dependencies是否声明了android_virtual_device、dimensions是否包含kvm、tags 是否混入了android。另外,若要在 PR 阶段大量使用模拟器任务,请留意 Devicelab 的 presubmit 池容量有限(dev/devicelab/README.md 的 "Adding tests to presubmit" 一节建议向team-infra提出 issue 评估可行性)。
小结
在 Devicelab 中用 Android 模拟器验证 Android 侧改动,是物理设备池之外一条成本更低、可自动化的回归路径。核心要点可以归结为三句话:新增目标时完整声明device_type: "none"、字符串形式的use_emulator: "true"以及android_virtual_device依赖,并把平台选为携带完整 Android 依赖的Linux_Android(当前仓库对应Linux_android_emu平台组);更新既有目标时补上dimensions、删掉 android 标签即可完成迁移;presubmit: true是在改动镜像到 google3 之前尽早发现缺陷的最佳实践。
更详尽的 CI 目标语法可继续研读 .ci.yaml 头部注释及其真实目标条目,框架层机制可深入 dev/devicelab/lib/framework/runner.dart 与 dev/devicelab/lib/framework/devices.dart 验证。
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考