首页
/ Flutter customer_testing 工具深度解析:如何用“客户视角”的测试守住 tip-of-tree 稳定性

Flutter customer_testing 工具深度解析:如何用“客户视角”的测试守住 tip-of-tree 稳定性

2026-09-04 09:11:06作者:舒璇辛Bertina

customer_testing 是 Flutter 仓库中的一个回归验证工具,它通过检出 flutter/tests 仓库中注册的“客户测试”(即模拟真实用户应用与库的测试套件),在 Flutter 当前主干(tip-of-tree)上运行,以验证最新代码不会破坏真实世界的用户应用。读完本文,你将掌握如何在本地运行指定 SHA 的客户测试、如何更新 CI 使用的测试仓库版本、.test 测试注册文件的完整指令格式,以及底层执行器(分片、临时目录、fetch/setup/update/test 阶段)的实现细节。

工具定位与核心职责

按照 dev/customer_testing/README.md 的说明,该工具的职责非常明确:

  • tests.version 指定的提交 SHA 处检出 flutter/tests 仓库;
  • 运行其中注册的测试,以验证“当前 Flutter 主干上的端到端用户应用和库仍然可以正常工作”。

这与 Flutter 自身的单元测试或框架测试不同:它模拟的是下游消费者的视角——用户应用升级 Flutter 后能否继续编译和通过测试。当前 tests.version 中记录的 SHA 为 9853d386f609dc41aa22648131756f4eb69fee70pubspec.yaml 中也对该包有一句话描述:“Tool to run the tests listed in the flutter/tests repository”,并要求 Dart SDK ^3.11.0-0

本地运行:ci.dart 与 ci.sh 两种入口

README 给出的本地运行方式是:

cd dev/customer_testing
dart ci.dart [sha]

其中 [sha] 是可选参数:不传时默认读取 tests.version 中的 SHA,传入时则测试任意指定版本。这一点可以从 ci.dart 的参数处理逻辑中得到印证——args.isEmpty 时从 tests.version 读取,args.length == 1 时使用传入值,其他情况打印 Usage: dart ci.dart [sha] 并以退出码 1 结束。

ci.dart 的完整执行链如下:

  1. 清理缓存目录:定位 Flutter 根目录(p.canonicalize('../../')),若 bin/cache/pkg/tests 存在则先删除;
  2. 浅克隆 flutter/tests:git clone --depth 1bin/cache/pkg/tests;
  3. 检出目标 SHA:在克隆出的仓库中执行 git fetch origin <sha>git checkout <sha>;
  4. 运行测试:
