首页
/ 在 PR 上重跑 Flutter DeviceLab 测试:基于 LUCI `led` 的完整实战指南

在 PR 上重跑 Flutter DeviceLab 测试:基于 LUCI `led` 的完整实战指南

2026-09-06 18:41:48作者:贡沫苏Truman

DeviceLab(设备实验室)是 Flutter 在真实物理设备上执行集成测试与基准测试的基础设施。当你的 PR 已通过全部预提交(presubmit)检查,但合入后在 dashboard 上留下红色记录,或者你想在提交前反复执行某条 flaky 测试来验证其稳定性时,就需要把一条“后提交(post-submit)级别”的 DeviceLab 测试临时挂到 PR 的代码上再跑一次。本文以 Flutter 主仓库 中的 docs/infra/Running-Devicelab-Tests-For-PR.md 为核心骨架,结合 .ci.yamldev/botsdev/devicelab 等源码佐证,完整讲解用 LUCI 的 led 工具在 PR 上手动触发 DeviceLab 测试的流程、每条命令的真实含义与可落地的操作示例。

何时需要“在 PR 上跑 DeviceLab 测试”

常规开发流程中,普通 PR 只会触发预提交测试;而 DeviceLab 测试大多为后提交(post-submit)任务,通常在代码合入 main 分支后才执行,且容量有限。以下几种典型场景会迫使你手动把某条后提交测试临时“挂”到某个 PR 上去执行:

  • 修复已合入代码引入的回归:PR 合入前预提交全绿,合入后 dashboard 上的 DeviceLab 任务变红,需要快速验证修复是否有效;
  • 定位 flaky 测试:某条测试偶尔红偶尔绿,你想连续复跑若干次观察稳定性,再决定如何修复与落地;
  • 验证仅在后提交阶段才会执行的任务:例如部分 devicelab benchmark 任务在 .ci.yaml 中被标记为 presubmit: false,普通 PR 根本不会触发它们,必须手动干预。

上述情形都可以通过本文介绍的 led 流水线,将某个 builder 的源码指针(git_ref)重定向到目标 PR 的 refs/pull/<PR_NUMBER>/head,从而让 LUCI 以“该 PR 的代码”为待测版本执行一次完整任务。

前置准备:理解 Flutter 的构建基础设施

动手之前,请先阅读仓库根目录下 dev/bots/README.md 中列出的先决条件(该文件同时也是原文档 Running-Devicelab-Tests-For-PR.md 显式要求你遵循的 Prerequisites)。

Flutter 的构建基础设施建立在 LUCI(Layered Universal Continuous Integration)之上:

  • 仓库根目录的 .ci.yaml 与引擎侧的 engine/src/flutter/.ci.yaml 分别配置了 framework 与 engine 的 CI 目标;
  • 所有 PR 与提交由 LUCI bot 通过 dev/bots/test.dart 脚本驱动,该脚本负责工具、framework 的测试,提交后的变更还会重建 main 分支 API 文档站点;
  • 构建结果的构建面板地址为 https://flutter-dashboard.appspot.com,其中 #/build 页面展示了物理设备上的后提交测试状态(参见 dev/devicelab/README.md)。

需要准备的运行环境

要打通本流程,需要具备以下环境(依据 dev/bots/README.md):

  1. 安装 depot_tools 并加入 PATHled 正是由 depot_tools 提供的 CLI 工具;dev/bots/README.md 特别提示:如果 led 执行失败,请先确认你的 depot_tools 检出是否已更新到最新。
  2. 具备基础设施操作权限:Flutter 的 infra 对“重新触发构建”有特殊权限要求;没有权限时需提交 team-infra issue 申请(见 dev/bots/README.md 开篇说明)。
  3. 一份 recipes 仓库的本地检出:因为流水线中的 led edit-recipe-bundle 会把你本地 recipes 检出中携带的 recipe bundle 打入任务,命令必须在一个 recipes 仓库检出中执行(原文档第 2 步即注明 “From the recipes repository checkout”)。recipes 项目可通过 git clone https://flutter.googlesource.com/recipes 获得。

[!Warning] 请务必先完成 dev/bots/README.md 中的前置准备步骤,再执行下文命令。

第一步:收集两个关键参数

