From 4cd9b5eaa4adecb3102c6866f14587beac86a931 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 31 Oct 2026 08:58:16 +0000 Subject: [PATCH] =?UTF-8?q?coverage85-step3:=20=E8=BF=90=E8=A1=8C=E5=99=A8?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=8A=A0=E6=A0=B9=E8=B7=AF=E5=BE=84=EF=BC=8C?= =?UTF-8?q?tests/=20E402=20=E6=B8=85=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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%),未补新测试。 --- .coveragerc | 1 + .github/workflows/host-verify.yml | 9 +- .../tests/test_coverage85_verify_multiuser.py | 1 - run_tests.py | 158 ++++++++++++++++++ tests/test_client.py | 5 - tests/test_host.py | 5 +- tests/test_host_continue.py | 6 - tests/test_host_coverage85.py | 8 +- tests/test_host_coverage85_2.py | 7 +- tests/test_host_dispatch_branch.py | 5 - tests/test_host_resume_missing.py | 5 - tests/test_host_run_verify.py | 6 - tests/test_host_sleep.py | 5 - tests/test_host_state_lock.py | 7 - tests/test_host_thread_cli.py | 6 - tests/test_host_trace.py | 8 +- tests/test_run_parallel.py | 5 - 17 files changed, 172 insertions(+), 75 deletions(-) create mode 100755 run_tests.py diff --git a/.coveragerc b/.coveragerc index 78a0805..005131d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -18,6 +18,7 @@ omit = tests/* work/* examples/* + */run_tests.py [report] show_missing = False diff --git a/.github/workflows/host-verify.yml b/.github/workflows/host-verify.yml index 68f899c..7b9044a 100644 --- a/.github/workflows/host-verify.yml +++ b/.github/workflows/host-verify.yml @@ -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: diff --git a/deploy/tests/test_coverage85_verify_multiuser.py b/deploy/tests/test_coverage85_verify_multiuser.py index b826d71..45992e9 100644 --- a/deploy/tests/test_coverage85_verify_multiuser.py +++ b/deploy/tests/test_coverage85_verify_multiuser.py @@ -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")) diff --git a/run_tests.py b/run_tests.py new file mode 100755 index 0000000..5cf911a --- /dev/null +++ b/run_tests.py @@ -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:])) diff --git a/tests/test_client.py b/tests/test_client.py index 3bfd5d1..4a4fa4d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -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 diff --git a/tests/test_host.py b/tests/test_host.py index accbcbe..7a2afec 100644 --- a/tests/test_host.py +++ b/tests/test_host.py @@ -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): diff --git a/tests/test_host_continue.py b/tests/test_host_continue.py index b13a241..baa60f5 100644 --- a/tests/test_host_continue.py +++ b/tests/test_host_continue.py @@ -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 diff --git a/tests/test_host_coverage85.py b/tests/test_host_coverage85.py index 26d6a3b..5d7b07d 100644 --- a/tests/test_host_coverage85.py +++ b/tests/test_host_coverage85.py @@ -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 diff --git a/tests/test_host_coverage85_2.py b/tests/test_host_coverage85_2.py index 4b9ba84..ab8b631 100644 --- a/tests/test_host_coverage85_2.py +++ b/tests/test_host_coverage85_2.py @@ -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 diff --git a/tests/test_host_dispatch_branch.py b/tests/test_host_dispatch_branch.py index 155c93f..37b9f3a 100644 --- a/tests/test_host_dispatch_branch.py +++ b/tests/test_host_dispatch_branch.py @@ -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 diff --git a/tests/test_host_resume_missing.py b/tests/test_host_resume_missing.py index 9f7717c..f595c29 100644 --- a/tests/test_host_resume_missing.py +++ b/tests/test_host_resume_missing.py @@ -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 diff --git a/tests/test_host_run_verify.py b/tests/test_host_run_verify.py index 488b92f..9b47473 100644 --- a/tests/test_host_run_verify.py +++ b/tests/test_host_run_verify.py @@ -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 diff --git a/tests/test_host_sleep.py b/tests/test_host_sleep.py index 44790f9..4f48265 100644 --- a/tests/test_host_sleep.py +++ b/tests/test_host_sleep.py @@ -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 diff --git a/tests/test_host_state_lock.py b/tests/test_host_state_lock.py index 7758da5..a1ae84c 100644 --- a/tests/test_host_state_lock.py +++ b/tests/test_host_state_lock.py @@ -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 diff --git a/tests/test_host_thread_cli.py b/tests/test_host_thread_cli.py index 9dd91a8..d196a52 100644 --- a/tests/test_host_thread_cli.py +++ b/tests/test_host_thread_cli.py @@ -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 diff --git a/tests/test_host_trace.py b/tests/test_host_trace.py index e75e621..e2e2f80 100644 --- a/tests/test_host_trace.py +++ b/tests/test_host_trace.py @@ -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): diff --git a/tests/test_run_parallel.py b/tests/test_run_parallel.py index 751ec07..ab969aa 100644 --- a/tests/test_run_parallel.py +++ b/tests/test_run_parallel.py @@ -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