judge: add multi-pass problem support (#1167)

This commit is contained in:
panda authored and GitHub committed 2026-06-06 18:28:33 +08:00
1 parent 01771a43e5
commit 809d1ac6ad
11 files changed
+166 -33

No files matched your search

+12
View File
@@ -32,6 +32,18 @@ function isValidConfig(config) {
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
throw new FormatError('You did not specify a checker.');
}
if (config.type === 'interactive' && !config.interactor) {
throw new FormatError('Interactive problems require an interactor.');
}
if (config.multi_pass && (!Number.isInteger(config.multi_pass) || config.multi_pass < 2 || config.multi_pass > 20)) {
throw new FormatError('Multi Pass must be between 2 and 20.');
}
if (config.multi_pass > 1 && !['default', 'interactive'].includes(config.type)) {
throw new FormatError('Multi Pass only supported on default and interactive problems.');
}
if (config.multi_pass > 1 && config.type === 'default' && !['testlib', 'kattis'].includes(config.checker_type)) {
throw new FormatError('Multi Pass on default problems requires a testlib or kattis checker.');
}
}
async function collectFiles(folder: string) {
+33 -3
View File
@@ -20,6 +20,7 @@ type Checker = (config: CheckConfig) => Promise<{
status: number;
score: number;
message: string;
nextPass?: { input: CopyInFile, state?: Record<string, CopyInFile> };
}>;
function parseDiffMsg(msg: string) {
@@ -236,7 +237,7 @@ const checkers: Record<string, Checker> = new Proxy({
},
async testlib(config) {
const { stderr, status, code } = await runQueued(`${config.execute} /w/in /w/user_out /w/answer`, {
const { stderr, status, code, fileIds } = await runQueued(`${config.execute} /w/in /w/user_out /w/answer`, {
copyIn: {
in: config.input,
user_out: config.user_stdout,
@@ -245,6 +246,7 @@ const checkers: Record<string, Checker> = new Proxy({
...config.copyIn,
},
env: config.env,
copyOutCached: ['nextpass.in?', 'state.txt?'],
});
if ([STATUS.STATUS_SYSTEM_ERROR, STATUS.STATUS_TIME_LIMIT_EXCEEDED, STATUS.STATUS_MEMORY_LIMIT_EXCEEDED].includes(status)) {
const message = {
@@ -265,12 +267,22 @@ const checkers: Record<string, Checker> = new Proxy({
message: `Checker exited with code ${code}`,
};
}
return parse(stderr, config.score, config.detail);
const result = parse(stderr, config.score, config.detail);
if (result.status === STATUS.STATUS_ACCEPTED && fileIds['nextpass.in']) {
return {
...result,
nextPass: {
input: { fileId: fileIds['nextpass.in'] },
state: fileIds['state.txt'] ? { 'state.txt': { fileId: fileIds['state.txt'] } } : undefined,
},
};
}
return result;
},
// https://www.kattis.com/problem-package-format/spec/2023-07-draft.html#output-validator
async kattis(config) {
const { files, code } = await runQueued(`${config.execute} input answer_file feedback_dir`, {
const { files, fileIds, code } = await runQueued(`${config.execute} input answer_file feedback_dir`, {
copyIn: {
input: config.input,
answer_file: config.output,
@@ -284,6 +296,10 @@ const checkers: Record<string, Checker> = new Proxy({
'feedback_dir/teammessage.txt?',
'feedback_dir/judgeerror.txt?',
],
copyOutCached: [
'feedback_dir/nextpass.in?',
'feedback_dir/state.txt?',
],
});
const status = code === 42
@@ -302,6 +318,20 @@ const checkers: Record<string, Checker> = new Proxy({
? files['feedback_dir/teammessage.txt'] || files['feedback_dir/judgemessage.txt'] || ''
: '';
if (status === STATUS.STATUS_ACCEPTED && fileIds['feedback_dir/nextpass.in']) {
return {
status,
score,
message,
nextPass: {
input: { fileId: fileIds['feedback_dir/nextpass.in'] },
state: fileIds['feedback_dir/state.txt']
? { 'feedback_dir/state.txt': { fileId: fileIds['feedback_dir/state.txt'] } }
: undefined,
},
};
}
return { status, score, message };
},
}, {
@@ -39,7 +39,7 @@ function judgeCase(c: NormalizedCase) {
});
}
execute[0].execute += managerArgs;
const res = await runPiped(execute, pipeMapping);
const res = await runPiped(execute, pipeMapping, undefined, `judgeCase[${c.id}]<${ctx.rid}>`);
const resManager = res[0];
let time = 0;
let memory = 0;
+32 -11
View File
@@ -3,16 +3,19 @@ import checkers from '../checkers';
import { runFlow } from '../flow';
import { runQueued } from '../sandbox';
import signals from '../signals';
import { Context, ContextSubTask } from './interface';
import { Context, ContextSubTask, MultiPassContext } from './interface';
function judgeCase(c: NormalizedCase) {
const mp: MultiPassContext = { i: 0 };
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
if (ctx.config.multi_pass && !mp.i) mp.i = 1;
await using res = await runQueued(
ctx.execute.execute,
{
stdin: { src: c.input },
copyIn: ctx.execute.copyIn,
stdin: mp.input ?? { src: c.input },
copyIn: { ...ctx.execute.copyIn, ...mp.state },
filename: ctx.config.filename,
time: c.time,
memory: c.memory,
@@ -20,25 +23,29 @@ function judgeCase(c: NormalizedCase) {
addressSpaceLimit: address_space_limit,
processLimit: process_limit,
},
`judgeCase[${c.id}]<${ctx.rid}>`,
`judgeCase[${c.id}]${mp.i ? `[pass=${mp.i}]` : ''}<${ctx.rid}>`,
);
const {
code, signalled, time, memory, fileIds,
} = res;
let { status } = res;
let message: any = '';
let message: any = mp.i ? `[Pass ${mp.i}] ` : '';
let score = 0;
let nextPass: any;
if (status === STATUS.STATUS_ACCEPTED) {
if (time > c.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > c.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
} else {
({ status, score, message } = await checkers[ctx.config.checker_type]({
const resChecker = await checkers[ctx.config.checker_type]({
execute: ctx.checker.execute,
copyIn: ctx.checker.copyIn || {},
copyIn: {
...ctx.checker.copyIn,
...mp.state ?? {},
},
code: ctx.code,
input: { src: c.input },
input: mp.input ?? { src: c.input },
output: { src: c.output },
user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' },
user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' },
@@ -49,16 +56,30 @@ function judgeCase(c: NormalizedCase) {
HYDRO_TESTCASE: c.id.toString(),
HYDRO_TIME_USAGE: time.toString(),
HYDRO_MEMORY_USAGE: Math.floor(memory / 1024).toString(),
...(mp.i ? { HYDRO_MULTI_PASS: mp.i.toString() } : {}),
},
}));
});
({ status, score, nextPass } = resChecker);
message += resChecker.message;
}
} else if (status === STATUS.STATUS_RUNTIME_ERROR && code && ctx.config.detail === 'full') {
if (code < 32 && signalled) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] };
else message = { message: `${message}Your program returned {0}.`, params: [code] };
}
if (nextPass) {
if (mp.i < ctx.config.multi_pass) {
mp.input = nextPass.input;
mp.state = nextPass.state ?? undefined;
mp.i++;
return await runner(ctx, ctxSubtask, runner);
}
status = STATUS.STATUS_SYSTEM_ERROR;
score = 0;
message = { message: 'Exceeded maximum number of passes ({0}).', params: [ctx.config.multi_pass] };
}
if (runner && ctx.rerun && c.time <= 5000 && status === STATUS.STATUS_TIME_LIMIT_EXCEEDED) {
ctx.rerun--;
return await runner(ctx, ctxSubtask);
return await runner(ctx, ctxSubtask, runner);
}
if (!ctx.request.rejudged && !ctx.analysis && [STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) {
ctx.analysis = true;
+31 -9
View File
@@ -3,17 +3,20 @@ import { runFlow } from '../flow';
import { runPiped } from '../sandbox';
import signals from '../signals';
import { parse } from '../testlib';
import { Context, ContextSubTask } from './interface';
import { Context, ContextSubTask, MultiPassContext } from './interface';
function judgeCase(c: NormalizedCase) {
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
const mp: MultiPassContext = { i: 0 };
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
if (ctx.config.multi_pass && !mp.i) mp.i = 1;
const [{
code, signalled, time, memory,
}, resInteractor] = await runPiped([
{
execute: ctx.executeUser.execute,
copyIn: ctx.executeUser.copyIn,
copyIn: { ...ctx.executeUser.copyIn, ...mp.state },
time: c.time,
memory: c.memory,
addressSpaceLimit: address_space_limit,
@@ -22,23 +25,29 @@ function judgeCase(c: NormalizedCase) {
{
execute: `${ctx.executeInteractor.execute} /w/in /w/tout /w/out`,
copyIn: {
in: c.input ? { src: c.input } : { content: '' },
in: mp.input ?? { src: c.input },
out: c.output ? { src: c.output } : { content: '' },
...ctx.executeInteractor.copyIn,
...mp.state ?? {},
},
time: c.time * 2,
memory: c.memory * 2,
copyOut: ['/w/tout?'],
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
copyOutCached: ['nextpass.in?', 'state.txt?'],
env: {
...ctx.env,
HYDRO_TESTCASE: c.id.toString(),
...(mp.i ? { HYDRO_MULTI_PASS: mp.i.toString() } : {}),
},
},
], [
{ in: { index: 0, fd: 1 }, out: { index: 1, fd: 0 }, name: 'userToInteractor' },
{ in: { index: 1, fd: 1 }, out: { index: 0, fd: 0 }, name: 'interactorToUser' },
]);
], undefined, `judgeCase[${c.id}]${mp.i ? `[pass=${mp.i}]` : ''}<${ctx.rid}>`);
// TODO handle tout (maybe pass to checker?)
let status: number;
let score = 0;
let message: any = '';
let message: any = mp.i ? `[Pass ${mp.i}] ` : '';
if (time > c.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > c.memory * 1024) {
@@ -46,13 +55,26 @@ function judgeCase(c: NormalizedCase) {
} else if (ctx.config.detail === 'full' && ((code && code !== 13/* Broken Pipe */) || (code === 13 && !resInteractor.code))) {
status = STATUS.STATUS_RUNTIME_ERROR;
if (code < 32 && signalled) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] };
else message = { message: `${message}Your program returned {0}.`, params: [code] };
} else {
const result = parse(resInteractor.stderr, c.score, ctx.config.detail);
status = result.status;
score = result.score;
message = result.message;
message += result.message;
if (resInteractor.code && !(resInteractor.stderr || '').trim().length) message += ` (Interactor exited with code ${resInteractor.code})`;
if (status === STATUS.STATUS_ACCEPTED) {
if (resInteractor.fileIds['nextpass.in']) {
if (mp.i < ctx.config.multi_pass) {
mp.input = { fileId: resInteractor.fileIds['nextpass.in'] };
mp.state = resInteractor.fileIds['state.txt'] ? { 'state.txt': { fileId: resInteractor.fileIds['state.txt'] } } : undefined;
mp.i++;
return await runner!(ctx, ctxSubtask, runner);
}
status = STATUS.STATUS_SYSTEM_ERROR;
score = 0;
message = { message: 'Exceeded maximum number of passes ({0}).', params: [ctx.config.multi_pass] };
}
}
}
return {
id: c.id,
@@ -1,6 +1,7 @@
import PQueue from 'p-queue';
import { NormalizedSubtask } from '@hydrooj/common';
import { Execute } from '../interface';
import { CopyInFile } from '../sandbox';
import { JudgeTask } from '../task';
export type Context = JudgeTask & RuntimeContext;
@@ -29,3 +30,9 @@ export interface ContextSubTask {
score: number;
status: number;
}
export interface MultiPassContext {
i: number;
input?: CopyInFile;
state?: Record<string, CopyInFile>;
}