在执行任何命令之前,先收集以下两项信息:

  1. PR_NUMBER:你要把测试挂到其代码上的那个 PR 的编号。

  2. 目标测试对应的 builder 完整名称:即 .ci.yaml 中定义的 name 字段,采用“平台前缀 + 空格 + 任务名”的完整写法。原文档给出的示例是:

    Windows_mokey hot_mode_dev_cycle_win__benchmark
    

    这一名称并非虚构。在仓库根目录 .ci.yaml 中可以找到完全一致的定义:

    # windows mokey benchmark
    - name: Windows_mokey hot_mode_dev_cycle_win__benchmark
      recipe: devicelab/devicelab_drone
      presubmit: false
      timeout: 60
      properties:
        tags: >
          ["devicelab", "android", "windows", "mokey"]
        task_name: hot_mode_dev_cycle_win__benchmark
    

    从该定义可以读出几层重要信息:

    • recipe: devicelab/devicelab_drone:这条 builder 使用 DeviceLab 的 drone recipe 执行,属于物理设备测试流水线;
    • presubmit: false:它默认不会在 PR 预提交中运行,这正是需要“手动在 PR 上触发”的典型对象;
    • task_namehot_mode_dev_cycle_win__benchmark 才是 devicelab 内部的任务名(一个任务对应 devicelab 中的一个 Dart 测试程序,可参见 dev/devicelab/bin/tasks 目录);
    • tags:标明该任务需要的设备池(devicelab / android / windows / mokey);
    • timeout: 60:单位为分钟,60 分钟超时上限。

    因此在确定“要跑哪条测试”时,正确做法是:先在 .ci.yaml 中搜索 task_name 找到目标任务,再向上读取它所在目标的 name 字段,取出那个完整的 builder 名称(含 Linux_mokeyMac_benchmarkWindows_mokey 这类平台前缀),而不是只取裸的 task_name

第二步:执行 led 触发流水线

在原文档给出的最简流程中,确认好参数后,在一个 recipes 仓库检出目录 中执行以下命令即可(PR_NUMBERPRESUBMIT_TEST 按实际值替换):

led get-builder 'luci.flutter.staging:PRESUBMIT_TEST' \
| led edit -pa git_ref='refs/pull/PR_NUMBER/head' \
| led edit -pa git_url='https://github.com/flutter/flutter' \
| led edit-recipe-bundle \
| led launch

