首页
/ Flutter CI 发布冒烟测试:release_smoke_test 如何保证 release 模式应用在真机上可构建、可运行

Flutter CI 发布冒烟测试:release_smoke_test 如何保证 release 模式应用在真机上可构建、可运行

2026-09-06 11:39:15作者:董宙帆

release_smoke_test 是 Flutter 仓库 CI 流水线中的一个专项冒烟测试工程:它的职责非常单一——验证一个 release 模式的 Flutter 应用能够在真机上完成构建并正常运行,且运行过程中不产生任何 E/flutter 级别错误。读完本文,你将了解该工程的目录结构与依赖组织方式、main.dart 中针对 release 模式特性预埋的多个回归检查点、它如何通过 .ci.yaml 接入 Firebase Test Lab 物理/虚拟设备矩阵,以及 CI 判定测试通过的底层依据(logcat 中不得出现 E/flutter),从而掌握 Flutter 官方 CI 中"发布前真机冒烟验证"的完整机制。

工程定位:一个极简的 release 模式冒烟用例

工程说明文档 dev/integration_tests/release_smoke_test/README.md 对其定位只有三句话:"A simple Flutter project used in CI to test that a release app can build and run on a physical device."(一个用于 CI 的简单 Flutter 工程,用于测试 release 应用能否在真机上构建并运行)。

这句话就是整个工程的全部设计目标,也决定了它的实现风格:

  • 应用本身只有一个 "Hello, world!" 静态文本界面,不存在任何业务逻辑;
  • 所有"测试价值"都来自两个方面:一是 release 模式与 debug/profile 模式行为差异的回归检查(写在 main.dart 中,靠日志 grep 判定);二是 真实设备上的构建与启动(由 Firebase Test Lab 完成,靠设备执行结果判定)。

它位于 dev/integration_tests/ 集成测试目录下,与 android_viewschannels 等工程并列,是 Firebase Test Lab 体系(详见 docs/infra/Flutter-FirebaseLab-Tests.md)的一个标准 task_name

工程结构与依赖:workspace 解析 + 最小化依赖

dev/integration_tests/release_smoke_test/pubspec.yaml 体现了"最小化"原则,全文仅 19 行:

name: release_smoke_test

environment:
  sdk: ^3.11.0-0

resolution: workspace

dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

几个值得注意的配置点:

  1. resolution: workspace:该工程加入 Flutter 仓库根目录的 workspace 统一依赖解析,与仓库内其他 dev 工程共享依赖版本锁定策略,避免各自维护独立的 pubspec.lock 漂移。
  2. SDK 约束 ^3.11.0-0:允许 prerelease 版本号的 caret 范围,跟随仓库滚动 Dart SDK 版本。
  3. 依赖面收敛到 SDK 内置包:运行时仅依赖 flutter,测试侧仅 flutter_testintegration_test(均为 sdk: flutter 来源,指向仓库内 packages/integration_test),不引入任何第三方包——冒烟测试自身不应成为外部依赖故障的受害者。

工程目录布局为标准的 Flutter Android 应用加一个 Dart 测试适配层:

dev/integration_tests/release_smoke_test/
├── lib/main.dart                      # 应用入口,内含 release 模式回归检查
├── test_adapter/hello_world_test.dart # Dart 侧集成测试
├── android/                           # Flutter Gradle 插件标准工程
│   └── app/src/androidTest/.../MainActivityTest.java  # Android instrumented 测试适配
├── ios/                               # iOS Runner(供 Firebase iOS 场景复用)
└── pubspec.yaml

核心代码:main.dart 中预埋的 release 模式回归检查

冒烟测试的"测试逻辑"并不在 test 目录里,而是直接写在应用入口 dev/integration_tests/release_smoke_test/lib/main.dart 中。原因在于:这些代码在 release 模式下才会触发差异化的运行时路径,而 release 构建会剥离断言与调试基础设施,无法通过普通的 flutter_test 模拟。

Future<void> main() async {
  const text = Text('Hello, world!', textDirection: TextDirection.ltr);
  // These calls must not result in an error. They behave differently in
  // release mode compared to debug or profile.
  // The test will grep logcat for any errors emitted by Flutter.
  print(text.toDiagnosticsNode());
  print(text.toStringDeep());
  // regression test for https://github.com/flutter/flutter/issues/49601
  final List<int> computed = await compute(_utf8Encode, 'test');
  print(computed);

  // regression test for https://github.com/flutter/flutter/issues/148983
  const value = 'testValueKey';
  const valueKey = ValueKey<String>(value);
  if (!valueKey.toString().contains(value)) {
    throw Exception('ValueKey string does not contain the value');
  }

  runApp(const Center(child: text));
}

逐项解读源码中的四个检查点:

