cf6bc4c844
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>
225 lines
10 KiB
TypeScript
225 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { spawnSync } from "node:child_process";
|
||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { after, test, type TestContext } from "node:test";
|
||
import { Runner } from "../src/runner/index.ts";
|
||
import type { ProcessResult, RunDefinition } from "../src/vocab/index.ts";
|
||
|
||
// 用真实 Docker 测(任务书 05 第五部分):没有 Docker 时整个文件失败,不跳过。
|
||
// 镜像、容器名一律 inforsi-test- 开头的固定名字;本机还有别的项目在建删容器,只动自己前缀的。
|
||
|
||
const TASK_DIR = join(import.meta.dirname, "..", "tasks", "circle_packing");
|
||
const IMAGE_TAG = "inforsi-test-circle";
|
||
const MISSING_DIGEST = `sha256:${"0".repeat(64)}`;
|
||
|
||
const runner = new Runner(null);
|
||
const digest = await runner.buildImage(TASK_DIR, IMAGE_TAG);
|
||
after(() => docker(["rmi", IMAGE_TAG]));
|
||
|
||
function docker(args: string[]): { status: number | null; stdout: string; stderr: string } {
|
||
return spawnSync("docker", args, { encoding: "utf8" });
|
||
}
|
||
|
||
/** 删掉测试自己的容器;不存在不算错,别的错误照样报。 */
|
||
function forceRemove(name: string): void {
|
||
const removed = docker(["rm", "-f", name]);
|
||
if (removed.status !== 0 && !removed.stderr.includes("No such container")) throw new Error(removed.stderr);
|
||
}
|
||
|
||
/** 一个测试用的容器名:先清掉上次没删干净的同名容器,测试结束后再删。 */
|
||
function containerName(t: TestContext, suffix: string): string {
|
||
const name = `inforsi-test-${suffix}`;
|
||
forceRemove(name);
|
||
t.after(() => forceRemove(name));
|
||
return name;
|
||
}
|
||
|
||
function dirs(t: TestContext): { runDir: string; logDir: string } {
|
||
const root = mkdtempSync(join(tmpdir(), "inforsi-runner-"));
|
||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||
const runDir = join(root, "run");
|
||
const logDir = join(root, "log");
|
||
mkdirSync(runDir);
|
||
mkdirSync(logDir);
|
||
return { runDir, logDir };
|
||
}
|
||
|
||
function definition(command: string[], wallSeconds: number, image: string): RunDefinition {
|
||
return {
|
||
command,
|
||
workdir: "/work",
|
||
image: { digest: image, runtime: "runc" },
|
||
wallSeconds,
|
||
cores: 1,
|
||
inheritEnv: false,
|
||
env: {},
|
||
};
|
||
}
|
||
|
||
const python = (code: string): string[] => ["python3", "-c", code];
|
||
|
||
/** 起一个容器并等它结束,交回结果和日志目录。 */
|
||
async function runToEnd(t: TestContext, suffix: string, command: string[], wallSeconds: number) {
|
||
const name = containerName(t, suffix);
|
||
const { runDir, logDir } = dirs(t);
|
||
const outcome = await runner.start(name, definition(command, wallSeconds, digest), runDir);
|
||
assert.deepEqual(outcome, { started: true });
|
||
const result = await runner.wait(name, logDir);
|
||
return { name, result, logDir };
|
||
}
|
||
|
||
function value(result: ProcessResult, key: "cpuSeconds" | "wallSeconds" | "peakMemoryBytes"): number {
|
||
assert.notEqual(result.ending, "notStarted");
|
||
if (result.ending === "notStarted") throw new Error("unreachable");
|
||
const item = result[key];
|
||
assert.equal(item.status, "measured", `${key} 应当测到`);
|
||
if (item.status !== "measured") throw new Error("unreachable");
|
||
return item.value;
|
||
}
|
||
|
||
test("建镜像命中参考项目的缓存;查镜像:有的答有,没有的答没有", async () => {
|
||
assert.match(digest, /^sha256:7ed52793365c/);
|
||
assert.equal(await runner.hasImage(digest), true);
|
||
assert.equal(await runner.hasImage(MISSING_DIGEST), false);
|
||
});
|
||
|
||
test("正常退出:带退出码和三项资源用量,标准输出、标准错误存成两个文件", async (t) => {
|
||
const code = "import sys; print('out'); print('err', file=sys.stderr); sys.exit(3)";
|
||
const { result, logDir } = await runToEnd(t, "exit", python(code), 30);
|
||
assert.equal(result.ending, "exited");
|
||
assert.equal(result.ending === "exited" && result.exitCode, 3);
|
||
assert.ok(value(result, "cpuSeconds") > 0);
|
||
assert.ok(value(result, "wallSeconds") > 0 && value(result, "wallSeconds") < 30);
|
||
assert.ok(value(result, "peakMemoryBytes") > 0);
|
||
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "out\n");
|
||
assert.equal(readFileSync(join(logDir, "stderr.log"), "utf8"), "err\n");
|
||
});
|
||
|
||
test("超时:结局 timeout,CPU 秒接近时限,墙钟不短于时限", async (t) => {
|
||
const { result } = await runToEnd(t, "timeout", python("while True: pass"), 2);
|
||
assert.equal(result.ending, "timeout");
|
||
const cpu = value(result, "cpuSeconds");
|
||
assert.ok(cpu > 1.7 && cpu < 2.3, `CPU 秒 ${cpu}`);
|
||
assert.ok(value(result, "wallSeconds") >= 2);
|
||
});
|
||
|
||
test("候选伪造计量结果再死循环:伪造不成,结局仍是 timeout,资源用量是真实测到的;写出的文件归当前用户", async (t) => {
|
||
// 审阅时实测过的手法:先写一份假的 usage,再把目录改成只读,然后死循环到超时。
|
||
const code = [
|
||
"import os",
|
||
"for attempt in (lambda: open('/inforsi-record/usage', 'w').write('exited 0 1 1 1\\n'), lambda: os.chmod('/inforsi-record', 0o555)):",
|
||
" try:",
|
||
" attempt()",
|
||
" except OSError as error:",
|
||
" print(type(error).__name__, flush=True)",
|
||
"open('out.txt', 'w').write('x')",
|
||
"while True: pass",
|
||
].join("\n");
|
||
const name = containerName(t, "forge");
|
||
const { runDir, logDir } = dirs(t);
|
||
assert.deepEqual(await runner.start(name, definition(python(code), 2, digest), runDir), { started: true });
|
||
const result = await runner.wait(name, logDir);
|
||
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "PermissionError\nPermissionError\n");
|
||
assert.equal(result.ending, "timeout");
|
||
const cpu = value(result, "cpuSeconds");
|
||
t.diagnostic(`结局 ${result.ending},CPU 秒 ${cpu},墙钟秒 ${value(result, "wallSeconds")}`);
|
||
assert.ok(cpu > 1.7 && cpu < 2.3, `CPU 秒 ${cpu}`);
|
||
assert.ok(value(result, "wallSeconds") >= 2);
|
||
assert.equal(statSync(join(runDir, "out.txt")).uid, process.getuid?.());
|
||
});
|
||
|
||
test("命令自己以 124 或 137 退出,不判成超时", async (t) => {
|
||
const own124 = await runToEnd(t, "exit124", python("import sys; sys.exit(124)"), 30);
|
||
assert.equal(own124.result.ending, "exited");
|
||
assert.equal(own124.result.ending === "exited" && own124.result.exitCode, 124);
|
||
const own137 = await runToEnd(t, "exit137", python("import os; os.kill(os.getpid(), 9)"), 30);
|
||
assert.equal(own137.result.ending, "exited");
|
||
assert.equal(own137.result.ending === "exited" && own137.result.exitCode, 137);
|
||
});
|
||
|
||
test("从外面 docker kill:结局 cancelled,三项资源都测不到并写明原因", async (t) => {
|
||
const name = containerName(t, "killed");
|
||
const { runDir, logDir } = dirs(t);
|
||
const outcome = await runner.start(name, definition(python("import time; time.sleep(60)"), 120, digest), runDir);
|
||
assert.deepEqual(outcome, { started: true });
|
||
assert.equal(await runner.status(name), "running");
|
||
const killed = docker(["kill", name]);
|
||
assert.equal(killed.status, 0, killed.stderr);
|
||
const result = await runner.wait(name, logDir);
|
||
assert.equal(result.ending, "cancelled");
|
||
if (result.ending !== "cancelled") return;
|
||
for (const item of [result.cpuSeconds, result.wallSeconds, result.peakMemoryBytes]) {
|
||
assert.equal(item.status, "unmeasured");
|
||
assert.match(item.status === "unmeasured" ? item.reason : "", /从外面被结束/);
|
||
}
|
||
});
|
||
|
||
test("镜像编号不存在:没启动,原因是 Docker 的报错原文,也没留下容器", async (t) => {
|
||
const name = containerName(t, "noimage");
|
||
const { runDir } = dirs(t);
|
||
const outcome = await runner.start(name, definition(python("pass"), 30, MISSING_DIGEST), runDir);
|
||
assert.equal(outcome.started, false);
|
||
assert.match(outcome.started ? "" : outcome.reason, /No such image/);
|
||
assert.equal(await runner.status(name), "absent");
|
||
});
|
||
|
||
test("断网:容器里连不上外网", async (t) => {
|
||
const code = [
|
||
"import socket, sys",
|
||
"try:",
|
||
" socket.create_connection(('1.1.1.1', 53), timeout=3)",
|
||
"except OSError as error:",
|
||
" print(type(error).__name__); sys.exit(0)",
|
||
"sys.exit(1)",
|
||
].join("\n");
|
||
const { result } = await runToEnd(t, "network", python(code), 30);
|
||
assert.equal(result.ending === "exited" && result.exitCode, 0);
|
||
});
|
||
|
||
test("核数:容器里 os.cpu_count() 报 1", async (t) => {
|
||
const { logDir } = await runToEnd(t, "cores", python("import os; print(os.cpu_count())"), 30);
|
||
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "1\n");
|
||
});
|
||
|
||
test("查状态:没有这个容器、建了没启动、在跑、已结束、问不到", async (t) => {
|
||
assert.equal(await runner.status(containerName(t, "status-absent")), "absent");
|
||
|
||
const created = containerName(t, "status-created");
|
||
const create = docker(["create", "--name", created, digest, "true"]);
|
||
assert.equal(create.status, 0, create.stderr);
|
||
assert.equal(await runner.status(created), "created");
|
||
|
||
const name = containerName(t, "status-running");
|
||
const { runDir, logDir } = dirs(t);
|
||
await runner.start(name, definition(python("import time; time.sleep(3)"), 30, digest), runDir);
|
||
assert.equal(await runner.status(name), "running");
|
||
await runner.wait(name, logDir);
|
||
assert.equal(await runner.status(name), "exited");
|
||
|
||
const nowhere = new Runner("unix:///nonexistent/docker.sock");
|
||
assert.equal(await nowhere.status(name), "unreachable");
|
||
|
||
await runner.remove(name);
|
||
assert.equal(await runner.status(name), "absent");
|
||
});
|
||
|
||
test("等结束能接上另一个 Runner 实例启动的容器;已结束的容器再等一次,结果相同", async (t) => {
|
||
const name = containerName(t, "resume");
|
||
const { runDir, logDir } = dirs(t);
|
||
const starter = new Runner(null);
|
||
await starter.start(name, definition(python("import time; time.sleep(2); print('done')"), 30, digest), runDir);
|
||
const resumed = new Runner(null);
|
||
const first = await resumed.wait(name, logDir);
|
||
assert.equal(first.ending === "exited" && first.exitCode, 0);
|
||
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "done\n");
|
||
const again = await new Runner(null).wait(name, logDir);
|
||
assert.deepEqual(again, first);
|
||
});
|
||
|
||
test("容器名不符合 Docker 的要求时报错", async () => {
|
||
await assert.rejects(runner.status("inforsi test"), /不符合 Docker 的要求/);
|
||
await assert.rejects(runner.status("-x"), /不符合 Docker 的要求/);
|
||
});
|