把它代入前面收集到的具体值(以 Windows_mokey hot_mode_dev_cycle_win__benchmark 与 PR #12345 为例),完整可执行版本为:

led get-builder 'luci.flutter.staging:Windows_mokey hot_mode_dev_cycle_win__benchmark' \
| led edit -pa git_ref='refs/pull/12345/head' \
| led edit -pa git_url='https://github.com/flutter/flutter' \
| led edit-recipe-bundle \
| led launch

注:示例中的 Windows_mokey.ci.yaml 里真实存在的 builder 前缀,并非拼写错误;mokey 是 Flutter infra 内部对 Windows + Android 组合设备池的既有命名。

逐段理解这条命令

led(LUCI Editor)的基本工作方式是“读取一份 job 定义 → 用管道逐级修改 → 交给 swarming 执行”。这条流水线每一段职责如下:

命令段 作用
led get-builder 'luci.flutter.staging:BUILDER' 从 LUCI 的 luci.flutter.staging 桶中抓取该 builder 的完整 job 定义(recipe 引用 + 属性 + 依赖)。使用 staging 桶意味着这次实验性重跑不会占用生产桶(prod)的配额,也不影响正式的提交记录;staging 中通常维护着一套与 prod 镜像的 builder 定义。
led edit -pa git_ref='refs/pull/PR_NUMBER/head' 修改 job 属性的 Git ref 指针,让 LUCI 检出目标 PR 的 head 作为待测代码。Flutter 使用 GitHub 的 pull request refs 命名约定 refs/pull/<编号>/head
led edit -pa git_url='https://github.com/flutter/flutter' 指定代码仓库地址,确保步骤二中的 ref 指向正确仓库的 PR ref。
led edit-recipe-bundle 把你本地 recipes 检出中的 recipe 定义重新打包进 job。这既保证了任务使用与本地一致的 recipe 版本,也是“必须在 recipes 检出目录内执行”这一要求的原因。
led launch 将最终 job 提交到 LUCI 调度,由空闲的 swarming bot 领取执行。

这条流水线与 dev/bots/README.md 中“编辑 recipe 后用 led 验证改动”所用的管线结构完全一致(后者写作一行管道形式),差异仅在参数 git_ref/git_url 的具体值——那里用于构建提交中的改动,而本文用于构建某个 PR 的 head。

第三步:观察执行结果

led launch 成功后,LUCI 会为该 job 分配 swarming bot 并返回对应的任务地址。你可以:

需要特别说明 DeviceLab 的自动重试机制(依据 dev/devicelab/README.md):

  • 任务失败时会自动重跑(默认最多额外重试 2 次),只要最终一次成功即记为成功;
  • 若全部重试均失败,测试运行器会把失败上报到基础设施,且不采集性能指标
  • 连续重试后成功会标记 flake 并写入测试结果。

因此如果你是为了判断“某条测试是否 flaky”,连续重跑几次后需要结合任务页面里每次 run 的原始结果来判断,而不只是看最终绿/红状态。

失败排查与权限边界

结合 dev/bots/README.mddev/devicelab/README.md,几个常见问题如下:

  • led 命令执行失败:优先升级 depot_tools 检出;Flutter infra 的很多工具都依赖 depot_tools 的最新版本(dev/bots/README.md)。
  • 没有重跑/触发权限:重新触发构建需要特殊权限,请提交 team-infra issue 申请(dev/bots/README.md 开篇)。
  • 找不到 builder 名:确认你拿到的不是裸 task_name,而是 .ci.yaml 中带平台前缀的完整 name;并确认该 builder 在 luci.flutter.staging 桶下存在。
  • 想新增一条预提交测试:DeviceLab 在 presubmit 中的容量非常有限,需先提交 team-infra issue 评估可行性,切勿直接修改配置把大量 devicelab 任务设为 presubmit: true(见 dev/devicelab/README.md 的 “Adding tests to presubmit” 一节)。

补充:在本地先复现,再上 CI

在反复占用真实设备跑 CI 之前,更经济的路径是先本地复现。devicelab 测试本就是仓库内的一段 Dart 程序(入口封装在 dev/devicelab/lib/framework/framework.dart,任务实现散落在 dev/devicelab/bin/tasksdev/devicelab/lib/tasks),因此在 .../flutter/dev/devicelab 目录下可以直接用测试运行器执行单条任务:

../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t {NAME_OF_TEST}

其中 NAME_OF_TEST 是任务文件名去掉 .dart 后缀的名字,例如 flutter_gallery__transition_perf。追加 --exit 可以关闭默认的自动重试逻辑,便于快速暴露真实失败。如需对本地引擎构建做性能对比,dev/devicelab/bin/run.dart 还支持 --ab=N --local-engine=... 的 A/B 模式。详细命令族请查阅 dev/devicelab/README.md 的 “Running tests locally” 一节。

本地复现与 PR 触发互为补充:前者帮你用最小成本定位根因,后者帮你以真实的后提交环境验证修复是否通过。

总结

在 Flutter 仓库上手动于 PR 中运行 DeviceLab 后提交测试,本质是:把一条平时只在合入后执行的 builder,通过 LUCI led 的逐段编辑能力,临时指向某个 PR 的 head 代码,再投递到设备池中运行。只需记住四个要素即可随时操作:

  1. 准备 depot_tools、infra 权限与一份 recipes 检出(见 dev/bots/README.md);
  2. .ci.yaml 中检索带平台前缀的完整 builder 名;
  3. 在 recipes 检出目录中执行 led get-builderled edit -pa git_ref='refs/pull/<PR_NUMBER>/head'led edit -pa git_url=...led edit-recipe-bundleled launch 的管道;
  4. 在任务页面与 Flutter 构建面板 上跟踪结果,并结合 DeviceLab 的自动重试语义判断 flake。

这条流程既能帮你快速修复 dashboard 上的红色记录,也能在你落地 flaky 测试修复前提供充分的重复验证依据。关于更底层的 builder/recipe 编辑与发布流程,可继续深入阅读仓库中的 dev/bots/README.mddev/devicelab/README.md

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