检查点 验证的 release 模式行为 关联问题
text.toDiagnosticsNode() / text.toStringDeep() 打印 诊断工具链(DiagnosticsNode、深度字符串化)在 debug 下功能完整,而 release 构建中大量诊断路径被条件编译剔除(kFlutterTestMode/assert 相关分支)。此处要求这些调用在 release 下不抛错,防止诊断代码在缺失调试支持时崩溃
compute(_utf8Encode, 'test') 跨 isolate 计算 release 模式下 isolate 创建与 AOT 编译路径与 debug 不同,曾出现 compute 在 release 中异常的问题 issue 49601
ValueKey<String>('testValueKey').toString() 包含原始值 release 模式下 toString 输出路径(Object.toString 的默认实现与 key 的字符串化)曾发生回归,导致 key 不再包含其值 issue 148983
runApp(Center(child: text)) 渲染 "Hello, world!" 最基本的 release 帧调度与布局验证,配合 Dart 侧集成测试断言文本存在

源码注释明确写道:"The test will grep logcat for any errors emitted by Flutter."(测试将 grep logcat 中 Flutter 发出的任何错误)。也就是说,main.dart 的判定标准不是"没有异常堆栈打印",而是 logcat 中不存在 E/flutter 前缀的日志——这与后文 Firebase Lab 的通过判据完全一致。

测试适配层:Dart 集成测试与 Android instrumented 测试

工程提供了两层测试入口,供不同执行场景使用。

Dart 侧dev/integration_tests/release_smoke_test/test_adapter/hello_world_test.dart 使用 integration_test 包的 IntegrationTestWidgetsFlutterBinding,直接调用应用入口 smoke.main() 并触发一帧,随后断言 "Hello, world!" 文本被渲染:

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Hello world smoke test', (WidgetTester tester) async {
    await smoke.main(); // builds the app and schedules a frame but doesn't trigger one

    await tester.pump(); // triggers a frame

    expect(find.text('Hello, world!'), findsOneWidget);
  });
}

Android 侧MainActivityTest.java 是一个"空壳" instrumented 测试——仅声明 ActivityTestRule 加载 MainActivity,没有任何断言:

@RunWith(FlutterRunner.class)
public class MainActivityTest {
  @Rule
  public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
}

从源码结构看,这个空测试的作用是充当 Firebase Test Lab 的执行载体:Test Lab 需要 androidTest 中的 instrumented runner 来安装、启动应用并在真机上运行;真正的验证逻辑(release 行为 + 日志 grep)在应用 main() 与 CI 的 logcat 检查中完成,Android 测试类本身只是让设备"把应用跑起来"。它使用的 dev.flutter.plugins.instrumentationadapter.FlutterRunner 即 integration_test 包随 Android 平台提供的 instrumentation 适配插件。

Android 平台构建配置

android/app/build.gradle 是标准的 Flutter Android 插件模板工程,两处与冒烟测试直接相关的配置:

  • minSdkVersiontargetSdkVersioncompileSdkndkVersion 均取自 dev.flutter.flutter-gradle-plugin 暴露的 flutter.* 属性(第 30–43 行),使工程自动跟随 Flutter 工具链的 SDK 版本,无需手工同步;
  • release 构建类型直接复用 debug 签名(第 49–55 行,注释注明 "Signing with the debug keys for now, so flutter run --release works")——冒烟测试不关心正式签名,只需保证 release 构建管线(含混淆、剥离 assert、AOT 编译)完整走通;
  • testInstrumentationRunner 配置为 androidx.test.runner.AndroidJUnitRunner(第 46 行),与 MainActivityTest 配合,供 androidTest 在设备上启动。

iOS 侧提供了完整的 Runner 工程(ios/RunnerPodfile、Xcode 工程),保证同一 task_name 在 Firebase 的 iOS 设备矩阵上也可执行;ios/Flutter/Release.xcconfigDebug.xcconfig 分离,确保 release 配置路径同样被覆盖。

CI 接入:.ci.yaml 中的 firebaselab 目标

该工程在 CI 中的定义位于 .ci.yaml,目标名为 Linux firebase_release_smoke_test

