JUnit XML 测试报告分析
1. 触发条件(Trigger)
本 skill 仅在以下两个条件同时满足时调用:
- 用户明确提到 JUnit XML(指名 JUnit 格式的 XML 测试报告;泛泛的"测试报告"不算);
- 用户明确要求计算通过率(pass rate / 通过率;仅"看一下报告"不算)。
任一条件不满足即不调用,并向用户说明本 skill 的适用场景:解析 JUnit XML 测试报告并计算通过率。
正例(应触发)
- "帮我分析这份 JUnit XML 报告,算一下通过率"
- "Parse the surefire JUnit XML report and tell me the pass rate"
- "这是 CI 产出的 JUnit XML(TEST-xxx.xml),帮我统计一下通过率"
反例(不应触发)
- "看一下这份测试报告" —— 未指名 JUnit XML,也未要求通过率
- "分析这份 JUnit XML,告诉我哪些用例失败了" —— 指名了 JUnit XML,但未要求通过率
- "统计一下这次测试的通过率" —— 要求了通过率,但未指名 JUnit XML 格式
2. 输入契约
| 项 | 约定 |
|---|---|
| 参数名 | xml_path(必填,字符串) |
| 允许形式 | 本地文件路径(以 .xml 结尾,不区分大小写);或 http:// / https:// URL |
xml_path必须为非空字符串;不满足直接失败,不进入解析。- 仅当输入以
http://或https://开头时才启用网络加载分支;file://、ftp://、data:等其他协议一律拒绝。 - 路径处理一律使用 pathlib:收到相对路径时先转为绝对路径再处理,并在 message 中回显解析后的绝对路径(便于排查 cwd 问题);禁止字符串拼接路径。
- 唯一输入源就是
xml_path:不接受目录、不扫描通配符、不接受内联 XML 文本。 - 本地文件按二进制读取后自行解码,兼容 UTF-8 与 UTF-8-BOM;解码失败按失败返回。
3. 调用方式
触发条件满足后执行(脚本可从任意 cwd 调用,建议使用绝对路径):
python "<skill-root>/scripts/parse_junit_xml.py" --xml-path "<xml_path>"
- stdout 只允许输出一个 JSON 对象:单行、
ensure_ascii=False、UTF-8。所有日志、warning、traceback 一律走 stderr,stdout 不允许出现任何其他内容。 - exit code 语义固定:0 = status success;非 0 = status failed,与 JSON 的
status字段保持一致。 - 脚本内部以自身文件位置定位依赖模块,不依赖调用时的 cwd。
- Agent 行为约束:将脚本 stdout 的 JSON 原样作为本 skill 的输出返回给用户,不得增删改字段、不得二次加工结构。 如需额外解释,可在 JSON 之外附加说明文字,但 JSON 本身保持原样。
4. 输出契约(JSON,字段固定)
顶层字段为 source / summary / suites / status / message 五个,固定不变,不允许增删(以下示例值仅供参考):
{
"source": "<原始输入 xml_path,原样回显>",
"summary": {
"total": 0, "passed": 0, "failed": 0,
"skipped": 0, "errors": 0, "pass_rate": 0.0
},
"suites": [
{
"name": "<suite 名称>",
"total": 0, "passed": 0, "failed": 0,
"skipped": 0, "errors": 0, "pass_rate": 0.0
}
],
"status": "success | failed",
"message": "<成功/失败原因的可读中文描述>"
}
统计口径(写死,脚本严格一致):
- 计数优先级:优先使用
<testsuite>属性上的tests/failures/errors/skipped;属性缺失或非数字时,回退为统计子元素数量;属性值与子元素统计不一致时,以子元素统计为准,并在 message 中追加一条 warning。 total为该 suite 的 testcase 总数;failed与errors来自两个不同来源(<failure>/<error>),分字段输出;passed = total − failed − skipped − errors,结果不为负。- 参数化测试、重试产生的同名 testcase 按实际条数计数,不按名字去重。
summary为全部 suite 之和;suites中每个最内层含 testcase 的<testsuite>一项,按出现顺序排列;同名 suite 不合并;嵌套<testsuite>只统计最内层含 testcase 的 suite,避免父子重复计数。pass_rate = round(passed / total, 4),为 0~1 的小数,不使用百分比字符串;total=0 时 pass_rate=0.0 且 status 仍为 success,message 提示"无测试用例",不得抛 ZeroDivisionError。- 类型约束:所有计数输出为 int,
pass_rate输出为 float,禁止输出字符串数字。 - 成功 message 至少包含:suite 数、用例总数、通过率、解析后的绝对路径;发生口径回退或不一致时追加 warning。
- 失败时:
status="failed",summary各计数为 0,suites为空数组[],message写明原因;source仍回显原始输入。
5. 边界与异常规则
统一要求:所有失败都必须以上述契约 JSON 返回,不允许崩溃或静默吞错。失败 message 必须以失败类别开头(参数 / 路径 / 网络 / 解析 / 口径 / 资源)并包含关键上下文(路径、文件大小、耗时、重试次数、解析器报错摘要等),让人不看源码也能定位。脚本顶层必须有统一异常兜底:任何未捕获异常都转换为契约 JSON 输出(exit code 非 0,message 含异常类型与摘要);traceback 不允许出现在 stdout;兜底 except 只允许存在于最外层,内部不得用 except: pass 或宽泛捕获后继续执行。
路径与文件
| 情形 | 行为 |
|---|---|
xml_path 为空 / 非字符串 / 非法形式 |
[参数] 失败,不进入解析 |
| 路径不存在 | [路径] 失败,指明路径;不重试 |
| 路径是目录 | [路径] 失败(与"不存在"分别报错,不合并) |
| 无读权限 | [路径] 失败 |
| 文件为空(0 字节) | [路径] 失败 |
| 文件大小超过 200MB(读取前用 getsize 预检) | [资源] 失败,报告实际大小与上限 |
| 内容不是 UTF-8 / UTF-8-BOM,解码失败 | [解析] 失败 |
网络(仅 http/https 输入启用)
| 情形 | 行为 |
|---|---|
| 非 http(s) 协议(file://、ftp://、data: 等) | [参数] 失败 |
| 重定向到非 http(s),或解析到内网私有地址(127.0.0.0/8、10/8、172.16/12、192.168/16、169.254/16、::1) | [网络] 立即失败,说明被拒绝的目标(初始 URL 与每一跳重定向同等校验) |
| 重定向次数超过 3 | [网络] 失败 |
| 连接+读取总超时 | 单次尝试上限 10 秒(覆盖该次尝试的全部重定向跳转);最多尝试 3 次(固定间隔 1 秒);超时/连接类错误参与重试,3 次均失败 → [网络] 失败,message 记录尝试次数与最后一次错误原因。重试序列总时长(3×10s + 2×1s = 32s)必须落在 wall-clock 上限内,重试耗尽的消息优先于超时兜底 |
| 响应体超过 100MB | [资源] 立即失败 |
| HTTP 状态码非 200 | [网络] 失败,message 含状态码 |
XML 解析
| 情形 | 行为 |
|---|---|
| 文档含 DOCTYPE 或 ENTITY 声明 | [解析] 解析前直接拒绝(防 XXE/实体膨胀),不进入解析器 |
| XML 格式非法 / 解析失败 | [解析] 失败,message 含解析器报错摘要(含行/列) |
根节点(剥离命名空间后)既不是 testsuites 也不是 testsuite |
[解析] 失败,message 说明期望的根节点名,不得当成功处理 |
空文档 / 不含任何 <testsuite> |
[解析] 失败 |
有 <testsuite> 但用例总数为 0 |
success,total=0,pass_rate=0.0,message 提示无测试用例;不得抛 ZeroDivisionError |
资源与运行
| 情形 | 行为 |
|---|---|
| 脚本整体 wall-clock 超过 40 秒 | [资源] 失败,message 明确说明是超时而非解析错误(上限须高于重试序列总时长 32s,保证重试路径优先) |
| 其他任何未捕获异常 | 顶层兜底转为契约 JSON(类别 + 异常类型与摘要),exit code 非 0 |
其他硬性要求:所有文件句柄用 with 管理;URL 下载流式读取且不落盘(无临时文件、无缓存目录残留);脚本可重复调用、无副作用——不写业务数据到磁盘,不在 skill 目录生成缓存。
6. 功能边界(禁项)
- 本 skill 只做"本地 XML 文件解析 + 通过率计算"(URL 加载仅作为
xml_path的可选输入形式):不包含任何数据库访问代码——无 DB 连接、无 SQL 语句、无相关注释占位。 - 不读取目录、不解压归档、不调用外部程序、不做本契约之外的任何分析。
- 仅使用 Python 3.8+ 标准库(xml.etree、pathlib、urllib、json、argparse、sys、os、time 等);不引入第三方库。若未来确需第三方库,必须先在本文件声明,且脚本启动时探测缺失并以契约 JSON 报错,而不是 ImportError 崩溃。
7. 实现位置
scripts/parse_junit_xml.py—— CLI 入口:参数接入、统一异常兜底、wall-clock 上限、JSON 输出与 exit codescripts/junit_common.py—— 契约常量(各项上限)、失败类别、统一结果构造scripts/junit_loader.py—— 参数校验、本地/URL 加载(超时、重试、重定向与私网地址校验、响应体大小上限)scripts/junit_parser.py—— XML 解析(命名空间剥离、DOCTYPE 拒绝、嵌套 suite、计数口径与 warning)- 本文件(SKILL.md)只描述规范与契约,不包含可执行代码或算法实现。