新增 run_tests.py:三套件统一入口,只加仓库根(deploy 套件 再加 deploy/ 目录在根之前),各套件走独立子进程;verify 作 业改走运行器,coverage 作业因子进程测不到覆盖率保持原 discover 命令。tests/ 13 个文件的 sys.path 前置 import 全部 改回正常 import(含 test_host_trace 的 try/except 回落); deploy 的 deploy 目录插入与 verify-multiuser 的 spec 加载按 陷阱 2 如实保留(仅删 verify-multiuser 冗余的根插入)。 .coveragerc omit 加 */run_tests.py,使新运行器文件不计入 生产覆盖率。门槛未动(仍 75%),未补新测试。
159 lines
6.5 KiB
Python
Executable File
159 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""三套件统一测试运行器:只加仓库根路径,绝不加 cloud/ 本身。
|
||
|
||
背景:tests/ 的单测原来各文件用 sys.path 前置 import(先插路径
|
||
再 import host/client/run_parallel),产生 ruff E402;cloud 测试
|
||
用 try/except ImportError 回落、deploy 测试插 deploy 目录,各自
|
||
为政。本运行器把路径收敛到一处,测试文件只写正常 import。
|
||
|
||
用法(仓库任意目录均可跑,路径由本文件位置推导,不依赖 cwd):
|
||
python3 run_tests.py # 三套件依次跑,汇总后退出
|
||
python3 run_tests.py host|cloud|deploy
|
||
python3 run_tests.py --test tests.test_host_coverage85
|
||
python3 run_tests.py --test cloud.tests.test_server
|
||
python3 run_tests.py --test test_deploy # deploy 套件只认裸模块名
|
||
python3 run_tests.py -v ... # 透传 unittest -v
|
||
|
||
路径规则(两处陷阱,勿改):
|
||
1. 只加仓库根,绝不加 cloud/:cloud/queue.py 会遮蔽标准库 queue,
|
||
Playwright 同步层会报 module 'queue' has no attribute
|
||
'SimpleQueue'(2026-10-30 一期实测)。cloud 测试走
|
||
`from cloud.server import ...`,根路径已足够。
|
||
2. deploy 套件在根之前再加 deploy/ 目录:deploy.py 是 deploy/
|
||
下的文件而非包,只加根时 `import deploy` 会解析成 deploy/
|
||
命名空间包而非 deploy.py(deploy.py 自身 docstring 已警告
|
||
deploy.tests 路径遮蔽问题)。顺序必须是 [deploy/, 根],
|
||
且 deploy 测试仍保留文件内的 deploy 目录插入(见下)。
|
||
|
||
为什么不用其它方案:
|
||
- `discover -s tests -t .`:start 目录缺 __init__.py,unittest
|
||
报 Start directory is not importable,实测三套件全灭。
|
||
- conftest.py:那是 pytest 的机制,unittest 不读。
|
||
- sitecustomize:会污染仓库外的每一次 python 启动,不可接受。
|
||
- 不给 tests/ 加 __init__.py:会改模块名与 discover 语义,
|
||
超出本步范围。
|
||
|
||
与 CI 的分工(host-verify.yml):
|
||
- verify 作业走本运行器(python3 run_tests.py)。
|
||
- coverage 作业仍走三条 `coverage run -m unittest discover`
|
||
原命令:coverage 测不到子进程里的测试,运行器是子进程分套件,
|
||
走它会量出 0 覆盖;从根目录用 -m 跑时 cwd 本就在 sys.path
|
||
上,host/cloud 套件不靠文件内插入也能找到模块,deploy 套件
|
||
靠文件内保留的 deploy 目录插入(陷阱 2)。
|
||
|
||
单文件直跑(迁移后仍可用,须在仓库根目录执行):
|
||
- python3 -m unittest tests.test_host_coverage85
|
||
- python3 -m unittest cloud.tests.test_server
|
||
- deploy 单文件用 python3 run_tests.py --test test_deploy
|
||
不再支持 `python3 tests/test_x.py` 直接脚本执行:那种跑法下
|
||
sys.path[0] 是 tests/ 目录,根不在路径上,改用上面 -m 形式。
|
||
|
||
例外(运行器修不了,如实保留):
|
||
- deploy/tests/test_coverage85_verify_multiuser.py:目标文件
|
||
verify-multiuser.py 带连字符,无法 import,只能继续用
|
||
importlib spec 加载;其根路径插入已删(运行器/覆盖率
|
||
命令都已提供根路径),spec 加载与 noqa: E402 保留。
|
||
"""
|
||
|
||
import subprocess
|
||
import sys
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent
|
||
DEPLOY_DIR = ROOT / "deploy"
|
||
|
||
SUITES = {
|
||
"host": ROOT / "tests",
|
||
"cloud": ROOT / "cloud" / "tests",
|
||
"deploy": ROOT / "deploy" / "tests",
|
||
}
|
||
|
||
|
||
def _setup_paths(suite):
|
||
"""按套件把路径摆好。顺序即优先级,勿调换 deploy 与根的次序。"""
|
||
sys.path.insert(0, str(ROOT))
|
||
if suite == "deploy":
|
||
sys.path.insert(0, str(DEPLOY_DIR))
|
||
|
||
|
||
def _run_discovery(suite, verbose):
|
||
start = str(SUITES[suite])
|
||
_setup_paths(suite)
|
||
loader = unittest.TestLoader()
|
||
found = loader.discover(start, pattern="test_*.py",
|
||
top_level_dir=start)
|
||
runner = unittest.TextTestRunner(verbosity=2 if verbose else 1)
|
||
result = runner.run(found)
|
||
return 0 if result.wasSuccessful() else 1
|
||
|
||
|
||
def _run_single(name, verbose):
|
||
"""单文件/单用例。deploy 套件只认裸模块名(见模块 docstring)。"""
|
||
if name.startswith("tests."):
|
||
suite = "host"
|
||
elif name.startswith("cloud.tests."):
|
||
suite = "cloud"
|
||
elif (SUITES["deploy"] / (name.split(".")[0] + ".py")).is_file():
|
||
suite = "deploy"
|
||
else:
|
||
print("无法推断套件:%s(host 用 tests.…,cloud 用 "
|
||
"cloud.tests.…,deploy 用裸模块名)" % name, file=sys.stderr)
|
||
return 2
|
||
_setup_paths(suite)
|
||
if suite == "deploy":
|
||
sys.path.insert(0, str(SUITES["deploy"]))
|
||
loader = unittest.TestLoader()
|
||
try:
|
||
found = loader.loadTestsFromName(name)
|
||
except Exception as exc: # noqa: BLE001 - 名字错误直接报给调用方
|
||
print("加载失败:%s" % exc, file=sys.stderr)
|
||
return 2
|
||
runner = unittest.TextTestRunner(verbosity=2 if verbose else 1)
|
||
result = runner.run(found)
|
||
return 0 if result.wasSuccessful() else 1
|
||
|
||
|
||
def _run_suite_subprocess(suite, verbose):
|
||
cmd = [sys.executable, str(ROOT / "run_tests.py"),
|
||
"--suite", suite]
|
||
if verbose:
|
||
cmd.append("-v")
|
||
print("=== %s ===" % suite, flush=True)
|
||
proc = subprocess.run(cmd)
|
||
print("=== %s: %s ===" % (
|
||
suite, "PASS" if proc.returncode == 0 else "FAIL"), flush=True)
|
||
return proc.returncode
|
||
|
||
|
||
def main(argv):
|
||
verbose = "-v" in argv or "--verbose" in argv
|
||
args = [a for a in argv if a not in ("-v", "--verbose")]
|
||
if args and args[0] == "--suite":
|
||
# 子进程入口:单套件、当前解释器内跑 discover。
|
||
return _run_discovery(args[1], verbose)
|
||
if args and args[0] == "--test":
|
||
return _run_single(args[1], verbose)
|
||
suites = []
|
||
if not args:
|
||
suites = ["host", "cloud", "deploy"]
|
||
elif len(args) == 1 and args[0] in SUITES:
|
||
suites = [args[0]]
|
||
else:
|
||
print(__doc__.split("用法")[0], file=sys.stderr)
|
||
print("用法:run_tests.py [host|cloud|deploy] "
|
||
"[--test <用例>] [-v]", file=sys.stderr)
|
||
return 2
|
||
# 各套件走独立子进程:sys.path 顺序(尤其 deploy)互不污染,
|
||
# 且与 CI 原来三条独立命令的隔离性一致。
|
||
failed = [s for s in suites if _run_suite_subprocess(s, verbose) != 0]
|
||
if failed:
|
||
print("失败套件:%s" % ", ".join(failed), file=sys.stderr)
|
||
return 1
|
||
print("全部通过:%s" % ", ".join(suites))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv[1:]))
|