AAlice_zhouupdate
8ac7e837创建于 25 天前历史提交
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""压测结束后归档脚本:将本次新增/修改的 aclnn 接口的 test-design 与 cpp 文件归档至 out_git/ 目录

功能定位:out_git/ 是 output/ 的干净可 git 提交产物镜像(仅 test-design 约束文件 + 各场景 CPP,
不含 fuzz_logs/plog/执行日志/unverified),压测结束后归档本次触碰接口的新增/修改文件,
供版本管理与跨环境复用(详见 AGENTS.md "out_git 归档" 章节)。

归档范围(每接口):
  - output/{api}/test-design/**            → out_git/{api}/test-design/**(03~08 yaml、error_scenarios.yaml、
                                             deterministic_check.json 等全部约束产物)
  - output/{api}/*.cpp(顶层)              → out_git/{api}/(normal/non_contig/multithread/consistent/
                                             error/sample CPP;unverified/ 子目录与 fuzz_logs/ 不归档)

增量语义("本次新增依旧修改"):
  按内容 sha256 对比 out_git/ 已有文件——新增文件拷贝并计"新增",内容变化拷贝并计"修改",
  未变化跳过(幂等);--force 强制全量覆盖。归档历史追加 out_git/archive_history.jsonl。

接口清单来源(三选一,优先级 --apis > --xlsx > --all):
  --apis aclnnAdd,aclnnLayerNorm   显式指定(单接口流程/指定列表)
  --xlsx out_res/批量压测结果.xlsx  从本地结果表格提取本次压测的接口清单(批量压测默认)
  --all                            output/ 下全部接口(全量归档/补档)

用法:
  python scripts/archive_out_git.py --apis aclnnAdd                          # 单接口流程压测结束后
  python scripts/archive_out_git.py --xlsx out_res/批量压测结果.xlsx          # 批量压测 B5(默认 xlsx)
  python scripts/archive_out_git.py --all                                    # 全量归档/补档
  python scripts/archive_out_git.py --apis aclnnAdd --force                  # 强制覆盖未变化文件

退出码: 0=归档完成(含无新变化); 1=错误(无接口清单/目录缺失)
"""
import argparse
import hashlib
import json
import os
import shutil
import sys
import time

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")

DEFAULT_OUTPUT_ROOT = "output"
DEFAULT_OUT_GIT = "out_git"
DEFAULT_XLSX = os.path.join("out_res", "批量压测结果.xlsx")
HISTORY_FILE = "archive_history.jsonl"


def log(tag, msg):
    print("[{}] {}".format(tag, msg))


def file_sha256(path):
    h = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def apis_from_xlsx(xlsx_path):
    """从本地批量压测结果表格提取接口清单(第一列去重)。"""
    try:
        import openpyxl
    except ImportError:
        raise RuntimeError("openpyxl 未安装(pip install openpyxl),或改用 --apis/--all 指定接口清单")
    if not os.path.isfile(xlsx_path):
        raise RuntimeError("结果表格不存在: {}(批量压测未产生结果;单接口流程请用 --apis,全量归档用 --all)".format(xlsx_path))
    wb = openpyxl.load_workbook(xlsx_path, read_only=True)
    ws = wb.active
    apis = []
    for row in ws.iter_rows(min_row=2, max_col=1, values_only=True):
        val = row[0] if row else None
        if val is not None and str(val).strip():
            api = str(val).strip()
            if api not in apis:
                apis.append(api)
    wb.close()
    if not apis:
        raise RuntimeError("结果表格无数据行: {}".format(xlsx_path))
    return apis


def apis_from_output(output_root):
    """output/ 下全部接口目录(含 test-design 或 cpp 的目录)。"""
    apis = []
    if not os.path.isdir(output_root):
        raise RuntimeError("output 目录不存在: {}".format(output_root))
    for name in sorted(os.listdir(output_root)):
        api_dir = os.path.join(output_root, name)
        if not os.path.isdir(api_dir):
            continue
        has_td = os.path.isdir(os.path.join(api_dir, "test-design"))
        has_cpp = any(n.endswith(".cpp") for n in os.listdir(api_dir)
                      if os.path.isfile(os.path.join(api_dir, n)))
        if has_td or has_cpp:
            apis.append(name)
    if not apis:
        raise RuntimeError("output/ 下无可归档接口目录")
    return apis


def collect_api_files(output_root, api):
    """该接口的可归档文件清单:[(相对路径, 绝对路径)],test-design/** + 顶层 *.cpp。"""
    api_dir = os.path.join(output_root, api)
    if not os.path.isdir(api_dir):
        return None
    files = []
    for name in sorted(os.listdir(api_dir)):
        path = os.path.join(api_dir, name)
        if os.path.isfile(path) and name.endswith(".cpp"):
            files.append((name, path))
    td_dir = os.path.join(api_dir, "test-design")
    for dirpath, _dirnames, names in os.walk(td_dir):
        for name in sorted(names):
            path = os.path.join(dirpath, name)
            rel = os.path.relpath(path, api_dir).replace(os.sep, "/")
            files.append((rel, path))
    files.sort()
    return files


def archive_api(output_root, out_git, api, force=False):
    """归档单接口:返回 (新增数, 修改数, 未变化数, 文件总数);接口目录缺失返回 None。"""
    files = collect_api_files(output_root, api)
    if files is None or not files:
        return None
    added = modified = unchanged = 0
    for rel, src in files:
        dst = os.path.join(out_git, api, *rel.split("/"))
        os.makedirs(os.path.dirname(dst), exist_ok=True)
        if not os.path.isfile(dst):
            shutil.copy2(src, dst)
            added += 1
        elif force or file_sha256(dst) != file_sha256(src):
            shutil.copy2(src, dst)
            modified += 1
        else:
            unchanged += 1
    return added, modified, unchanged, len(files)


def main():
    parser = argparse.ArgumentParser(
        description="压测结束后归档:本次新增/修改接口的 test-design 与 cpp → out_git/(干净可 git 提交镜像)")
    parser.add_argument("--apis", default=None,
                        help="显式接口清单(逗号分隔,如 aclnnAdd,aclnnLayerNorm;单接口流程使用)")
    parser.add_argument("--xlsx", default=DEFAULT_XLSX,
                        help="批量压测结果表格(从中提取本次压测接口清单,默认 out_res/批量压测结果.xlsx)")
    parser.add_argument("--all", action="store_true",
                        help="全量归档 output/ 下全部接口(补档场景)")
    parser.add_argument("--force", action="store_true",
                        help="强制覆盖未变化文件(默认按内容 hash 增量,未变化跳过)")
    parser.add_argument("--output_root", default=DEFAULT_OUTPUT_ROOT, help="output 根目录(默认 output/)")
    parser.add_argument("--out_git", default=DEFAULT_OUT_GIT, help="归档目录(默认 out_git/)")
    args = parser.parse_args()

    # 接口清单解析:--apis > --all > --xlsx(默认)
    if args.apis:
        apis = [a.strip() for a in args.apis.split(",") if a.strip()]
        if not apis:
            log("ERROR", "--apis 为空")
            return 1
        source = "--apis"
    elif args.all:
        try:
            apis = apis_from_output(args.output_root)
        except RuntimeError as err:
            log("ERROR", str(err))
            return 1
        source = "--all"
    else:
        try:
            apis = apis_from_xlsx(args.xlsx)
        except RuntimeError as err:
            log("ERROR", str(err))
            return 1
        source = "--xlsx({})".format(args.xlsx)

    os.makedirs(args.out_git, exist_ok=True)
    log("ARCHIVE", "接口清单来源: {} | 共 {} 个接口 | 归档目标: {}/".format(source, len(apis), args.out_git))

    total_added = total_modified = total_unchanged = 0
    archived_apis, skipped_apis, new_apis = [], [], []
    for api in apis:
        result = archive_api(args.output_root, args.out_git, api, force=args.force)
        if result is None:
            skipped_apis.append(api)
            log("WARN", "{} 在 output/ 下无 test-design/cpp 可归档,跳过".format(api))
            continue
        added, modified, unchanged, total = result
        total_added += added
        total_modified += modified
        total_unchanged += unchanged
        archived_apis.append(api)
        if added == total:  # 全部文件均为新增 → 本次新增接口
            new_apis.append(api)
        log("ARCHIVE", "{}: 新增 {} / 修改 {} / 未变化 {}(共 {} 文件){}".format(
            api, added, modified, unchanged, total, "  [本次新增接口]" if api in new_apis else ""))

    # 归档历史追加(审计追溯)
    history_path = os.path.join(args.out_git, HISTORY_FILE)
    record = {
        "time": time.strftime("%Y-%m-%d %H:%M:%S"),
        "source": source,
        "api_count": len(archived_apis),
        "new_apis": new_apis,
        "added": total_added,
        "modified": total_modified,
        "unchanged": total_unchanged,
        "force": args.force,
    }
    with open(history_path, "a", encoding="utf-8") as fh:
        fh.write(json.dumps(record, ensure_ascii=False) + "\n")

    log("ARCHIVE", "[RESULT] 归档完成:{} 个接口(本次新增接口 {} 个: {})| 新增 {} 文件 / 修改 {} 文件 / "
                  "未变化 {} 文件 | 跳过 {} 接口 | 历史记录 → {}".format(
                      len(archived_apis), len(new_apis), ",".join(new_apis[:10]) or "-",
                      total_added, total_modified, total_unchanged,
                      ",".join(skipped_apis[:5]) or "-", history_path))
    return 0


if __name__ == "__main__":
    sys.exit(main())