CI 测试计划选择器(test_plan_v2)
CI 测试计划选择器是位于 scripts/ci/test_plan_v2.py 的模块化 Python 脚本。它分析拉取请求中变更的文件,生成有针对性的 twister 测试计划,只运行可能受改动影响的测试,避免每次提交都执行全量测试。
脚本写出两个产物:
testplan.json:通过twister --load-tests传给 twister。.testplan:供 CI 编排脚本使用的纯文本KEY=value环境文件,包含TWISTER_TESTS、TWISTER_NODES和TWISTER_FULL。
架构
选择器围绕 策略流水线 构建。每个策略都是独立分析器,执行以下操作:
接收变更文件列表,或尚未被前序策略消费的子集。
按自身逻辑检查这些文件。
返回
TwisterCall描述符列表及已 处理 文件集合。
Orchestrator 驱动流水线,将所有结果合并到 PlanAccumulator,去除重复测试套件并写出文件。
策略顺序
策略按固定顺序执行,遵循两个原则:
具体优先于通用。 最精确的策略先运行。位于 tests/kernel/sched/ 的文件先由 DirectTestStrategy 明确处理,仅运行对应测试,之后兜底的 MaintainerAreaStrategy 才有机会加入整个 Kernel 领域的测试。
消费优先于追加。 消费型策略先于追加型策略执行。一旦文件被消费,后续策略就不会再看到它。这样可防止同一开发板文件变更还触发驱动兼容性扫描和 Kconfig 全面检查。
当前顺序如下:
# |
策略 |
是否消费 |
理由 |
|---|---|---|---|
0 |
|
否 |
使用 pydriller 和 lizard 对补丁集评分。必须最先运行,供 |
1 |
|
是 |
静默丢弃不可能影响测试的文件,例如文档、CI 工作流和工具。尽早运行可避免后续策略在忽略路径上浪费精力。 |
1b |
|
是 |
消费差异完全由空白调整、空行变化、SPDX 许可证标识、版权声明或单独注释分隔符组成的文件。这些改动不影响运行时行为,提前移除可让后续策略专注实质改动。需要 Git 提交范围,否则不执行操作。 |
2 |
|
是 |
测试或示例文件变化应 仅 触发对应测试,而非整个领域的测试。消费该文件可防止修改 |
3 |
|
是 |
代码片段变更是独立的,只有在 |
4 |
|
是 |
开发板专用改动需要对每个开发板变体执行定向集成测试。消费文件可防止开发板 |
5 |
|
是 |
SoC 级改动影响基于该 SoC 系列的所有开发板。消费后可避免同一 |
6 |
|
是 |
|
7 |
|
否 |
追加型:驱动文件也可能受领域模式覆盖,因此不消费文件, |
8 |
|
否 |
追加型:绑定变更同时采用基于 overlay 的测试选择和面向开发板的领域调用。 |
9 |
|
否 |
追加型:Kconfig 变更可能影响许多不相关文件;仅发现非广泛使用符号时才消费文件,广泛使用符号所在文件留给兜底策略。 |
10 |
|
否 |
追加型:头文件变更通过包含它的使用者追溯维护者领域。跳过被包含次数超过配置阈值的广泛使用头文件。 |
11 |
|
否 |
兜底策略:将剩余文件与 |
模板性内容过滤器
BoilerplateFilter 紧随 IgnoreStrategy,先于所有测试选择策略运行。它通过 git diff 检查各变更文件的实际差异,消费所有内容改动均不具实质性的文件:
空白与空行改动:通过
git diff -w --ignore-blank-lines检测。如果某文件没有输出,则其所有改动行均仅涉及空白或空行。SPDX 和版权头编辑:以
SPDX-License-Identifier:、SPDX-FileCopyrightText:或单词Copyright开头的行。仅含注释分隔符的行:非空白内容完全由
/*、*/、//、#或*字符组成的行,例如重新排版的块注释边框。
只有差异中 每一行 新增或删除内容均属于上述类别时才消费文件。只要有一行实质改动,例如语句、宏或声明变化,文件就原样交给后续策略。
未提供 --commits 时,例如使用 --modified-files,由于缺少提交范围而无法计算差异,过滤器不执行操作,所有文件留在待处理池中。
消费与追加行为
每个策略子类都带有类属性 consumes: bool。
- 当
consumes = True时: 下一策略运行前,从
remaining池移除 handled 集合中的文件。适用于策略对其文件类型具有 最终决定权 的情况,即后续策略再处理会产生冗余或错误结果。- 当
consumes = False(默认)时: 无论策略返回什么,文件都留在池中,所有后续策略仍接收同一文件列表。适用于提供 额外 测试覆盖、但不要求独占处理权的追加型策略。
备注
remaining 池为空时,Orchestrator 完全跳过策略。非消费型策略不会减少文件,因此“剩余”池只会因消费型策略缩小。一旦池为空,编排器就停止调用策略。
全量运行和回退条件
满足以下任一条件时,编排器在 .testplan 中设置 TWISTER_FULL=True:
存在未解析文件。 所有策略运行后,至少一个变更文件未被任何策略 处理。脚本会回退到全量运行,避免静默漏掉未知路径的覆盖而放过回归。
显式全量运行信号。 策略返回
full_run=True的TwisterCall。编排器遇到它会立即设置TWISTER_FULL=True,清空剩余池,停止执行后续调用。策略可借此放弃定向选择,例如整个代码树使用的核心子系统头文件发生变更时。
TWISTER_FULL=True 时,CI 脚本应丢弃 testplan.json,不带 --load-tests 运行 twister。
节点数量计算
.testplan 中的 TWISTER_NODES 按以下方式计算:
0:未选中测试。1:选中测试数小于--tests-per-builder,一个构建节点即可容纳。ceil(total / tests_per_builder):其他情况。向上取整确保除不尽时不会超过单个构建节点容量。
--tests-per-builder 默认值为 900。
添加新策略
继承
SelectionStrategy,实现两个抽象成员:class MyStrategy(SelectionStrategy): consumes: bool = False # or True if authoritative @property def name(self): return "MyStrategy" def analyze(self, changed_files): # inspect changed_files calls = [...] # list of TwisterCall handled = {...} # subset of changed_files this strategy owns return calls, handled
决定消费还是追加。 判断后续策略看到同一文件会增加有用覆盖,还是产生冗余/错误结果。若为后者,设
consumes = True。在
build_strategies()中 插入正确位置,一般遵循:消费型策略位于所有追加型策略之前。
更具体的策略位于较通用的策略之前。
不改变文件池的追加型策略可按成本排序,低成本优先。
保护可选依赖:在
analyze内延迟导入,缺少依赖时平稳返回[], set()。使用# noqa: PLC0415注释抑制延迟导入警告。在
scripts/tests/ci/test_test_plan_v2.py中 编写单元测试,覆盖:正常路径:正确识别文件并选择处理方式。
无操作路径:无关文件不产生调用或已处理集合。
边界情况:磁盘文件缺失、YAML 格式错误、空输入。
共享流水线上下文
PipelineContext 是通过 build_strategies() 传给所有策略的数据类,目前包含:
complexity_score:补丁集总评分,由ComplexityStrategy写入、RiskClassifierStrategy读取。file_metrics:每个文件对应的ComplexityMetrics映射,用于详细日志及后续决策。
新增跨策略状态应作为带类型的字段加入 PipelineContext,而非保存为策略级实例变量。
验证选择行为的测试策略
测试套件位于 scripts/tests/ci/test_test_plan_v2.py,使用 pytest 运行:
pytest scripts/tests/ci/test_test_plan_v2.py -v
每个策略或共享组件对应一个测试类。每个新策略应包含以下测试类别:
- 单元测试(无需 Zephyr 代码树)
使用
tmp_pathfixture 创建覆盖各代码路径所需的最小文件系统结构,例如board.yml、snippet.yml、testcase.yaml。测试应快速且与外部环境隔离。- 辅助方法测试
独立测试路径遍历、YAML 解析和正则提取等内部方法,传入人工构造内容,而不依赖仓库中的真实文件。
- 编排器集成测试
使用模拟策略及模拟的
TwisterExecutor,无需调用 twister 即可测试消费/追加逻辑、全量运行信号和.testplan输出。- 真实代码树集成测试 (可选,
@pytest.mark.integration) 可以添加此类测试并用
@pytest.mark.integration标记,使其不参与默认运行。它们仅在完整 Zephyr 检出目录中有意义。
新策略的最小测试清单
输入列表不含相关文件时,策略不返回调用或已处理文件。
对已知有效输入,策略正确识别并返回预期的
TwisterCall。文件系统缺少所需文件时,策略不会崩溃。
消费型策略的已处理集合必须恰好等于匹配文件。
追加型策略的已处理集合应为空,或仅包含其具有最终决定权的文件。
命令行参考
usage: test_plan_v2.py [-c A..B] [-m FILE] [-f PATH]
[-o FILE] [-p PLATFORM] [--maintainers-file FILE]
[-T DIR] [--quarantine-list FILE]
[--tests-per-builder N] [--disable-strategy NAME]
[--detailed-test-id]
-c A..B Git commit range (e.g. ``main..HEAD``). Changed files
are derived from ``git diff --name-only A..B``.
-m FILE JSON file containing a list of changed file paths.
-f PATH Treat PATH as a changed file (repeatable).
-o FILE Output JSON file (default: ``testplan.json``).
-p PLATFORM Restrict all selections to this platform (repeatable).
--maintainers-file Path to ``MAINTAINERS.yml``.
-T DIR Extra testsuite root forwarded to every twister call.
--quarantine-list Quarantine YAML forwarded to twister.
--tests-per-builder Tests per CI builder node (default: 900).
--disable-strategy Skip a strategy by name (repeatable).
--detailed-test-id Pass ``--detailed-test-id`` to twister.