- name: Linux firebase_release_smoke_test
  recipe: firebaselab/firebaselab
  timeout: 60
  properties:
    dependencies: >-
      [
        {"dependency": "android_sdk", "version": "version:37v2"},
        {"dependency": "open_jdk", "version": "version:21"},
        {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"},
        {"dependency": "cmake", "version": "build_id:8787856497187628321"},
        {"dependency": "ninja", "version": "version:1.9.0"},
        {"dependency": "gradle_dists", "version": "8.4-bin:8.13-rc-1-bin:8.14-bin:9.3.1-bin:9.3.1-all"}
      ]
    tags: >
      ["firebaselab"]
    task_name: release_smoke_test
    # TODO(flutter/flutter#181954) Restore the Pixel 9/API 36 device when
    # it is available in Firebase Test Lab.
    physical_devices: >-
      [
        "--device", "model=shiba,version=34",
        "--device", "model=redfin,version=30",
        "--device", "model=griffin,version=24"
      ]
    virtual_devices: >-
      [
        "--device", "model=Nexus5.gce_x86,version=21",
        "--device", "model=Nexus5.gce_x86,version=22",
        "--device", "model=Nexus5.gce_x86,version=23",
        "--device", "model=Nexus6P,version=25",
        "--device", "model=MediumPhone.arm,version=26",
        "--device", "model=MediumPhone.arm,version=27",
        "--device", "model=SmallPhone.arm,version=29"
      ]

配置要点:

  • recipe: firebaselab/firebaselab:指定由 Firebaselab recipe 执行;task_name: release_smoke_testdev/integration_tests/ 下的本目录名,recipe 据此定位要构建的集成测试工程;
  • 物理设备矩阵:三台真实硬件——shiba(Pixel 8 Pro,API 34)、redfin(Pixel 5,API 30)、griffin(Pixel 4a,API 24),覆盖新、中、旧三代 Android 版本。配置中的 TODO 注释说明 Pixel 9 / API 36(oriole)设备因在 Firebase Test Lab 上暂不可用而暂时撤下(issue 181954);
  • 虚拟设备矩阵:从 API 21(Android 5.0)到 API 29 的 7 个 AVD 档位,向下兼容到相当老的系统版本;
  • 依赖锁定:android_sdk 37v2、JDK 21、固定 git revision 的 clang 与 cmake、ninja 1.9.0 及一组 gradle_dists,保证构建环境与主线一致、可复现;
  • timeout: 60(分钟),tags: ["firebaselab"] 用于指标采集与 swarming 任务过滤。

执行机制:Firebase Test Lab 如何判定"通过"

关于这类 firebaselab 目标的完整执行流程,仓库文档 docs/infra/Flutter-FirebaseLab-Tests.md 给出了权威描述,可归纳为六步:

  1. 读取 physical_devices 属性;若非空,则为 task_name 指向的集成测试工程构建 App Bundle(真机用 bundle 以避免安装限制);
  2. 读取 virtual_devices 属性;若非空,则构建 APK——文档特别指出为虚拟设备构建 APK 是为了防止 AVD 选错二进制而落入运行时转译;
  3. 通过 gcloud firebase 命令上传二进制,把执行委托给 Firebase Test Lab;
  4. gcloud 命令阻塞直至执行完成;
  5. recipe 读取 logcat,仅当 logcat 文件中不存在任何 E/flutter 行时测试才判定为通过——这正是 main.dart 注释里 "The test will grep logcat" 的另一端;
  6. 若测试失败,最多重试 3 次;同时 recipe 支持 infra_failure_codes = (1, 15, 20),将 Firebase 基础设施故障与产品缺陷区分开,避免 infra 抖动关闭代码树。

把两条证据链合起来看,release_smoke_test 的完整闭环是:main.dart 在 release 模式下触发差异化的运行时路径并可能向 logcat 输出 E/flutter 级错误 → CI 在真机/AVD 上安装并运行 release 构建 → recipe grep logcat 判定 → 无 E/flutter 即通过。工程本身"简单",但它守住的是发布质量中最基础也最致命的一条线:release 模式产物在真实设备上能构建、能启动、不静默抛错

本地验证方式

在本地查看或演练该工程时(仓库为只读,以下仅描述运行方式):

  • 作为标准 Flutter 应用,可在工程目录下执行 flutter build apk --release 验证 release 构建管线走通(Android 侧 release 使用 debug 签名,无需额外密钥);
  • 在连接 Android 设备后,可运行 test_adapter/hello_world_test.dart 中的集成测试(基于 integration_test,需要真实设备而非模拟器断言环境)验证 "Hello, world!" 渲染路径;
  • 由于部分检查点(如 E/flutter grep)依赖 CI 的 logcat 采集,本地等价验证方式是运行 flutter run --release 后观察 logcat 输出中是否出现 E/flutter 前缀日志。

小结

release_smoke_test 展示了 Flutter 官方 CI 中一类典型的"以最小应用验证最大发布风险"的测试设计:应用逻辑收敛到一个静态文本界面,测试重心转移到 release 模式运行时行为(诊断打印、compute 跨 isolate、ValueKey 字符串化等历史回归点)与 真机构建/启动 两个维度;通过 .ci.yaml 的 firebaselab 目标接入覆盖 API 21–34 的虚拟与物理设备矩阵,并以 "logcat 无 E/flutter" 作为唯一判定标准、辅以 3 次重试与 infra 故障码隔离来降低误报。对于任何需要为自己的 Flutter 项目建立发布前真机冒烟验证的团队,这个工程的结构(最小应用 + 回归检查预埋 + logcat 判定 + 设备矩阵)提供了一个可直接参照的完整范式。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388