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%),未补新测试。
This commit is contained in:
host committed 2026-11-03 02:48:54 +00:00
1 parent 903f138fd9
commit 4cd9b5eaa4
17 files changed
+172 -75

No files matched your search

+1
View File
@@ -18,6 +18,7 @@ omit =
tests/*
work/*
examples/*
*/run_tests.py
[report]
show_missing = False
+6 -3
View File
@@ -28,9 +28,12 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python3 -m unittest discover -s tests -v
- run: python3 -m unittest discover -s cloud/tests -v
- run: python3 -m unittest discover -s deploy/tests -v
# 测试入口统一走 run_tests.py:它在运行器层面加仓库根路径
# (deploy 套件再加 deploy/ 目录,见 run_tests.py docstring),
# 测试文件本身不再写 sys.path 前置 import(ruff E402 清零轨)。
# coverage 作业仍用下面三条 discover 原命令:运行器是子进程
# 分套件,coverage 测不到子进程里的测试。
- run: python3 run_tests.py -v
lint:
runs-on: ubuntu-latest
steps:
@@ -20,7 +20,6 @@ from contextlib import redirect_stdout
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
_SPEC = importlib.util.spec_from_file_location(
"verify_multiuser",
os.path.join(REPO_ROOT, "deploy", "verify-multiuser.py"))
Executable
+158
View File
@@ -0,0 +1,158 @@
#!/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:]))
-5
View File
@@ -16,12 +16,7 @@ import tempfile
import types
import unittest
from contextlib import redirect_stdout, redirect_stderr
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import client as client_mod
+1 -4
View File
@@ -6,12 +6,9 @@ import sys
import tempfile
import unittest
from pathlib import Path
import host
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
class EffectiveCfgTest(unittest.TestCase):
-6
View File
@@ -10,13 +10,7 @@ import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
+2 -6
View File
@@ -12,17 +12,13 @@ import os
import queue
import sqlite3
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
from host import Server
FAKE_SCRIPT = r'''#!/usr/bin/env python3
+3 -4
View File
@@ -23,10 +23,6 @@ import time
import unittest
from contextlib import redirect_stdout
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
from host import (
dispatch,
@@ -39,6 +35,9 @@ from host import (
run_task_turn_with_retry,
)
REPO_ROOT = Path(__file__).resolve().parents[1]
HERE = os.path.dirname(os.path.abspath(host.__file__))
FAKE_SCRIPT = r'''#!/usr/bin/env python3
-5
View File
@@ -14,15 +14,10 @@
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
import uuid
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
-5
View File
@@ -13,12 +13,7 @@ import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
-6
View File
@@ -13,14 +13,8 @@ run_verify(1319)、notify(1497)、goal verify 两处
与 `$(touch )` 不执行、只落日志原文),本文件不再重复。
"""
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
-5
View File
@@ -5,12 +5,7 @@ import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
-7
View File
@@ -14,16 +14,9 @@ import glob
import json
import multiprocessing
import os
import sys
import tempfile
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
-6
View File
@@ -5,13 +5,7 @@ import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
+1 -7
View File
@@ -7,16 +7,10 @@
import json
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from host import Server, TRACE_MAX_LINES, _summarize_trace_item
try:
from host import Server, TRACE_MAX_LINES, _summarize_trace_item
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from host import Server, TRACE_MAX_LINES, _summarize_trace_item
def _msg(item, ts=1700000000000):
-5
View File
@@ -13,12 +13,7 @@ import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import run_parallel as rp_mod