dart --enable-asserts run_tests.dart \
  --skip-on-fetch-failure \
  --skip-template \
  <flutter-root>/bin/cache/pkg/tests/registry/*.test

注意两个固定附加的参数:--skip-on-fetch-failure 表示某个客户测试仓库拉取失败时跳过而不是判失败;--skip-template 表示跳过名为 template.test 的文件(那是给贡献者用的注册文件模板,不是真实测试)。测试文件路径使用了 p.posix.joinAll 拼接,以 registry/*.test 通配该仓库中所有已注册的测试。

除了直接 dart ci.dart,仓库还提供了一个跨平台的 Shell 入口 ci.sh(以及 Windows 版 ci.bat)。ci.sh 的逻辑是:

set -e
# 先切换到脚本自身所在目录,保证用户可以从哪里调用都能工作
cd "$(script_location)"
dart pub get
../../bin/dart run ci.dart

文件头注释中明确说明:该脚本由 LUCI recipes 调用(对应 dev/bots/suite_runners/run_customer_testing_tests.dart),并且不要求先运行 flutter update-packages——因为运行 flutter/tests 测试基本不需要,CI 可以借此省去这一步;但它确实需要对本目录执行 dart pub get

更新 CI 使用的测试版本:修改 tests.version

README 中给出的第二条操作路径是:要更新 CI 所使用的 SHA,直接编辑 tests.version 并提交 PR。该文件内容极其简单——只有一行 40 位的提交哈希:

9853d386f609dc41aa22648131756f4eb69fee70

由于 ci.dart 在无参数时正是读取这一行(io.File('tests.version').readAsStringSync().trim()),因此修改该文件并合并 PR,就等价于让所有 CI 和后续本地无参运行都切换到新的 flutter/tests 版本。

测试注册文件格式:.test 指令语法

flutter/tests 仓库中每个客户测试由一个 registry/*.test 文件描述,这些文件的解析逻辑全部集中在 lib/customer_test.dartCustomerTest 类中。文件逐行解析,空行和 # 开头的注释被忽略,每行必须以下列指令之一开头(否则会抛出 FormatException: Unexpected directive):

指令 是否可重复 说明
contact= 是(至少 1 个) 联系人邮箱,必须是合法邮箱且不能是 @example.com;测试失败时输出器会打印这些联系人
fetch= 是(至少 2 行) 拉取客户仓库的命令;第 1 行必须匹配 git clone https://github.com/<user>/<repo>.git tests 模式,第 2 行必须匹配 git -C tests checkout <hash> 模式(两行均允许可选的 -c core.longPaths=true 参数)
setup= 是(可选) fetch 成功后在客户仓库根目录(tests/)执行的准备命令;支持平台限定后缀 setup.windows=setup.macos=setup.linux=setup.posix=
update= 是(至少 1 个) 需要“升级依赖”的目录;. 表示仓库根。运行器会在该目录执行 flutter packages getdart fix --apply,该目录必须包含 pubspec.yaml
test= 是(至少 1 个) 实际执行的测试命令;同样支持 test.windows=test.posix= 等平台限定变体
iterations= 否(至多 1 个) 限定该测试最多重复执行的轮数,必须为正整数;与命令行 --repeat 取较小值

一个符合上述语法的完整注册文件示例(取自 test/customer_test_test.dart 的单元测试):

contact=abc@gmail.com
fetch=git clone https://github.com/flutter/cocoon.git tests
fetch=git -C tests checkout abc123
setup=flutter --version
setup.windows=flutter doctor
setup.posix=flutter -h
setup.linux=flutter analyze -h
setup.macos=flutter build -h
update=.
# Runs flutter analyze, flutter test, and builds web platform
test.posix=./test_utilities/bin/flutter_test_runner.sh app_flutter
test.posix=./test_utilities/bin/flutter_test_runner.sh repo_dashboard
test.windows=.\test_utilities\bin\flutter_test_runner.bat repo_dashboard

平台限定的实现见 _PlatformType 枚举:windows 对应 Platform.isWindows,posix 对应 Linux 或 macOS 任一成立时生效;无后缀的指令则视为全平台生效。校验规则同样有对应的自动化测试兜底:缺少 contact=、缺少 test=、只给一行 fetch=、出现未知指令等场景都会在 test/customer_test_test.dart 中触发 throwsFormatException 断言。

run_tests.dart:命令行参数全解

run_tests.dart 是真正的测试执行入口(ci.dart 最终调用的就是它)。使用格式为 run_tests.dart [options...] path/to/file1.test ...,所有路径参数支持 glob 通配(内部用 package:glob 展开)。完整参数表:

参数 默认值 说明
--repeat <count> 1 每个测试重复执行的次数;设为较大值可用于挖掘 flaky test。若测试自身声明了 iterations=,取两者中的较小值
--shards <count> 1 将测试拆分成多少个分片,用于 CI 并行
--shard-index <count> 0 当前分片的下标,取值范围 [0 .. shards - 1]
--skip-on-fetch-failure 拉取仓库失败时跳过对应测试而不是判为失败
--skip-template 跳过名为 template.test 的文件
--verbose 打印执行细节(临时目录、每条命令、输出日志等)
--help 打印帮助信息

参数校验也比较严格:shards 必须大于 0,shard-index 必须落在 [0, shards-1] 区间内,否则报错退出;当分片数多于测试文件数时会打印告警“Some shards will not run any tests”。

单个测试的执行生命周期

真正的执行逻辑在 lib/runner.dartrunTests 中,每个 .test 文件的处理流程是:

  1. 创建临时目录:在系统临时目录下创建 flutter_customer_testing.<测试名>.XXXX 目录,作为该测试的隔离工作区;
  2. fetch 阶段:依次执行注册文件中的每条 fetch= 命令(即 clone + checkout)。若某条失败且开启了 skipOnFetchFailure,则打印 “Skipping ... (fetch failed)” 并跳过该测试;
  3. setup 阶段:在 tests/ 子目录(即 clone 下来的客户仓库)中依次执行 setup= 命令,任何一条失败即判该测试失败;
  4. update 阶段:对每个 update= 目录先检查 pubspec.yaml 是否存在,然后执行 flutter packages get,再执行 dart fix --apply。这一步正是工具“模拟用户升级 Flutter”的关键动作——把客户仓库的依赖刷新到当前主干的 Flutter;
  5. test 阶段:按 repeat(受 iterations= 上限约束)轮次执行所有 test= 命令,任何一条命令非零退出即判失败并停止后续轮次;
  6. 计时与清理:无论成败,最终用 finally 块删除临时目录,并打印平均每轮耗时(Tests finished in X.XX seconds per iteration);
  7. 失败归因:测试失败时会额外打印注册文件中的 contact= 联系人列表,方便 Flutter 团队第一时间找到对应客户仓库的负责人。

命令执行本身由 shell 辅助函数完成:Windows 上通过 CMD.EXE /S /C 执行,其他平台按空白分割后直接 Process.start;非 verbose 模式下会先缓冲子进程输出,仅在命令失败时把完整输出打印出来,以避免噪音淹没主日志。

分片策略是简单的交错取模:for (i = shardIndex; i < files.length; i += numberShards) 让各分片均匀交错地拿到测试文件,而不是顺序切块。

CI 集成与整体调用链

把各环节串起来,完整的调用链是:

LUCI recipe
  -> dev/bots/suite_runners/run_customer_testing_tests.dart
  -> dev/customer_testing/ci.sh (或 ci.bat)
  -> dart pub get && ../../bin/dart run ci.dart
  -> git clone/checkout flutter/tests @ SHA
  -> dart --enable-asserts run_tests.dart --skip-on-fetch-failure --skip-template registry/*.test
  -> 逐个解析 .test 注册文件 (lib/customer_test.dart)
  -> 按 fetch / setup / update / test 阶段执行 (lib/runner.dart)

其中 ci.sh 顶部注释直接注明了它与 LUCI recipes 的对接关系,并指向 dev/bots/suite_runners/run_customer_testing_tests.dart 作为 CI 侧入口。这也解释了 --shards/--shard-index 参数的用途:CI 把同一批客户测试拆成多个并行 shard 执行,每个 shard 只跑交错分配给自己的那部分。

小结

  • 两个操作入口:本地验证任意 SHA 用 dart ci.dart [sha](或 ci.sh/ci.bat);更新 CI 基线则编辑 tests.version 提 PR。
  • 注册文件格式:contact=/fetch=/setup=/update=/test=/iterations= 六类指令,fetch 固定为“clone 到 tests 目录 + checkout 指定哈希”两行,setuptest 支持 .windows/.macos/.linux/.posix 平台后缀,完整规则由 lib/customer_test.dart 解析并强校验。
  • 执行器特性:--repeat/iterations 双重控制重复轮次、--shards/--shard-index 交错分片、--skip-on-fetch-failure 容忍拉取失败、每个测试独立临时目录并在结束后清理。
  • 失败可追溯:注册文件中的联系人会在失败时自动打印,配合 update 阶段的 flutter packages get + dart fix --apply,构成了“模拟真实用户升级 Flutter”这一完整验证闭环。
登录后查看全文
热门项目推荐
相关项目推荐