Skip to content

Commit bf2ec62

Browse files
SummerOneTwoclaude
andcommitted
fix: 修复代码审查发现的多个问题
## Server 模块 - 修复 SolutionAnalyzeTool 导入路径错误(从 complexity.py 导入) - 更新 docstring 工具数量(14 → 15) - 补充测试验证 SolutionAnalyzeTool 注册 ## Utils 模块 - 新增 macOS 资源限制实现(resource 模块 + preexec_fn) - 改进异常处理:捕获具体异常类型而非裸 Exception - 添加日志记录便于调试 - win_job.py 捕获 pywintypes.error ## Tools 模块 - 完善 constraints 参数验证(t_max, sum_n_max) - 新增 test_configs 参数验证(type, n_min, n_max, t_min, t_max) ## 类型注解 - RunToolMixin.run() 添加返回类型 RunResult - solution_type 参数限制为 Literal["sol", "brute"] - solution.py 类型注解统一 ## 测试 - 新增 test_configs 验证测试用例 - 测试数量 129 → 131 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5ff1403 commit bf2ec62

File tree

11 files changed

+259
-18
lines changed

11 files changed

+259
-18
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ dmypy.json
4747
*.swp
4848
*.swo
4949
*~
50+
.claude/
51+
.codex/
52+
.opencode/
5053

