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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""候选代码的轻量静态检查(不执行候选的任何一行代码)。
|
||||
|
||||
实验员需要一个便宜的办法确认代码能跑;不给他这个办法,他就会自己跑一遍完整实验
|
||||
去验证。这个脚本就是那个便宜的办法:只做三件事,全部基于抽象语法树。
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user