Ansible hacking/azp 工具实战:从 Azure Pipelines 下载 CI 结果并分析 Incidental 代码覆盖率
【免费下载链接】ansibleAnsible is a radically simple IT automation platform that makes your applications and systems easier to deploy and maintain. Automate everything from code deployment to network configuration to cloud management, in a language that approaches plain English, using SSH, with no agents to install on remote systems. https://docs.ansible.com.项目地址: https://gitcode.com/GitHub_Trending/ans/ansible
本文以 Ansible 仓库 hacking/azp 目录下的 CI 辅助脚本为主体,讲清四个工具(run.py、download.py、get_recent_coverage_runs.py、incidental.py)各自解决什么问题,并完整走通「获取最近覆盖率运行 → 下载 CI 结果 → 分析 incidental 覆盖率报告 → 补齐测试缺口」这条工作流。读完本文,你将能够独立下载 Azure Pipelines 的覆盖率数据,定位那些"顺带覆盖"了核心代码的测试,并理解覆盖率报告中山丘弧线(arc)标记的含义。
目录概览:四个脚本各管一件事
hacking/azp/README.md 声明该目录包含以下脚本,每个脚本的职责与源码中的实现一一对应:
| 脚本 | 职责 | 关键实现入口 |
|---|---|---|
download.py | 从 CI 下载运行结果 | 解析 run id,下载 artifacts/日志/元数据 |
get_recent_coverage_runs.py | 获取最近覆盖率测试运行的 CI URL 与状态 | 轮询 pipeline 20 的运行列表 |
incidental.py | 基于 CI 数据生成 incidental 覆盖率报告 | 组合ansible-test coverage analyze targets子命令 |
run.py | 在 CI 上发起新的运行 | 调用 Azure Pipelines REST API 创建 run |
run.py:手动触发一次 CI 运行
run.py 通过 Azure Pipelines REST API 发起新运行,源码中的参数定义(hacking/azp/run.py#L58-L77)如下:
-p, --pipeline-id:要触发的 pipeline,默认值为20(Ansible 官方 pipeline);--ref:要运行的 git 引用(分支/标签);--env KEY VALUE:传递给运行的环境变量,可重复出现(nargs=2, action='append')。
使用前必须设置AZP_TOKEN环境变量,否则脚本直接报错退出(见 hacking/azp/run.py#L50-L53):
export AZP_TOKEN="<your token>" hacking/azp/run.py --ref devel --env KEY VALUEstart_run函数会向https://dev.azure.com/ansible/ansible/_apis/pipelines/<id>/runs发送 POST 请求,并把返回的运行信息以 JSON 打印到终端。源码注释中还留有一条 TODO,提示 Dev 团队缺少 AZP token、该路径未被充分测试,使用时如遇到认证问题属于已知情况。
第一步:获取最近的覆盖率运行
覆盖率工作流的第一步是找到最近一次带代码覆盖率的 CI 运行。仓库每天自动在 Azure Pipelines 上执行一次全量测试并开启代码覆盖率,get_recent_coverage_runs.py 负责列出这些运行:
hacking/azp/get_recent_coverage_runs.py <optional branch name>分支名默认为devel(对应源码中的BRANCH = 'devel',若sys.argv有值则覆盖,见 hacking/azp/get_recent_coverage_runs.py#L33-L38)。
从源码可以确认它的筛选逻辑(hacking/azp/get_recent_coverage_runs.py#L41-L77):
- 拉取 pipeline 20 最近至多 1000 条运行;
- 过滤出指定分支(
refs/heads/<branch>)的运行; - 过滤掉超过 24 小时(
MAX_AGE = datetime.timedelta(hours=24))的运行; - 只保留 artifact 名称以
Coverage开头的运行——即真正开启了覆盖率采集的运行。
输出格式为每行一条,带彩色 PASS/FAIL 标记与 Azure Pipelines 构建 URL,进行中的运行单独列在末尾。源码中还有一段兼容性处理:遇到使用容器资源的老运行会因 Azure API 序列化错误返回 500,脚本会直接break停止继续翻页,避免无谓请求失败。
拿到 run id(URL 中的buildId)后,即可进入下一步下载结果。
第二步:下载 CI 结果到本地
hacking/azp/download.py <run id> --artifacts --run-metadata -v是 README 给出的标准用法。其完整参数(解析逻辑见 hacking/azp/download.py#L55-L119):
| 参数 | 说明 |
|---|---|
RUN(位置参数) | AZP 运行 id,或直接的构建 URL,如https://dev.azure.com/ansible/ansible/_build/results?buildId=14075。run_id_arg用正则提取其中的纯数字 id |
-v, --verbose | 打印实际下载了什么 |
-t, --test | dry-run,只显示将下载的内容而不真正下载 |
-p, --pipeline-id | pipeline id,默认20,仅影响 run 元数据请求的 URL |
--artifacts | 下载 artifacts(zip 包,解压到运行 id 命名的目录) |
--console-logs | 下载各 job 的控制台日志 |
--run-metadata | 下载运行元数据并保存为run.json |
--all | 等价于同时打开上面三项 |
--match-artifact-name | 只下载文件名匹配该正则的 artifact |
--match-job-name | 只处理 job 名(<父job名> <子job名>)匹配该正则的内容 |
结果统一落到以 run id 命名的目录(output_dir = '<run id>'),例如:
# 结果下载到当前目录下的 ansible/ansible 路径,14075 替换为你要下载的运行号 hacking/azp/download.py 14075 --artifacts --run-metadata -v从源码看,下载流程为:
--run-metadata时请求 pipeline run API,把完整 JSON 写入<run id>/run.json;- 请求构建的 timeline 接口,把 job/step 构建出父子关系树,再按
--match-job-name决定哪些子树"允许下载"; --artifacts时遍历 artifact 列表,逐个下载 zip 并在内存中用zipfile解压到输出目录;--console-logs时沿 timeline 的父子链拼接出父job 子job step风格的日志文件名(把路径分隔符替换为_)逐个保存。
注意两点前提:run.json必须存在,后续incidental.py依赖它读取被测 commit sha 与运行结果(见下文);至少选择--artifacts/--run-metadata/--console-logs之一,否则脚本以parser.error报错退出。
什么是 Incidental Code Coverage
incidental.py 是整套工具中技术含量最高的部分,理解它的前提是先理解 README 中定义的 incidental 概念:
当一个测试在测试 A 代码的同时,非预期地顺带覆盖了一部分 B 代码,就产生了 incidental 测试与代码覆盖率。
原文档给的例子:dnf集成测试本意是测dnf模块,但过程中同时使用并"无意"测试了file模块。
这个概念与 Ansible 模块化历史强相关。README 说明:在把模块和插件迁移进 collections 的过程中,发现了一些独占性 incidental 覆盖——即即将随迁移出仓库的测试,所覆盖的代码在迁移后没有任何剩余测试再覆盖。为避免覆盖丢失,这些集成测试目标被加上incidental_前缀保留在仓库中,其依赖的插件也被保留在 test/support 目录下。当前仓库中可以看到这样的存量目标,例如 test/integration/targets/incidental_win_reboot。这些 incidental 测试的长期目标是被有意的(intentional)测试替代:随着有意测试的增加,incidental 测试提供的独占覆盖会下降,降到零后即可删除,而不损失任何测试覆盖。
减少 Incidental 覆盖的完整工作流
README 给出了四步流程,下面逐步展开并补充源码依据。
步骤 1:获取最近的覆盖率运行 URL
即上文get_recent_coverage_runs.py的用法。
步骤 2:下载覆盖率数据
即上文download.py的用法。
步骤 3:分析每个测试覆盖的代码
# 确认 ansible-test 在 $PATH 中 source hacking/env-setup # 用实际下载结果的目录名替换 14075/ hacking/azp/incidental.py 14075/incidental.py的参数全集(hacking/azp/incidental.py#L55-L107):
| 参数 | 说明 |
|---|---|
result(位置参数) | 从 Azure Pipelines 下载的结果目录(必须是目录,否则报错) |
--output | 报告输出目录,默认test/results/.tmp/incidental |
--source | Ansible 源码 git 仓库路径,默认取脚本所在仓库根 |
--skip-checks | 跳过一致性检查,仅供调试 |
--ignore-cache | 忽略已缓存的中间文件 |
-v, --verbose | 提高输出详细度 |
--result-sha | 覆盖从run.json中读取的结果 sha |
--targets | 待分析 target 的正则,默认^incidental_ |
--plugin-path | 改为对指定插件路径报告"其自身测试缺失的" incidental 覆盖;与--targets互斥 |
从源码看其内部执行链路(incidental_report函数,hacking/azp/incidental.py#L131-L257):
- 读取运行元数据:
CoverageData从结果目录中的run.json取出被测 commit(resources.repositories.self.version)与运行结果result,并从 glob 到的各 job 产物*/coverage-analyze-targets.json收集覆盖率数据。这正是download.py --run-metadata --artifacts必须同时使用的底层原因; - 一致性检查:被测 commit 必须在本仓库可
git show(否则提示"先更新你的源码仓库");若运行结果不是succeeded则拒绝继续(可用--skip-checks降级为警告);若无coverage-analyze-targets.json则报错提示"确认下载的是覆盖率运行的结果"; - 生成哈希子目录:对所有输入覆盖率文件路径做 SHA-256,得到
test/results/.tmp/incidental/{hash}/,这与 README 中"{hash}基于生成报告所用输入文件"的描述一致; - 调用 ansible-test 子命令做集合运算:
CoverageTool类封装了对ansible-test coverage analyze targets的调用,依次执行combine(合并各 job 报告)、filter(按 target 保留/排除,得到only-<target>.json与without-<target>.json)、missing(求差集,--only-gaps只保留缺口)、expand(展开行号区间)。这套子命令的实现在 test/lib/ansible_test/_internal/commands/coverage/analyze/targets/ 下,包含combine.py、filter.py、missing.py、expand.py、generate.py等模块; - 求独占覆盖:默认模式下,
exclusive = missing(only_target, without_target),即"只有该 target 覆盖、其他所有 target 都不覆盖"的代码行/弧; - 生成文本报告:对每个 target 写出
reports/<target>.txt,并在终端打印汇总行<target>: N arcs, M lines, K files - <report path>。所有中间产物通过cached()辅助函数做文件级缓存,重复运行可跳过已生成的文件。
另外注意源码中两处硬编码的排除项:test/support/下的测试支持插件与lib/ansible/module_utils/six/不参与分析(后者被注释说明"会报告虚假的注释行覆盖")。
步骤 4:编写有意测试补齐缺口
根据test/results/.tmp/incidental/{hash}/reports/下的报告,为新覆盖的代码创建新测试或扩展现有测试。随着该过程循环进行,独占覆盖会逐步下降;当某个 incidental 测试不再提供独占覆盖时即可删除。README 特别警告:一次只能删一个 incidental 测试,因为删掉一个后,原本由它分担覆盖的代码可能使另一个测试获得新的独占覆盖。
针对插件的覆盖率缺口分析
incidental 分析不限于incidental_前缀的测试:某个 filter 插件自身测试覆盖不全时,缺口可能由无关测试顺带填补,incidental.py同样能定位这些缺口。用法是在步骤 3 中加--plugin-path {path_to_plugin},可对任意多个插件重复执行。
一次分析所有 filter 插件的示例(README 原文):
find lib/ansible/plugins/filter -name '*.py' -not -name __init__.py -exec hacking/azp/incidental.py 14075/ --plugin-path '{}' ';'指定--plugin-path后,脚本行为切换为"missing"模式(missing = True):把插件路径映射到其集成测试 target 名(get_target_name_from_plugin_path,如lib/ansible/modules/dnf.py→dnf,lib/ansible/plugins/filter/xxx.py→filter_xxx,见 hacking/azp/incidental.py#L259-L280),然后计算missing(without_target, only_target),即"该插件自身测试未覆盖、但其他测试覆盖了"的缺口。即使该插件没有对应测试 target,也会生成一份报告说明缺失的覆盖。README 同时提醒:报告不标注这些 incidental 覆盖来自哪个测试。
如何阅读覆盖率报告
每行被覆盖的代码都会出现在报告中:左列是源码行号;若是 Python 代码,行尾注释还会标注涉及的覆盖弧(arc)。README 给出的真实报告样例:
Target: incidental_win_psexec GitHub: https://github.com/ansible/ansible/blob/6994ef0b554a816f02e0771cb14341a421f7cead/test/integration/targets/incidental_win_psexec Source: lib/ansible/executor/task_executor.py (2 arcs, 3/1141 lines): GitHub: https://github.com/ansible/ansible/blob/6994ef0b554a816f02e0771cb14341a421f7cead/lib/ansible/executor/task_executor.py 705 if 'rc' in result and result['rc'] not in [0, "0"]: ### (here) -> 706 706 result['failed'] = True ### 705 -> (here) ### (here) -> 711 711 if self._task.until: ### 706 -> (here)报告头部给出产生该覆盖的 target 名,以及指向目标目录与源码文件的链接——链接中的 commit 与覆盖率数据匹配,确保看到的代码与 CI 实际测试的代码一致。
弧标记的语义(README 原文解释,与 hacking/azp/incidental.py#L409-L428 的报告生成逻辑一致):
### (here) -> 706(写在第 705 行)表示执行流从第 705 行进到第 706 行,可以有多个出边行号;### 706 -> (here)(写在第 711 行)表示执行流从第 706 行进到第 711 行,可以有多个入边行号;(here)只是"当前这一行"的占位引用。
弧(arc)信息仅对 Python 代码可用;PowerShell 代码只报告被覆盖的行号。终端汇总行中也会区分统计口径:exclusive 模式显示N arcs, M lines,纯行号模式(如 PowerShell)只显示M lines(报告头部相应地省略 arcs 计数,见 hacking/azp/incidental.py#L391-L405)。
适用前提小结
- 四个脚本全部面向 Ansible 官方 Azure Pipelines(pipeline 20、
dev.azure.com/ansible/ansible项目),run.py还需要有效的AZP_TOKEN; incidental.py依赖ansible-test在$PATH中(source hacking/env-setup)、依赖一个完整的源码 git 仓库(需要能git show被测 commit)、以及包含run.json与各 jobcoverage-analyze-targets.json的下载结果目录;- 报告中的 commit 链接以 CI 实际测试的 commit 为准,与本地未推送的代码可能不一致,因此分析前应先同步源码;
- 所有中间结果默认缓存在
test/results/.tmp/incidental/{hash}/下,可用--ignore-cache强制重新生成。
【免费下载链接】ansibleAnsible is a radically simple IT automation platform that makes your applications and systems easier to deploy and maintain. Automate everything from code deployment to network configuration to cloud management, in a language that approaches plain English, using SSH, with no agents to install on remote systems. https://docs.ansible.com.项目地址: https://gitcode.com/GitHub_Trending/ans/ansible
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考