test: test_queue_sandbox 改 cloud 包导入,不再依赖顶层 queue 遮蔽标准库
旧写法把 cloud/ 插到 sys.path 前面再 from queue import TaskQueue: 一旦其它测试模块先 import 标准库 queue(缓进 sys.modules),本文件 即 ImportError。改成 cloud.queue / cloud.sandbox 包导入(失败时按 test_docker_sandbox.py 同款 fallback 补项目根目录),mock.patch 目标 同步改成 cloud.sandbox.*。新增 TestImportRobustness:断言标准库 queue 未被遮蔽,子进程先 import 标准库 queue 再加载本文件仍全过(子进程 以环境变量跳过自身用例防递归)。
This commit is contained in:
1 parent
1e024f175f
commit
48e66d6c14
1 file changed
+77
-18
@@ -3,25 +3,32 @@
|
||||
运行(在 ~/workspace/codex-app-server 下):
|
||||
python3 -m unittest discover -s cloud/tests -p "test_queue_sandbox.py" -v
|
||||
|
||||
说明:cloud/ 没有 __init__.py(协调员统一创建),因此这里把 cloud/
|
||||
目录插到 sys.path 前面再导入 queue / sandbox,与 discover 的
|
||||
top-level 目录无关。
|
||||
说明:用 cloud.queue / cloud.sandbox 包导入,绝不把 cloud/ 目录
|
||||
插到 sys.path 前面——后者会让顶层模块名 queue 遮蔽标准库 queue,
|
||||
一旦有其它测试模块先 import 标准库 queue(已缓进 sys.modules),
|
||||
本文件就会 ImportError(见 TestImportRobustness 的回归证明)。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
|
||||
from queue import TaskQueue, budget_exceeded, order_tasks, ready_tasks
|
||||
from sandbox import (CloudExecutor, DockerExecutor, Executor,
|
||||
LocalExecutor, _parse_goal_output,
|
||||
_parse_host_result, _result_text)
|
||||
try:
|
||||
from cloud.queue import TaskQueue, budget_exceeded, order_tasks, ready_tasks
|
||||
from cloud.sandbox import (CloudExecutor, DockerExecutor, Executor,
|
||||
LocalExecutor, _parse_goal_output,
|
||||
_parse_host_result, _result_text)
|
||||
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.queue import TaskQueue, budget_exceeded, order_tasks, ready_tasks
|
||||
from cloud.sandbox import (CloudExecutor, DockerExecutor, Executor,
|
||||
LocalExecutor, _parse_goal_output,
|
||||
_parse_host_result, _result_text)
|
||||
|
||||
|
||||
def _task(task_id, depends_on=None, priority=0, status="queued"):
|
||||
@@ -212,7 +219,7 @@ class TestExecutors(unittest.TestCase):
|
||||
{"text": text}, "unused"))
|
||||
|
||||
def test_docker_executor_no_docker_raises_not_local(self):
|
||||
with mock.patch("sandbox.shutil.which", return_value=None):
|
||||
with mock.patch("cloud.sandbox.shutil.which", return_value=None):
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
DockerExecutor().execute({"id": "t1"})
|
||||
self.assertIn("docker not available", str(ctx.exception))
|
||||
@@ -225,7 +232,7 @@ class TestExecutors(unittest.TestCase):
|
||||
seen.append(cmd)
|
||||
return SimpleNamespace(returncode=0, stdout="ok-status")
|
||||
|
||||
with mock.patch("sandbox.shutil.which",
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
executor = DockerExecutor(
|
||||
"/fake/host.py", runner=fake_runner,
|
||||
@@ -388,7 +395,7 @@ class TestGoalExecutors(unittest.TestCase):
|
||||
return SimpleNamespace(returncode=1, stdout="",
|
||||
stderr="boom")
|
||||
|
||||
with mock.patch("sandbox.shutil.which",
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
executor = DockerExecutor(
|
||||
"/fake/host.py", runner=fake_runner,
|
||||
@@ -406,7 +413,7 @@ class TestGoalExecutors(unittest.TestCase):
|
||||
return SimpleNamespace(returncode=1, stdout="",
|
||||
stderr="boom")
|
||||
|
||||
with mock.patch("sandbox.shutil.which",
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
executor = DockerExecutor(
|
||||
"/fake/host.py", runner=fake_runner,
|
||||
@@ -437,7 +444,7 @@ class TestGoalExecutors(unittest.TestCase):
|
||||
return SimpleNamespace(returncode=0, stdout=refusal,
|
||||
stderr="")
|
||||
|
||||
with mock.patch("sandbox.shutil.which",
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
executor = DockerExecutor(
|
||||
"/fake/host.py", runner=fake_runner,
|
||||
@@ -493,7 +500,7 @@ class TestGoalExecutors(unittest.TestCase):
|
||||
def __init__(self, cmd, **kwargs):
|
||||
seen.append(list(cmd))
|
||||
|
||||
with mock.patch("sandbox.subprocess.Popen", _FakePopen):
|
||||
with mock.patch("cloud.sandbox.subprocess.Popen", _FakePopen):
|
||||
LocalExecutor("/fake/host.py").popen_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "test -f x", "goal_max_rounds": 2},
|
||||
@@ -514,7 +521,7 @@ class TestGoalExecutors(unittest.TestCase):
|
||||
"goal unmet after rounds\n",
|
||||
stderr="")
|
||||
|
||||
with mock.patch("sandbox.shutil.which",
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
executor = DockerExecutor(
|
||||
"/fake/host.py", runner=fake_runner,
|
||||
@@ -626,7 +633,7 @@ class TestUsageParsing(unittest.TestCase):
|
||||
"reasoning=10 total=150 completions=2\n",
|
||||
stderr="")
|
||||
|
||||
with mock.patch("sandbox.shutil.which",
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
executor = DockerExecutor(
|
||||
"/fake/host.py", runner=fake_runner,
|
||||
@@ -638,5 +645,57 @@ class TestUsageParsing(unittest.TestCase):
|
||||
self.assertEqual(result["usage"]["cached_input_tokens"], 40)
|
||||
|
||||
|
||||
class TestImportRobustness(unittest.TestCase):
|
||||
"""导入顺序回归证明:本文件绝不依赖顶层 queue 遮蔽标准库。
|
||||
|
||||
旧写法把 cloud/ 插到 sys.path 前面再 `from queue import ...`:
|
||||
一旦其它测试模块先 import 标准库 queue,本文件就 ImportError。
|
||||
新写法走 cloud.queue / cloud.sandbox 包导入,与标准库 queue
|
||||
共存,下面的证明在两种层面都钉住这一点。
|
||||
"""
|
||||
|
||||
def test_stdlib_queue_not_shadowed(self):
|
||||
# 本文件加载后,sys.modules["queue"] 仍是标准库,不是 cloud/queue.py。
|
||||
import queue as stdlib_queue
|
||||
|
||||
self.assertNotIn("cloud", stdlib_queue.__file__)
|
||||
self.assertNotIn("TaskQueue", dir(stdlib_queue))
|
||||
# 包导入的 TaskQueue 与标准库无关。
|
||||
from cloud.queue import TaskQueue as pkg_queue
|
||||
|
||||
self.assertIs(pkg_queue, TaskQueue)
|
||||
|
||||
def test_stdlib_queue_preimported_then_module_still_passes(self):
|
||||
# 子进程里先 import 标准库 queue(缓进 sys.modules),再从
|
||||
# 文件加载本模块并跑全量:旧写法在此处 ImportError,新写法全过。
|
||||
# 子进程通过环境变量跳过本用例自身,避免无限递归派生。
|
||||
if os.environ.get("_QUEUE_SANDBOX_ROBUSTNESS_CHILD") == "1":
|
||||
self.skipTest("子进程内跳过,避免递归派生")
|
||||
test_file = str(Path(__file__).resolve())
|
||||
repo_root = str(Path(__file__).resolve().parents[2])
|
||||
child_code = (
|
||||
"import queue, sys\n"
|
||||
"assert 'TaskQueue' not in dir(queue)\n"
|
||||
"import importlib.util, unittest\n"
|
||||
"spec = importlib.util.spec_from_file_location(\n"
|
||||
" 'test_queue_sandbox_stdlib_first', %r)\n"
|
||||
"mod = importlib.util.module_from_spec(spec)\n"
|
||||
"spec.loader.exec_module(mod)\n"
|
||||
"suite = unittest.defaultTestLoader.loadTestsFromModule(mod)\n"
|
||||
"res = unittest.TextTestRunner(verbosity=0).run(suite)\n"
|
||||
"sys.exit(0 if res.wasSuccessful() else 1)\n" % test_file
|
||||
)
|
||||
env = dict(os.environ)
|
||||
env["_QUEUE_SANDBOX_ROBUSTNESS_CHILD"] = "1"
|
||||
env["PYTHONPATH"] = repo_root
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", child_code],
|
||||
env=env, cwd=repo_root, capture_output=True, text=True,
|
||||
timeout=120)
|
||||
self.assertEqual(proc.returncode, 0,
|
||||
msg=proc.stdout + proc.stderr)
|
||||
self.assertIn("OK", proc.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user