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
d39a772701
commit
38243d3e7f
1 file changed
+75
-16
@@ -3,23 +3,30 @@
|
|||||||
运行(在 ~/workspace/codex-app-server 下):
|
运行(在 ~/workspace/codex-app-server 下):
|
||||||
python3 -m unittest discover -s cloud/tests -p "test_queue_sandbox.py" -v
|
python3 -m unittest discover -s cloud/tests -p "test_queue_sandbox.py" -v
|
||||||
|
|
||||||
说明:cloud/ 没有 __init__.py(协调员统一创建),因此这里把 cloud/
|
说明:用 cloud.queue / cloud.sandbox 包导入,绝不把 cloud/ 目录
|
||||||
目录插到 sys.path 前面再导入 queue / sandbox,与 discover 的
|
插到 sys.path 前面——后者会让顶层模块名 queue 遮蔽标准库 queue,
|
||||||
top-level 目录无关。
|
一旦有其它测试模块先 import 标准库 queue(已缓进 sys.modules),
|
||||||
|
本文件就会 ImportError(见 TestImportRobustness 的回归证明)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
try:
|
||||||
os.path.abspath(__file__))))
|
from cloud.queue import TaskQueue, budget_exceeded, order_tasks, ready_tasks
|
||||||
|
from cloud.sandbox import (CloudExecutor, DockerExecutor, Executor,
|
||||||
from queue import TaskQueue, budget_exceeded, order_tasks, ready_tasks
|
LocalExecutor, _parse_goal_output,
|
||||||
from sandbox import (CloudExecutor, DockerExecutor, Executor,
|
_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,
|
LocalExecutor, _parse_goal_output,
|
||||||
_parse_host_result, _result_text)
|
_parse_host_result, _result_text)
|
||||||
|
|
||||||
@@ -212,7 +219,7 @@ class TestExecutors(unittest.TestCase):
|
|||||||
{"text": text}, "unused"))
|
{"text": text}, "unused"))
|
||||||
|
|
||||||
def test_docker_executor_no_docker_raises_not_local(self):
|
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:
|
with self.assertRaises(RuntimeError) as ctx:
|
||||||
DockerExecutor().execute({"id": "t1"})
|
DockerExecutor().execute({"id": "t1"})
|
||||||
self.assertIn("docker not available", str(ctx.exception))
|
self.assertIn("docker not available", str(ctx.exception))
|
||||||
@@ -225,7 +232,7 @@ class TestExecutors(unittest.TestCase):
|
|||||||
seen.append(cmd)
|
seen.append(cmd)
|
||||||
return SimpleNamespace(returncode=0, stdout="ok-status")
|
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"):
|
return_value="/usr/bin/docker"):
|
||||||
executor = DockerExecutor(
|
executor = DockerExecutor(
|
||||||
"/fake/host.py", runner=fake_runner,
|
"/fake/host.py", runner=fake_runner,
|
||||||
@@ -388,7 +395,7 @@ class TestGoalExecutors(unittest.TestCase):
|
|||||||
return SimpleNamespace(returncode=1, stdout="",
|
return SimpleNamespace(returncode=1, stdout="",
|
||||||
stderr="boom")
|
stderr="boom")
|
||||||
|
|
||||||
with mock.patch("sandbox.shutil.which",
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
return_value="/usr/bin/docker"):
|
return_value="/usr/bin/docker"):
|
||||||
executor = DockerExecutor(
|
executor = DockerExecutor(
|
||||||
"/fake/host.py", runner=fake_runner,
|
"/fake/host.py", runner=fake_runner,
|
||||||
@@ -406,7 +413,7 @@ class TestGoalExecutors(unittest.TestCase):
|
|||||||
return SimpleNamespace(returncode=1, stdout="",
|
return SimpleNamespace(returncode=1, stdout="",
|
||||||
stderr="boom")
|
stderr="boom")
|
||||||
|
|
||||||
with mock.patch("sandbox.shutil.which",
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
return_value="/usr/bin/docker"):
|
return_value="/usr/bin/docker"):
|
||||||
executor = DockerExecutor(
|
executor = DockerExecutor(
|
||||||
"/fake/host.py", runner=fake_runner,
|
"/fake/host.py", runner=fake_runner,
|
||||||
@@ -437,7 +444,7 @@ class TestGoalExecutors(unittest.TestCase):
|
|||||||
return SimpleNamespace(returncode=0, stdout=refusal,
|
return SimpleNamespace(returncode=0, stdout=refusal,
|
||||||
stderr="")
|
stderr="")
|
||||||
|
|
||||||
with mock.patch("sandbox.shutil.which",
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
return_value="/usr/bin/docker"):
|
return_value="/usr/bin/docker"):
|
||||||
executor = DockerExecutor(
|
executor = DockerExecutor(
|
||||||
"/fake/host.py", runner=fake_runner,
|
"/fake/host.py", runner=fake_runner,
|
||||||
@@ -493,7 +500,7 @@ class TestGoalExecutors(unittest.TestCase):
|
|||||||
def __init__(self, cmd, **kwargs):
|
def __init__(self, cmd, **kwargs):
|
||||||
seen.append(list(cmd))
|
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(
|
LocalExecutor("/fake/host.py").popen_task(
|
||||||
{"id": "t1", "prompt": "do it",
|
{"id": "t1", "prompt": "do it",
|
||||||
"verify_command": "test -f x", "goal_max_rounds": 2},
|
"verify_command": "test -f x", "goal_max_rounds": 2},
|
||||||
@@ -514,7 +521,7 @@ class TestGoalExecutors(unittest.TestCase):
|
|||||||
"goal unmet after rounds\n",
|
"goal unmet after rounds\n",
|
||||||
stderr="")
|
stderr="")
|
||||||
|
|
||||||
with mock.patch("sandbox.shutil.which",
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
return_value="/usr/bin/docker"):
|
return_value="/usr/bin/docker"):
|
||||||
executor = DockerExecutor(
|
executor = DockerExecutor(
|
||||||
"/fake/host.py", runner=fake_runner,
|
"/fake/host.py", runner=fake_runner,
|
||||||
@@ -626,7 +633,7 @@ class TestUsageParsing(unittest.TestCase):
|
|||||||
"reasoning=10 total=150 completions=2\n",
|
"reasoning=10 total=150 completions=2\n",
|
||||||
stderr="")
|
stderr="")
|
||||||
|
|
||||||
with mock.patch("sandbox.shutil.which",
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
return_value="/usr/bin/docker"):
|
return_value="/usr/bin/docker"):
|
||||||
executor = DockerExecutor(
|
executor = DockerExecutor(
|
||||||
"/fake/host.py", runner=fake_runner,
|
"/fake/host.py", runner=fake_runner,
|
||||||
@@ -638,5 +645,57 @@ class TestUsageParsing(unittest.TestCase):
|
|||||||
self.assertEqual(result["usage"]["cached_input_tokens"], 40)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user