From 48e66d6c147c949b20d659b823cf07ba953f681a Mon Sep 17 00:00:00 2001 From: host Date: Sat, 31 Oct 2026 12:06:21 +0000 Subject: [PATCH] =?UTF-8?q?test:=20test=5Fqueue=5Fsandbox=20=E6=94=B9=20cl?= =?UTF-8?q?oud=20=E5=8C=85=E5=AF=BC=E5=85=A5=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=A1=B6=E5=B1=82=20queue=20=E9=81=AE?= =?UTF-8?q?=E8=94=BD=E6=A0=87=E5=87=86=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧写法把 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 再加载本文件仍全过(子进程 以环境变量跳过自身用例防递归)。 --- cloud/tests/test_queue_sandbox.py | 95 +++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/cloud/tests/test_queue_sandbox.py b/cloud/tests/test_queue_sandbox.py index c8bb139..2f411a6 100644 --- a/cloud/tests/test_queue_sandbox.py +++ b/cloud/tests/test_queue_sandbox.py @@ -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()