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:
host committed 2026-11-03 02:49:26 +00:00
1 parent 1e024f175f
commit 48e66d6c14
1 file changed
+77 -18
+77 -18
View File
@@ -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()