5154
# Project specific
5255
*.exe

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.1] - 2026-04-08
9+
10+
### Bug Fixes
11+
12+
- **Server 模块**
13+
- 修复 `SolutionAnalyzeTool` 导入路径错误(从 `complexity.py` 导入而非 `solution.py`
14+
- 更新 docstring 中的工具数量(14 → 15)
15+
- 补充测试验证 `SolutionAnalyzeTool` 注册
16+
17+
- **Utils 模块**
18+
- 新增 macOS 资源限制实现(使用 `resource` 模块 + `preexec_fn`
19+
- 改进异常处理:将裸 `except Exception: pass` 改为捕获具体异常类型
20+
- 添加日志记录(`logging` 模块),便于调试
21+
- `win_job.py` 中捕获 `pywintypes.error` 而非通用 `Exception`
22+
23+
- **Tools 模块**
24+
- 完善 `constraints` 参数验证:新增 `t_max``sum_n_max` 验证
25+
- 新增 `test_configs` 参数验证:验证 `type``n_min``n_max``t_min``t_max` 字段
26+
27+
- **类型注解**
28+
- `RunToolMixin.run()` 添加返回类型注解 `-> RunResult`
29+
- `solution_type` 参数类型限制为 `Literal["sol", "brute"]`
30+
- `solution.py``solution_type` 参数类型统一
31+
32+
### Tests
33+
34+
- 新增 `test_problem_generate_tests_test_configs_validation` 测试用例
35+
- 测试数量从 129 增至 131
36+
837
## [0.3.0] - 2026-04-03
938

1039
### Features

src/autocode_mcp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
MCP Server 入口。
33
4-
提供 14 个原子工具,基于 AutoCode 论文框架。
4+
提供 15 个原子工具,基于 AutoCode 论文框架。
55
"""
66

77
from __future__ import annotations

src/autocode_mcp/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from .base import Tool, ToolResult
55
from .checker import CheckerBuildTool
6+
from .complexity import SolutionAnalyzeTool
67
from .file_ops import FileReadTool, FileSaveTool
78
from .generator import GeneratorBuildTool, GeneratorRunTool
89
from .interactor import InteractorBuildTool
@@ -18,6 +19,7 @@
1819
"FileSaveTool",
1920
"SolutionBuildTool",
2021
"SolutionRunTool",
22+
"SolutionAnalyzeTool",
2123
"StressTestRunTool",
2224
"ProblemCreateTool",
2325
"ProblemGenerateTestsTool",

src/autocode_mcp/tools/mixins.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
from __future__ import annotations
88

9-
from ..utils.compiler import CompileResult, compile_cpp, run_binary
9+
from typing import Literal
10+
11+
from ..utils.compiler import CompileResult, RunResult, compile_cpp, run_binary
1012
from ..utils.resource_limit import get_resource_limit
1113

1214

@@ -40,10 +42,10 @@ async def run(
4042
binary_path: str,
4143
input_data: str,
4244
problem_dir: str,
43-
solution_type: str,
45+
solution_type: Literal["sol", "brute"],
4446
timeout: int | None = None,
4547
memory_mb: int | None = None,
46-
):
48+
) -> RunResult:
4749
limit = get_resource_limit(problem_dir, solution_type, timeout=timeout, memory_mb=memory_mb)
4850
return await run_binary(
4951
binary_path,

src/autocode_mcp/tools/problem.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ async def execute(
202202
"""执行测试数据生成。"""
203203
# 验证 constraints 参数
204204
if constraints:
205+
# 验证 n_max 和 n_min
205206
n_max = constraints.get("n_max")
206207
if n_max is not None and n_max <= 0:
207208
return ToolResult.fail("n_max must be positive")
@@ -211,6 +212,45 @@ async def execute(
211212
if n_max is not None and n_min is not None and n_min > n_max:
212213
return ToolResult.fail("n_min cannot be greater than n_max")
213214

215+
# 验证 t_max
216+
t_max = constraints.get("t_max")
217+
if t_max is not None and t_max <= 0:
218+
return ToolResult.fail("t_max must be positive")
219+
220+
# 验证 sum_n_max
221+
sum_n_max = constraints.get("sum_n_max")
222+
if sum_n_max is not None and sum_n_max <= 0:
223+
return ToolResult.fail("sum_n_max must be positive")
224+
225+
# 验证约束之间的关系
226+
if n_max is not None and sum_n_max is not None and n_max > sum_n_max:
227+
return ToolResult.fail("n_max cannot be greater than sum_n_max")
228+
if t_max is not None and sum_n_max is not None and t_max > sum_n_max:
229+
return ToolResult.fail("t_max cannot be greater than sum_n_max")
230+
231+
# 验证 test_configs 参数
232+
if test_configs:
233+
for i, config in enumerate(test_configs):
234+
# 验证 type 字段
235+
if "type" not in config:
236+
return ToolResult.fail(f"test_configs[{i}]: 'type' is required")
237+
if config["type"] not in ("1", "2", "3", "4"):
238+
return ToolResult.fail(f"test_configs[{i}]: 'type' must be one of '1', '2', '3', '4'")
239+
240+
# 验证 n_min, n_max, t_min, t_max
241+
for field in ["n_min", "n_max", "t_min", "t_max"]:
242+
if field not in config:
243+
return ToolResult.fail(f"test_configs[{i}]: '{field}' is required")
244+
val = config[field]
245+
if not isinstance(val, int) or val < 0:
246+
return ToolResult.fail(f"test_configs[{i}]: '{field}' must be a non-negative integer")
247+
248+
# 验证范围关系
249+
if config["n_min"] > config["n_max"]:
250+
return ToolResult.fail(f"test_configs[{i}]: n_min cannot be greater than n_max")
251+
if config["t_min"] > config["t_max"]:
252+
return ToolResult.fail(f"test_configs[{i}]: t_min cannot be greater than t_max")
253+
214254
exe_ext = get_exe_extension()
215255

216256
# 检查必要文件
@@ -238,6 +278,7 @@ async def execute(
238278
errors = []
239279

240280
# 获取测试配置
281+
test_configs_list: list[tuple[str, str, str, str, str, str]]
241282
if test_configs:
242283
test_configs_list = [
243284
(
@@ -253,11 +294,11 @@ async def execute(
253294
else:
254295
test_configs_list = self._get_default_configs(constraints)
255296

256-
for i, config in enumerate(test_configs_list[:test_count], 1):
297+
for i, test_cfg in enumerate(test_configs_list[:test_count], 1):
257298
test_file = os.path.join(tests_dir, f"{i:02d}.in")
258299
ans_file = os.path.join(tests_dir, f"{i:02d}.ans")
259300

260-
seed_offset, type_param, n_min, n_max, t_min, t_max = config
301+
seed_offset, type_param, n_min, n_max, t_min, t_max = test_cfg
261302
cmd_args = [
262303
str(i + int(seed_offset)),
263304
type_param,

src/autocode_mcp/tools/solution.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import os
6+
from typing import Literal
67

78
from ..utils.platform import get_exe_extension
89
from .base import Tool, ToolResult
@@ -62,7 +63,7 @@ def input_schema(self) -> dict:
6263
async def execute(
6364
self,
6465
problem_dir: str,
65-
solution_type: str,
66+
solution_type: Literal["sol", "brute"],
6667
code: str,
6768
compiler: str = "g++",
6869
) -> ToolResult:
@@ -155,7 +156,7 @@ def input_schema(self) -> dict:
155156
async def execute(
156157
self,
157158
problem_dir: str,
158-
solution_type: str,
159+
solution_type: Literal["sol", "brute"],
159160
input_data: str,
160161
timeout: int = 30,
161162
) -> ToolResult:

src/autocode_mcp/utils/compiler.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from __future__ import annotations
1111

1212
import asyncio
13+
import logging
1314
import os
1415
import shutil
1516
import sys
@@ -24,6 +25,9 @@
2425
if TYPE_CHECKING:
2526
from .win_job import WinJobObject
2627

28+
# 模块级日志器
29+
_logger = logging.getLogger(__name__)
30+
2731
_cache = CompileCache()
2832

2933

@@ -193,6 +197,32 @@ async def compile_cpp(
193197
)
194198

195199

200+
def _set_macos_resource_limit(memory_mb: int) -> None:
201+
"""macOS 上设置进程资源限制(通过 preexec_fn 调用)。
202+
203+
Args:
204+
memory_mb: 内存限制(MB)
205+
206+
Note:
207+
使用 preexec_fn 与 asyncio 存在潜在死锁风险(在极端情况下),
208+
但实际触发概率极低。这是 macOS 上设置资源限制的标准方式。
209+
"""
210+
import resource
211+
212+
memory_bytes = memory_mb * 1024 * 1024
213+
# 设置虚拟内存限制 (address space)
214+
try:
215+
resource.setrlimit(resource.RLIMIT_AS, (memory_bytes, memory_bytes)) # type: ignore[attr-defined]
216+
except (ValueError, OSError) as e:
217+
_logger.debug("Failed to set RLIMIT_AS on macOS: %s", e)
218+
219+
# 设置数据段大小限制
220+
try:
221+
resource.setrlimit(resource.RLIMIT_DATA, (memory_bytes, memory_bytes)) # type: ignore[attr-defined]
222+
except (ValueError, OSError) as e:
223+
_logger.debug("Failed to set RLIMIT_DATA on macOS: %s", e)
224+
225+
196226
async def _force_terminate_process(
197227
process: asyncio.subprocess.Process,
198228
job: WinJobObject | None = None,
@@ -207,17 +237,17 @@ async def _force_terminate_process(
207237
if job:
208238
try:
209239
job.terminate()
210-
except Exception:
211-
pass
240+
except OSError as e:
241+
_logger.debug("Job object terminate failed: %s", e)
212242

213243
# 再尝试正常终止
214244
try:
215245
process.kill()
216246
except ProcessLookupError:
217247
# 进程已经不存在
218248
return
219-
except Exception:
220-
pass
249+
except OSError as e:
250+
_logger.debug("Failed to kill process: %s", e)
221251

222252
# 等待进程退出,设置超时防止卡住
223253
try:
@@ -226,8 +256,10 @@ async def _force_terminate_process(
226256
# 如果 2 秒后进程仍未退出,再次尝试强制终止
227257
try:
228258
process.kill()
229-
except Exception:
230-
pass
259+
except ProcessLookupError:
260+
return
261+
except OSError as e:
262+
_logger.debug("Second kill attempt failed: %s", e)
231263

232264

233265
async def _run_process(
@@ -257,17 +289,21 @@ async def _run_process(
257289

258290
job = WinJobObject(memory_mb=memory_mb, timeout_sec=timeout)
259291
job.assign_process(process.pid)
260-
except Exception:
261-
# Job Object 创建失败,但仍需确保进程可控
292+
except (OSError, RuntimeError) as e:
293+
# Job Object 创建/分配失败,记录日志并继续(进程仍可运行)
294+
_logger.warning("Failed to setup Windows Job Object: %s", e)
262295
job = None
263296
elif sys.platform == "darwin":
297+
# macOS: 使用 preexec_fn 设置资源限制
264298
process = await asyncio.create_subprocess_exec(
265299
*cmd,
266300
stdin=asyncio.subprocess.PIPE,
267301
stdout=asyncio.subprocess.PIPE,
268302
stderr=asyncio.subprocess.PIPE,
303+
preexec_fn=lambda: _set_macos_resource_limit(memory_mb),
269304
)
270305
else:
306+
# Linux: 使用 prlimit 设置资源限制
271307
memory_bytes = memory_mb * 1024 * 1024
272308
process = await asyncio.create_subprocess_exec(
273309
"prlimit",
@@ -313,7 +349,7 @@ async def _run_process(
313349
error=f"Binary not found or prlimit unavailable: {cmd[0]}",
314350
)
315351
except Exception as e:
316-
# 异常时确保进程被终止
352+
# 最后防线:捕获所有异常确保进程被终止,防止资源泄漏
317353
if process:
318354
await _force_terminate_process(process, job)
319355
return RunResult(

src/autocode_mcp/utils/win_job.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
_IS_WINDOWS = sys.platform == "win32"
1717

1818
if _IS_WINDOWS:
19+
import pywintypes
1920
import win32api
2021
import win32con
2122
import win32job
@@ -127,7 +128,9 @@ def assign_process(self, pid: int) -> None:
127128
win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, pid
128129
)
129130
win32job.AssignProcessToJobObject(self.job_handle, process_handle)
130-
except Exception as e:
131+
except pywintypes.error as e:
132+
raise RuntimeError(f"Failed to assign process {pid} to job object: {e}") from e
133+
except OSError as e:
131134
raise RuntimeError(f"Failed to assign process {pid} to job object: {e}") from e
132135

133136
def terminate(self) -> None:

tests/test_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def test_all_tools_registered():
7878
ProblemCreateTool,
7979
ProblemGenerateTestsTool,
8080
ProblemPackPolygonTool,
81+
SolutionAnalyzeTool,
8182
SolutionBuildTool,
8283
SolutionRunTool,
8384
StressTestRunTool,
@@ -90,6 +91,7 @@ def test_all_tools_registered():
9091
FileSaveTool(),
9192
SolutionBuildTool(),
9293
SolutionRunTool(),
94+
SolutionAnalyzeTool(),
9395
StressTestRunTool(),
9496
ProblemCreateTool(),
9597
ProblemGenerateTestsTool(),

0 commit comments

Comments
 (0)