Files
InfoRSI/tasks/circle_packing/check-candidate.py
T
iomgaa cf6bc4c844 feat: 任务书 05,runner 与 tasks 两个模块
runner 用 Docker 跑一条命令并交回结局和资源用量;tasks 读 task.json、准备运行目录、给出正式运行/校验器/轻量检查的运行定义,并从原始产物独立判分。任务目录从参考项目搬到 tasks/circle_packing/,新增 task.json;vocab 的 VERDICTS 删掉"判不了",ProcessResult 的 cancelled 改为"容器从外面被杀了"。

全新上下文审阅后的两处修复:计量 shell 以容器内 root 运行,命令经 setpriv 降权,结果写在容器自己的文件层、结束后 docker cp 取出,并加 no-new-privileges,候选无法伪造计量结果把超时记成正常退出;harness 开跑前删掉旧的 solution.json、metrics.json,旧产物不会被当成本次的值。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:55:47 -04:00

101 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""候选代码的轻量静态检查(不执行候选的任何一行代码)。
实验员需要一个便宜的办法确认代码能跑;不给他这个办法,他就会自己跑一遍完整实验
去验证。这个脚本就是那个便宜的办法:只做三件事,全部基于抽象语法树。
1. 入口文件在不在
2. 候选目录下每个 ``*.py`` 能不能 ``ast.parse``
3. 只对入口文件:顶层恰好有一个叫 ``run_packing`` 的函数,且零个必填参数
用法:``python -B check-candidate.py <候选目录> <入口文件相对候选目录的路径> <结果文件>``。
结果写进 <结果文件>,一个 JSON 对象:``{"valid": bool, "reasons": [...]}``。
对候选代码的每一种意见都走"退出码 0 加 reasons";非零退出说明这个脚本自己没跑成。
两条调用约定,改的时候不要破坏:
- **本文件不能放进候选目录。** 第 2 步从候选目录往下扫 ``*.py``,
脚本自己在里面就会把自己也扫进去。宿主把它放在候选目录的上一层。
- **只用标准库,``-B`` 要保留。** 不写 ``__pycache__``:检查本身不该往候选目录里添文件。
"""
import ast
import json
import pathlib
import sys
ENTRY_FUNCTION = "run_packing"
# 两种定义形式都要认,否则 `async def run_packing()` 会被判成"不存在"。
FUNCTION_NODES = (ast.FunctionDef, ast.AsyncFunctionDef)
def has_zero_required_arguments(node) -> bool:
"""判断函数是不是可以零参数调用。
位置参数(含仅位置参数)里没有默认值的那些必须为 0 个;
仅关键字参数里 kw_defaults 为 None 的表示没有默认值,一个都不能有。
"""
positional = len(node.args.posonlyargs) + len(node.args.args)
required_positional = positional - len(node.args.defaults)
keyword_without_default = any(
default is None for default in node.args.kw_defaults
)
return required_positional <= 0 and not keyword_without_default
def main() -> int:
if len(sys.argv) != 4:
print(
"用法: check-candidate.py <候选目录> <入口文件相对路径> <结果文件>",
file=sys.stderr,
)
return 2
root = pathlib.Path(sys.argv[1])
entry_point = sys.argv[2]
reasons: list[str] = []
if not (root / entry_point).is_file():
reasons.append(f"入口文件不存在: {entry_point}")
# 第 3 步挂在第 2 步的 rglob("*.py") 上:入口文件名不以 .py 结尾,
# 循环扫不到它,入口函数那条判定会整个被跳过,却照样报通过。
if not entry_point.endswith(".py"):
reasons.append(
f"入口文件必须以 .py 结尾,否则入口函数检查扫不到它: {entry_point}"
)
for path in root.rglob("*.py"):
relative = str(path.relative_to(root))
try:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=relative)
except (SyntaxError, ValueError) as error:
# SyntaxError:语法错;ValueError:含空字节或不是 UTF-8(UnicodeDecodeError 是它的子类)。
reasons.append(f"{relative} 解析失败: {error}")
continue
if relative != entry_point:
continue
candidates = [
node
for node in tree.body
if isinstance(node, FUNCTION_NODES) and node.name == ENTRY_FUNCTION
]
if len(candidates) != 1:
reasons.append(
f"{entry_point} 顶层的 {ENTRY_FUNCTION}() 应当恰好有一个定义,"
f"实际有 {len(candidates)} 个"
)
elif not has_zero_required_arguments(candidates[0]):
reasons.append(
f"{entry_point} 的 {ENTRY_FUNCTION}() 必须能零参数调用,"
f"当前有必填参数"
)
with open(sys.argv[3], "w", encoding="utf-8") as handle:
json.dump({"valid": not reasons, "reasons": reasons}, handle)
return 0
if __name__ == "__main__":
sys.exit(main())