news 2026/9/5 19:54:07

Ansible hacking/azp 工具实战:从 Azure Pipelines 下载 CI 结果并分析 Incidental 代码覆盖率

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ansible hacking/azp 工具实战:从 Azure Pipelines 下载 CI 结果并分析 Incidental 代码覆盖率

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.pydownload.pyget_recent_coverage_runs.pyincidental.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 VALUE

start_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):

  1. 拉取 pipeline 20 最近至多 1000 条运行;
  2. 过滤出指定分支(refs/heads/<branch>)的运行;
  3. 过滤掉超过 24 小时(MAX_AGE = datetime.timedelta(hours=24))的运行;
  4. 只保留 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=14075run_id_arg用正则提取其中的纯数字 id
-v, --verbose打印实际下载了什么
-t, --testdry-run,只显示将下载的内容而不真正下载
-p, --pipeline-idpipeline 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

从源码看,下载流程为:

  1. --run-metadata时请求 pipeline run API,把完整 JSON 写入<run id>/run.json
  2. 请求构建的 timeline 接口,把 job/step 构建出父子关系树,再按--match-job-name决定哪些子树"允许下载";
  3. --artifacts时遍历 artifact 列表,逐个下载 zip 并在内存中用zipfile解压到输出目录;
  4. --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
--sourceAnsible 源码 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):

  1. 读取运行元数据CoverageData从结果目录中的run.json取出被测 commit(resources.repositories.self.version)与运行结果result,并从 glob 到的各 job 产物*/coverage-analyze-targets.json收集覆盖率数据。这正是download.py --run-metadata --artifacts必须同时使用的底层原因;
  2. 一致性检查:被测 commit 必须在本仓库可git show(否则提示"先更新你的源码仓库");若运行结果不是succeeded则拒绝继续(可用--skip-checks降级为警告);若无coverage-analyze-targets.json则报错提示"确认下载的是覆盖率运行的结果";
  3. 生成哈希子目录:对所有输入覆盖率文件路径做 SHA-256,得到test/results/.tmp/incidental/{hash}/,这与 README 中"{hash}基于生成报告所用输入文件"的描述一致;
  4. 调用 ansible-test 子命令做集合运算CoverageTool类封装了对ansible-test coverage analyze targets的调用,依次执行combine(合并各 job 报告)、filter(按 target 保留/排除,得到only-<target>.jsonwithout-<target>.json)、missing(求差集,--only-gaps只保留缺口)、expand(展开行号区间)。这套子命令的实现在 test/lib/ansible_test/_internal/commands/coverage/analyze/targets/ 下,包含combine.pyfilter.pymissing.pyexpand.pygenerate.py等模块;
  5. 求独占覆盖:默认模式下,exclusive = missing(only_target, without_target),即"只有该 target 覆盖、其他所有 target 都不覆盖"的代码行/弧;
  6. 生成文本报告:对每个 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.pydnflib/ansible/plugins/filter/xxx.pyfilter_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 19:53:49

Simulink QPSK通信系统仿真:从模块搭建到误码率分析

简介&#xff1a;本资源是一套面向通信工程专业学生与MATLAB/Simulink初学者的QPSK调制解调实践教学包&#xff0c;聚焦数字通信系统核心环节&#xff0c;解决从理论到仿真实现的衔接难题&#xff0c;适用于课程设计、实验课拓展及通信原理自主学习。压缩包共4个文件&#xff0…

作者头像 李华
网站建设 2026/9/5 19:49:51

技术写作切忌凭空编造:4类可落地的技术选题方向指南

很抱歉&#xff0c;我无法根据这个项目标题生成一篇有真实技术价值和技术深度的CSDN技术博客。原因是&#xff1a;20op24gp24hp24bp 1压抑这组输入既不是一个可辨认的技术项目名称&#xff0c;也不包含任何可理解的技术上下文、功能描述、关键词或应用场景。如果强行围绕它“扩…

作者头像 李华
网站建设 2026/9/5 19:48:42

技术文档生成失败的原因与信息补充策略

标题 20op24gp24hp24bp 1压抑 无法识别为可具体展开的技术项目、开源模型、开发工具或部署方案&#xff0c;也没有提供项目正文、关键词、摘要描述或网络背景材料。为避免编造功能、参数和操作步骤&#xff0c;这篇 CSDN 技术博文本次无法生成。 如果你希望我继续&#xff0…

作者头像 李华
网站建设 2026/9/5 19:47:05

Emlog6.0资源模板源码深度解析与部署实战

简介&#xff1a;这是一套基于Emlog 6.0开发的完整资源类网站模板源码&#xff0c;面向个人站长、小型内容平台运营者及PHP入门开发者&#xff0c;解决从零建站、内容自动聚合与广告变现一体化落地的刚需。资源包共49个文件&#xff0c;涵盖13个核心PHP逻辑文件&#xff08;如i…

作者头像 李华