"""error 意外成功甄别结果 → 按接口逐个提单(GitCode Alice_zhou/issue 仓)
读取 out_res/error意外成功甄别_终版.xlsx「意外成功甄别明细」:
- 甄别结论 ∈ {是bug候选, 需要人工确认} 的场景为有效意外成功问题点
(FORMAT 类"不是bug"为框架规则无效用例,不提单)
- 按 接口名 分组:一个接口提一个 issue(https://gitcode.com/Alice_zhou/issue)
- 提单前上传该接口 error CPP 到 Alice_zhou/issue 仓 artifacts/{日期}/{api}_error.cpp,
issue 正文含 CPP 附件下载链接
- issue 正文含:问题描述 + 超预期的问题点明细表(场景ID/类别/异常场景描述/执行次数/
甄别结论/判定依据)+ 四步复现步骤(执行步骤)+ 附件链接
- 提单完成后生成含 issue 列的新 xlsx(--output-xlsx)
用法:
python scripts/file_unexpected_success_issues.py # 预览(dry-run,不提单不上传)
python scripts/file_unexpected_success_issues.py --file # 正式提单
python scripts/file_unexpected_success_issues.py --file --api aclnnTan # 只提指定接口
退出码: 0=完成; 1=配置/数据错误; 2=部分或全部提单失败
"""
import argparse
import datetime
import json
import os
import re
import sys
import urllib.error
import urllib.request
import openpyxl
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from upload_to_issue_repo import IssueRepoClient
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
DEFAULT_CONFIG = "config/issue_tracker.json"
DEFAULT_XLSX = "out_res/error意外成功甄别_终版.xlsx"
VALID_CONCLUSIONS = ("是bug候选", "需要人工确认")
DETAIL_SHEET = "意外成功甄别明细"
MD_COL = "md资料链接"
OPS_REPOS = ("ops-math", "ops-nn", "ops-cv", "ops-transformer")
MD_OVERRIDE = {
"aclnnTan": "ops-math/math/tan/docs/aclnnTan&aclnnInplaceTan.md",
"aclnnInplaceTan": "ops-math/math/tan/docs/aclnnTan&aclnnInplaceTan.md",
}
def log(tag, msg):
print("[{}] {}".format(tag, msg))
def esc(text):
"""markdown 表格转义:竖线替换为全角。"""
return str(text if text is not None else "").replace("|", "|").replace("\n", " ")
class IssueCreator:
def __init__(self, api_base, token, owner, repo):
self.api_base = api_base.rstrip("/")
self.token = token
self.owner = owner
self.repo = repo
def _request(self, path, method="GET", data=None):
url = "{}/repos/{}/{}{}".format(self.api_base, self.owner, self.repo, path)
body = json.dumps(data).encode("utf-8") if data is not None else None
req = urllib.request.Request(
url, data=body, method=method,
headers={"PRIVATE-TOKEN": self.token, "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.status, json.loads(resp.read().decode("utf-8", errors="replace"))
except urllib.error.HTTPError as exc:
try:
return exc.code, json.loads(exc.read().decode("utf-8", errors="replace"))
except Exception:
return exc.code, {}
except Exception as exc:
return -1, {"error_message": str(exc)}
def list_issues(self, state="all", limit=100):
code, data = self._request("/issues?state={}&limit={}&page=1".format(state, limit))
return data if code == 200 and isinstance(data, list) else []
def create_issue(self, title, body):
return self._request("/issues", "POST", {"title": title, "body": body})
def load_detail_rows(xlsx_path):
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
ws = wb[DETAIL_SHEET]
header, rows = None, []
for r in ws.iter_rows(values_only=True):
if header is None:
header = [str(c).strip() if c is not None else "" for c in r]
continue
if r[0] is None or str(r[0]).strip() in ("", "接口名"):
continue
rows.append(dict(zip(header, r)))
wb.close()
return rows
def group_valid_by_api(rows):
groups = {}
for rec in rows:
concl = str(rec.get("甄别结论") or "").strip()
if concl in VALID_CONCLUSIONS:
api = str(rec.get("接口名") or "").strip()
groups.setdefault(api, []).append(rec)
for api in groups:
groups[api].sort(key=lambda x: (int(x.get("场景ID") or 0)))
return groups
def log_final_line(api):
"""从 output/{api}/fuzz_logs/{api}_error_fuzz_output.log 提取 FINAL 汇总行。"""
path = os.path.join("output", api, "fuzz_logs", "{}_error_fuzz_output.log".format(api))
if not os.path.isfile(path):
return ""
for line in open(path, encoding="utf-8", errors="replace"):
if line.startswith("FINAL:"):
return line.strip()
return ""
def build_title(api, recs):
n = len(recs)
if all(str(r.get("甄别结论")).strip() == "是bug候选" for r in recs):
return "[Bug|算子缺陷]: {} error场景 存在{}处意外成功(资料描述不支持但实际执行成功)".format(api, n)
return "[Bug|算子缺陷]: {} error场景 存在{}处意外成功(资料描述不支持但实际执行成功,待人工复核)".format(api, n)
def build_body(api, recs, cfg, cpp_url):
bug_cnt = sum(1 for r in recs if str(r.get("甄别结论")).strip() == "是bug候选")
manual_cnt = len(recs) - bug_cnt
final = log_final_line(api)
cpp_name = "{}_error.cpp".format(api)
lines = []
lines.append("#### 一、问题描述(必填)")
lines.append("- 接口名:`{}`;测试场景:`error`(异常约束违反压测);来源:error意外成功甄别_终版.xlsx(意外成功甄别明细)".format(api))
if final:
lines.append("- 压测结果:{}(每个异常场景注入资料描述不支持的约束后检查返回码)".format(final))
lines.append("- 意外成功甄别:共 **{} 处**有效意外成功——预期接口应报 161001/161002 参数错误,"
"实际执行成功 ret=0(`[FUZZ_VERIFY] match=NO, actual_code=0`);"
"其中 是bug候选 {} 处 / 需要人工确认 {} 处".format(len(recs), bug_cnt, manual_cnt))
lines.append("- 说明:FORMAT 类意外成功(如 ACL_FORMAT_NCHW)已按测试框架规则甄别排除"
"(ND 通用布局无有效 format 违反,属框架无效用例,不计入本单问题点)")
lines.append("")
lines.append("#### 二、超预期的问题点(意外成功明细)")
lines.append("| 场景ID | 类别 | 异常场景描述(注入的约束违反) | 执行次数 | 甄别结论 | 判定依据 |")
lines.append("|---|---|---|---|---|---|")
for r in recs:
lines.append("| {} | {} | {} | {} | {} | {} |".format(
esc(r.get("场景ID")), esc(r.get("类别")), esc(r.get("异常场景描述")),
esc(r.get("执行次数")), esc(r.get("甄别结论")), esc(r.get("依据"))))
lines.append("")
lines.append("上述场景均为按资料约束注入异常(不支持 dtype / 维度超限 / dtype 配对不一致)后接口实际执行成功"
"(`[FUZZ_VERIFY] expected_code=161001或161002, actual_code=0, match=NO`),"
"接口未按资料描述的约束拒绝非法输入。")
lines.append("")
lines.append("#### 三、问题复现步骤(必填)")
lines.append("1. 下载测试框架:{}".format(cfg.get("framework_url")))
lines.append("2. 环境准备(清除原有内容,将附件 cpp 放入对应目录下):")
lines.append(" ```bash")
lines.append(" rm -rf aclnnAPI_fuzz/src/op/*")
lines.append(" # 将附件 {} 放入 aclnnAPI_fuzz/src/op/ 下".format(cpp_name))
lines.append(" ```")
lines.append("3. 编译:")
lines.append(" ```bash")
lines.append(" cd aclnnAPI_fuzz/ ; make")
lines.append(" ```")
lines.append("4. 执行:")
lines.append(" ```bash")
lines.append(" cd out ; ./aclnnAPI_fuzz {}_error_fuzz".format(api))
lines.append(" ```")
lines.append(" 执行后在打屏日志中查看 `[FUZZ_ERROR_POINT]`(注入的异常点,即上表场景)与 "
"`[FUZZ_VERIFY]`(expected_code vs actual_code),上表场景均复现为 match=NO 且 actual_code=0(意外成功)。")
lines.append("")
lines.append("#### 四、附件与证据(必填)")
lines.append("- CPP 附件下载链接:{}".format(cpp_url))
lines.append("- 日志证据:`output/{}/fuzz_logs/{}_error_fuzz_output.log`(意外成功场景 [FUZZ_VERIFY] match=NO, actual_code=0)".format(api, api))
lines.append("")
lines.append("💡 存在的问题(选填)")
lines.append("该问题由 fuzz 自动化压测平台 error 场景压测 + 意外成功甄别产出(按接口提单,一接口一单);"
"如需更多上下文(完整打屏日志/plog/覆盖度报告),请访问平台 `output/{}/fuzz_logs/` 目录。".format(api))
return "\n".join(lines)
def cpp_blob_url(owner, repo, branch, date_str, api):
return "https://gitcode.com/{}/{}/blob/{}/artifacts/{}/{}_error.cpp".format(
owner, repo, branch, date_str, api)
def collect_docs_mds():
"""单次遍历 ops-* 仓 docs/ 目录收集全部 md(接口 md 定位数据源)。"""
mds = []
for repo in OPS_REPOS:
if not os.path.isdir(repo):
continue
for root, _dirs, files in os.walk(repo):
if not root.replace(os.sep, "/").endswith("/docs"):
continue
for f in files:
if f.endswith(".md"):
mds.append(os.path.join(root, f).replace(os.sep, "/"))
return sorted(set(mds))
def resolve_md_repo_path(api, all_mds):
"""定位接口的 md 文档仓内路径。
优先级:① output/{api}/test-design 中记录的 md 源(约束推导实际使用的资料)
② MD_OVERRIDE 显式覆盖(甄别依据明确的接口)
③ 仓内搜索:md 文件名按 & 拆分后精确含接口名,非 experimental 优先
"""
td_dir = os.path.join("output", api, "test-design")
if os.path.isdir(td_dir):
pat = re.compile(r"((?:ops-(?:math|nn|cv|transformer))/[^\s\"'\\\]>]+\.md)")
for fn in sorted(os.listdir(td_dir)):
if not fn.endswith((".yaml", ".json", ".txt")):
continue
try:
text = open(os.path.join(td_dir, fn), encoding="utf-8").read()
except Exception:
continue
m = pat.search(text)
if m and os.path.isfile(m.group(1)):
return m.group(1)
if api in MD_OVERRIDE and os.path.isfile(MD_OVERRIDE[api]):
return MD_OVERRIDE[api]
cands = [m for m in all_mds if api in m.rsplit("/", 1)[-1][:-3].split("&")]
if not cands:
return ""
non_exp = [m for m in cands if "/experimental/" not in m]
pool = non_exp or cands
return sorted(pool, key=lambda p: (p.count("/"), len(p), p))[0]
def md_doc_url(repo_path):
"""md 仓内路径(含仓目录前缀,如 ops-math/math/.../xx.md)→ GitCode blob 链接(master 分支)。"""
if not repo_path:
return ""
repo, _, path_in_repo = repo_path.partition("/")
return "https://gitcode.com/cann/{}/blob/master/{}".format(repo, path_in_repo)
def main():
parser = argparse.ArgumentParser(description="error 意外成功甄别结果按接口提单(Alice_zhou/issue)")
parser.add_argument("--xlsx", default=DEFAULT_XLSX, help="甄别结果 xlsx")
parser.add_argument("--output-xlsx", default="", help="提单回填后的新 xlsx(默认 {原文件名}_issue.xlsx)")
parser.add_argument("--config", default=DEFAULT_CONFIG)
parser.add_argument("--api", action="append", default=None, help="只处理指定接口(可多次)")
parser.add_argument("--file", action="store_true", help="正式提单(默认 dry-run 预览)")
args = parser.parse_args()
if not os.path.isfile(args.xlsx):
log("ERROR", "甄别 xlsx 不存在: {}".format(args.xlsx))
return 1
if not os.path.isfile(args.config):
log("ERROR", "配置不存在: {}(模板 config/issue_tracker.example.json)".format(args.config))
return 1
cfg = json.load(open(args.config, encoding="utf-8"))
token = str(cfg.get("access_token") or "")
if token.startswith("${") and token.endswith("}"):
token = os.environ.get(token[2:-1], "")
if not token:
log("ERROR", "access_token 未配置")
return 1
owner = cfg.get("issue_repo_owner", "Alice_zhou")
repo = cfg.get("issue_repo_name", "issue")
branch = cfg.get("issue_repo_branch", "main")
cfg.setdefault("framework_url",
"https://gitcode.com/Alice_zhou/Vector_fuzz_cpp_testing/tree/main/turing_sec/aclnnAPI_fuzz/")
date_str = datetime.date.today().strftime("%Y-%m-%d")
rows = load_detail_rows(args.xlsx)
groups = group_valid_by_api(rows)
if args.api:
want = set(args.api)
groups = {k: v for k, v in groups.items() if k in want}
if not groups:
log("ERROR", "无有效意外成功问题点(甄别结论 ∈ {})".format(VALID_CONCLUSIONS))
return 1
log("INFO", "待提单接口 {} 个: {}".format(len(groups), ", ".join(sorted(groups))))
global_registry_path = "out_res/issues/filed_all.json"
global_registry = {}
if os.path.isfile(global_registry_path):
try:
global_registry = json.load(open(global_registry_path, encoding="utf-8"))
except Exception:
global_registry = {}
creator = IssueCreator(cfg.get("api_base", "https://gitcode.com/api/v5"), token, owner, repo)
existing_titles = {}
if args.file:
for it in creator.list_issues(state="all"):
existing_titles[str(it.get("title") or "")] = it.get("html_url") or ""
issue_dir = os.path.join("out_res", "issues", date_str)
os.makedirs(issue_dir, exist_ok=True)
filed_path = os.path.join(issue_dir, "filed.json")
filed = {}
if os.path.isfile(filed_path):
try:
filed = json.load(open(filed_path, encoding="utf-8"))
except Exception:
filed = {}
repo_client = IssueRepoClient(args.config)
issue_urls = {}
failed = []
filed_ok = []
for api in sorted(groups):
recs = groups[api]
sig = "{}__err:意外成功甄别:merged".format(api)
title = build_title(api, recs)
blob_url = cpp_blob_url(owner, repo, branch, date_str, api)
cpp_local = os.path.join("output", api, "{}_error.cpp".format(api))
repo_path = "artifacts/{}/{}_error.cpp".format(date_str, api)
if filed.get("{}_error意外成功甄别".format(api)):
issue_urls[api] = filed["{}_error意外成功甄别".format(api)]
log("REUSE", "{} 已提单(本地 filed.json),复用: {}".format(api, issue_urls[api]))
continue
if global_registry.get(sig):
issue_urls[api] = global_registry[sig]
log("REUSE", "{} 已提单(全局注册表),复用: {}".format(api, issue_urls[api]))
continue
if existing_titles.get(title):
issue_urls[api] = existing_titles[title]
log("REUSE", "{} 仓内已存在同标题 issue,复用: {}".format(api, issue_urls[api]))
continue
if not os.path.isfile(cpp_local):
log("WARN", "{} error CPP 不存在: {}(issue 仍提交,附件链接回退 blob 占位)".format(api, cpp_local))
cpp_url = blob_url
elif not args.file:
cpp_url = blob_url
else:
ok, dl_url, _html = repo_client.upload_file(cpp_local, repo_path,
"cpp: {} error 意外成功甄别复现附件".format(api))
cpp_url = dl_url or blob_url
if ok:
log("UPLOAD", "{} cpp 附件上传成功 → {}".format(api, cpp_url))
else:
log("WARN", "{} cpp 附件上传失败: {}(回退 blob 链接)".format(api, dl_url))
body = build_body(api, recs, cfg, cpp_url)
md_path = os.path.join(issue_dir, "{}_error意外成功甄别.md".format(api))
with open(md_path, "w", encoding="utf-8") as fh:
fh.write("# {}\n\n> 目标仓: {}/{}\n\n{}\n".format(title, owner, repo, body))
if not args.file:
log("DRYRUN", "[{}处意外成功] {} → {}(目标 {}/{},附件 {})".format(
len(recs), api, md_path, owner, repo, repo_path))
issue_urls[api] = "(dry-run 未提单)"
continue
code, resp = creator.create_issue(title, body)
if code in (200, 201):
url = resp.get("html_url") or "https://gitcode.com/{}/{}/issues/{}".format(owner, repo, resp.get("number", ""))
issue_urls[api] = url
filed["{}_error意外成功甄别".format(api)] = url
global_registry[sig] = url
filed_ok.append((api, url, len(recs)))
log("FILED", "[{}处意外成功] {} → {}/{} issue #{}: {}".format(
len(recs), api, owner, repo, resp.get("number", ""), url))
else:
failed.append(api)
log("ERROR", "{} 提单失败 [{}]: {}".format(api, code, json.dumps(resp, ensure_ascii=False)[:300]))
if args.file:
json.dump(filed, open(filed_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
json.dump(global_registry, open(global_registry_path, "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
summary = ["# {} error 意外成功甄别按接口提单汇总".format(date_str),
"- 目标仓: https://gitcode.com/{}".format("/{}/issues".format(repo) if owner == "Alice_zhou" else owner + "/" + repo),
"- 待提接口: {} 个 | 提单成功: {} | 失败/跳过: {}".format(len(groups), len(filed_ok), len(failed))]
for api, url, n in filed_ok:
summary.append("- [{}]({}) — {} 处意外成功".format(api, url, n))
if failed:
summary.append("- 失败清单: {}".format(",".join(failed)))
with open(os.path.join(issue_dir, "summary_error意外成功甄别.md"), "w", encoding="utf-8") as fh:
fh.write("\n".join(summary) + "\n")
all_mds = collect_docs_mds()
md_links = {}
for api in sorted(set(str(r.get("接口名") or "").strip() for r in rows if r.get("接口名"))):
repo_path = resolve_md_repo_path(api, all_mds)
if repo_path:
md_links[api] = md_doc_url(repo_path)
else:
log("WARN", "{} 未定位到 md 文档(md资料链接列留空)".format(api))
out_xlsx = args.output_xlsx or args.xlsx.replace(".xlsx", "_issue.xlsx")
wb = openpyxl.load_workbook(args.xlsx)
ws = wb[DETAIL_SHEET]
header = [str(c.value).strip() if c.value is not None else "" for c in ws[1]]
if MD_COL not in header:
ws.cell(row=1, column=len(header) + 1, value=MD_COL)
header.append(MD_COL)
if "issue" not in header:
ws.cell(row=1, column=len(header) + 1, value="issue")
header.append("issue")
md_col = header.index(MD_COL) + 1
issue_col = header.index("issue") + 1
for row_no in range(2, ws.max_row + 1):
api = str(ws.cell(row=row_no, column=header.index("接口名") + 1).value or "").strip()
concl = str(ws.cell(row=row_no, column=header.index("甄别结论") + 1).value or "").strip()
md_cell = ws.cell(row=row_no, column=md_col)
if api in md_links:
md_cell.value = md_links[api]
md_cell.hyperlink = md_links[api]
cell = ws.cell(row=row_no, column=issue_col)
if concl in VALID_CONCLUSIONS and api in issue_urls and issue_urls[api].startswith("http"):
cell.value = issue_urls[api]
cell.hyperlink = issue_urls[api]
elif concl in VALID_CONCLUSIONS:
cell.value = issue_urls.get(api, "")
else:
cell.value = "无需提单"
if "汇总" in wb.sheetnames:
ws2 = wb["汇总"]
ws2.append(("md资料链接", "已按接口回填 {} 个(GitCode cann/ops-* blob/master,甄别依据所用 md 基准版本)".format(len(md_links))))
ws2.append(("提单仓", "https://gitcode.com/{}/{}".format(owner, repo)))
if args.file:
reused = sum(1 for u in issue_urls.values() if str(u).startswith("http")) - len(filed_ok)
new_filed = len(filed_ok)
if reused > 0:
ws2.append(("提单结果", "共 {} 个接口已提单(本次新建 {} 个 / 复用已有 {} 个,明细见「意外成功甄别明细」issue 列)".format(
len(groups), new_filed, reused)))
else:
ws2.append(("提单结果", "共 {} 个接口提交 {} 个 issue(明细见「意外成功甄别明细」issue 列)".format(
len(groups), new_filed)))
else:
ws2.append(("提单结果", "dry-run 预览(--file 正式提单)"))
wb.save(out_xlsx)
log("RESULT", "提单成功 {} | 失败 {} | 结果表格 → {}".format(len(filed_ok), len(failed), out_xlsx))
if args.file and failed:
return 2
return 0
if __name__ == "__main__":
sys.exit(main())