Files
codex-app-server/run_tests.py
host 4cd9b5eaa4 coverage85-step3: 运行器统一加根路径,tests/ E402 清零
新增 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%),未补新测试。
2026-11-03 02:48:54 +00:00

159 lines
6.5 KiB
Python
Executable File
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.
#!/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:]))