muse-agent-example/工具/并行数据库测试.py
zizi 0658d178ad 工程底座:单一生成入口、验证期写闸门与并行分片
- Makefile:新增 验收数据库(生成→检查→库层全量,前置校验隔离库连接串)、数据库分片、浏览器测试三个入口;
  格式写入 末尾就地重新生成;数据库测试 与 验收数据库 统一排除 浏览器/网络;快检 不再静默跳过类型门;
  前端旅程 预检补齐四个必需变量;pytest 目标改用仓内解释器(uv run 在嵌套检出会解析到外层环境)。
- 工具/验证锁.py:验证会话持共享锁,写入口用 --执行 在独占锁内落盘,生成/格式写入/索引生成走同一闸门。
- 工具/并行数据库测试.py:按文件分片并行,缺连接串在采集前拒绝。
- 工具/构建编排.py、环境预检.py、构建资源包.py、维护索引.py 与上述口径对齐。
- .gitignore / CI / README / AGENTS:忽略构建产物、CI 与 Makefile 单一口径、README 按实际实现陈述、AGENTS 补日常入口与验证纪律。
2026-09-18 01:14:50 +08:00

121 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""隔离库层的分片并行入口:发现与修复期用分片,全量只跑一次做最终判定。
为什么能并行:每个 pytest 会话自带会话前缀的克隆库(`muse_test_<会话>_<随机>`),
角色初始化走共享目录的咨询锁,模板按指纹缓存——互不共享可写状态。
分片只按测试文件切,避免同一文件的夹具被拆到两个进程。
用法:
python 工具/并行数据库测试.py --片数 3 # 全部隔离库用例
python 工具/并行数据库测试.py --片数 3 tests/迁移 # 只跑指定范围
退出码:任一分片非零即非零;每片日志落在 /tmp/muse-数据库分片-<序号>.log。
"""
from __future__ import annotations
import argparse
import os
import shlex
import subprocess
import sys
import tempfile
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
根 = Path(__file__).resolve().parents[1]
基础表达式 = "数据库 and not 宿主 and not 真实模型 and not 浏览器 and not 网络"
def 收集分片范围(范围: list[str], 片数: int, 表达式: str) -> list[list[str]]:
"""按文件把用例均分到各片;同一文件的用例永不拆开。"""
命令 = [
sys.executable,
"-m",
"pytest",
"--外部环境",
"-m",
表达式,
"--collect-only",
"-q",
*范围,
]
结果 = subprocess.run(命令, cwd=根, capture_output=True, text=True, timeout=600)
if 结果.returncode != 0:
print(结果.stdout[-2000:], file=sys.stderr)
print(结果.stderr[-2000:], file=sys.stderr)
raise SystemExit("分片前采集失败;先修采集错误再分片")
计数: dict[str, int] = {}
for 行 in 结果.stdout.splitlines():
if "::" not in 行:
continue
文件 = 行.split("::", 1)[0].strip()
if 文件.endswith(".py"):
计数[文件] = 计数.get(文件, 0) + 1
if not 计数:
raise SystemExit("没有采集到隔离库用例;检查标记表达式与范围")
桶: list[list[str]] = [[] for _ in range(片数)]
负载 = [0] * 片数
for 文件, 数量 in sorted(计数.items(), key=lambda kv: (-kv[1], kv[0])):
最轻 = 负载.index(min(负载))
桶[最轻].append(文件)
负载[最轻] += 数量
return 桶
def 跑一片(序号: int, 文件: list[str], 表达式: str) -> tuple[int, str, str]:
日志 = Path(tempfile.gettempdir()) / f"muse-数据库分片-{序号}.log"
命令 = [
sys.executable,
"-m",
"pytest",
"--外部环境",
"-m",
表达式,
"-q",
"-p",
"no:cacheprovider",
"--durations=10",
*文件,
]
with 日志.open("w", encoding="utf-8") as 输出:
输出.write(" ".join(shlex.quote(x) for x in 命令) + "\n\n")
输出.flush()
进程 = subprocess.run(命令, cwd=根, stdout=输出, stderr=subprocess.STDOUT)
尾部 = "\n".join(日志.read_text(encoding="utf-8").splitlines()[-3:])
return 进程.returncode, str(日志), 尾部
def main(argv: list[str] | None = None) -> int:
解析 = argparse.ArgumentParser(description=__doc__)
解析.add_argument("--片数", type=int, default=3)
解析.add_argument(
"--含慢", action="store_true", help="含单例超过三分钟的慢用例;最终全量才需要"
)
解析.add_argument("范围", nargs="*", default=[])
值 = 解析.parse_args(argv)
if 值.片数 < 1:
raise SystemExit("片数必须为正整数")
# 连接串先查:采集期不连库,缺配置只会在采集之后暴露成误导性的「采集失败」。
环境 = {**os.environ}
if not 环境.get("MUSE_TEST_DATABASE_URL"):
print("缺少 MUSE_TEST_DATABASE_URL;隔离库层拒绝隐式回退", file=sys.stderr)
return 2
范围 = 值.范围 or ["tests"]
表达式 = 基础表达式 if 值.含慢 else f"{基础表达式} and not 慢"
桶 = 收集分片范围(范围, 值.片数, 表达式)
print(f"分片:{值.片数} 片,共 {sum(len(b) for b in 桶)} 个测试文件;表达式 {表达式}")
for 序号, 文件 in enumerate(桶, start=1):
print(f" 第 {序号} 片:{len(文件)} 个文件")
with ThreadPoolExecutor(max_workers=值.片数) as 池:
结果 = list(池.map(lambda 项: 跑一片(项[0], 项[1], 表达式), enumerate(桶, start=1)))
失败 = 0
for 序号, (码, 日志, 尾部) in enumerate(结果, start=1):
print(f"\n=== 第 {序号} 片 退出码 {码};日志 {日志}\n{尾部}")
失败 += 码 != 0
print(f"\n分片汇总:{值.片数} 片,{失败} 片非零")
return 1 if 失败 else 0
if __name__ == "__main__":
raise SystemExit(main())