judge: add type: submit_answer support

This commit is contained in:
undefined committed 2022-04-05 21:22:57 +08:00
1 parent 476457ac23
commit 20a1542bc3
9 files changed
+210 -46

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js",
"version": "2.16.0",
"version": "2.17.0",
"main": "package.json",
"author": "undefined <i@undefined.moe>",
"repository": "https://github.com/hydro-dev/Hydro.git",
-1
View File
@@ -185,7 +185,6 @@ async function readAutoCases(folder: string, { next }, cfg, rst) {
}
function isValidConfig(config) {
if (config.type === 'submit_answer' && !config.outputs.length) throw new FormatError('Problem data not found.');
if (config.count > (getConfig('testcases_max') || 100)) {
throw new FormatError('Too many testcases. Cancelled.');
}
+2 -1
View File
@@ -1,9 +1,10 @@
import * as def from './default';
import * as interactive from './interactive';
import { Context } from './interface';
import * as objective from './objective';
import * as run from './run';
import * as submit_answer from './submit_answer';
export = {
default: def, interactive, run, submit_answer, objective: submit_answer,
default: def, interactive, run, submit_answer, objective,
} as Record<string, { judge(ctx: Context, startPromise?: Promise<any>): Promise<void> }>;
@@ -0,0 +1,39 @@
import { readFile } from 'fs-extra';
import { STATUS } from '@hydrooj/utils/lib/status';
import { Context } from './interface';
export async function judge({
next, end, config, code,
}: Context) {
next({ status: STATUS.STATUS_JUDGING, progress: 0 });
const answer = ('src' in code)
? await readFile(code.src, 'utf-8')
: ('content' in code)
? code.content.toString().replace(/\r/g, '')
: '';
const outputs = answer.split('\n');
let totalScore = 0;
let totalStatus = 0;
for (const i in config.outputs) {
const c = config.outputs[i];
outputs[i] = outputs[i] || '';
let status = STATUS.STATUS_WRONG_ANSWER;
let score = 0;
if (outputs[i].trim() === (c.output || c[0]).trim()) {
score = c.score || c[1];
status = STATUS.STATUS_ACCEPTED;
}
totalScore += score;
totalStatus = Math.max(status, totalStatus);
next({
status: totalStatus,
progress: (100 * (+i + 1)) / config.outputs.length,
case: {
status, score, time_ms: 0, memory_kb: 0, message: '',
},
}, +i + 1);
}
return end({
status: totalStatus, score: totalScore, time_ms: 0, memory_kb: 0,
});
}
+151 -36
View File
@@ -1,41 +1,156 @@
import { readFile } from 'fs-extra';
import Queue from 'p-queue';
import { STATUS } from '@hydrooj/utils/lib/status';
import { Context } from './interface';
import { check, compileChecker } from '../check';
import { getConfig } from '../config';
import { FormatError } from '../error';
import { del, run } from '../sandbox';
import { parseFilename } from '../utils';
import {
Case, Context, ContextSubTask, SubTask,
} from './interface';
// TODO drop this format in next major version
// NOT RECOMMENDED TO USE
export async function judge({
next, end, config, code,
}: Context) {
next({ status: STATUS.STATUS_JUDGING, progress: 0 });
const answer = ('src' in code)
? await readFile(code.src, 'utf-8')
: ('content' in code)
? code.content.toString().replace(/\r/g, '')
: '';
const outputs = answer.split('\n');
let totalScore = 0;
let totalStatus = 0;
for (const i in config.outputs) {
const c = config.outputs[i];
outputs[i] = outputs[i] || '';
let status = STATUS.STATUS_WRONG_ANSWER;
let score = 0;
if (outputs[i].trim() === (c.output || c[0]).trim()) {
score = c.score || c[1];
status = STATUS.STATUS_ACCEPTED;
const Score = {
sum: (a: number, b: number) => (a + b),
max: Math.max,
min: Math.min,
};
function judgeCase(c: Case, sid: string) {
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
|| ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length)
) {
ctx.next({
case: {
status: STATUS.STATUS_CANCELED,
score: 0,
time_ms: 0,
memory_kb: 0,
message: '',
},
addProgress: 100 / ctx.config.count,
}, c.id);
return;
}
totalScore += score;
totalStatus = Math.max(status, totalStatus);
next({
status: totalStatus,
progress: (100 * (+i + 1)) / config.outputs.length,
case: {
status, score, time_ms: 0, memory_kb: 0, message: '',
const res = await run(
ctx.config.subType === 'multi'
? '/bin/bash -c "unzip foo.zip >/dev/null && read line && cat $line"'
: '/bin/cat foo.zip',
{
stdin: c.input,
copyIn: { 'foo.zip': ctx.code },
copyOutCached: [],
time: 1000,
memory: 128,
cacheStdoutAndStderr: true,
},
}, +i + 1);
}
return end({
status: totalStatus, score: totalScore, time_ms: 0, memory_kb: 0,
});
);
const { code } = res;
let { status } = res;
let message: any = '';
let score = 0;
if (status === STATUS.STATUS_ACCEPTED) {
[status, score, message] = await check({
execute: ctx.checker.execute,
copyIn: ctx.checker.copyIn || {},
input: { src: c.input },
output: { src: c.output },
user_stdout: { fileId: res.fileIds['stdout'] },
user_stderr: { fileId: res.fileIds['stderr'] },
checker_type: ctx.config.checker_type,
score: ctxSubtask.subtask.score,
detail: ctx.config.detail ?? true,
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
});
} else if (status === STATUS.STATUS_RUNTIME_ERROR && code) {
message = { message: 'Unzip failed or file not found.' };
}
await Promise.all(
Object.values(res.fileIds).map((id) => del(id)),
).catch(() => { /* Ignore file doesn't exist */ });
ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score);
ctxSubtask.status = Math.max(ctxSubtask.status, status);
if (ctxSubtask.status > STATUS.STATUS_ACCEPTED) ctx.failed[sid] = true;
ctx.next({
case: {
status,
score: 0,
time_ms: 0,
memory_kb: 0,
message,
},
addProgress: 100 / ctx.config.count,
}, c.id);
};
}
function judgeSubtask(subtask: SubTask, sid: string) {
return async (ctx: Context) => {
subtask.type = subtask.type || 'min';
const ctxSubtask = {
subtask,
status: 0,
score: subtask.type === 'min'
? subtask.score
: 0,
};
const cases = [];
for (const cid in subtask.cases) {
const runner = judgeCase(subtask.cases[cid], sid);
cases.push(ctx.queue.add(() => runner(ctx, ctxSubtask)));
}
await Promise.all(cases).catch((e) => {
ctx.errored = true;
throw e;
});
ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status);
return ctxSubtask.score;
};
}
export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
startPromise.then(() => ctx.next({ status: STATUS.STATUS_COMPILING }));
ctx.checker = await (() => {
if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) {
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
}
const copyIn = { user_code: ctx.code };
for (const file of ctx.config.judge_extra_files) {
copyIn[parseFilename(file)] = { src: file };
}
return compileChecker(
ctx.getLang,
ctx.config.checker_type,
ctx.config.checker,
copyIn,
);
})();
ctx.clean.push(ctx.checker.clean);
await startPromise;
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
const tasks = [];
ctx.total_status = 0;
ctx.total_score = 0;
ctx.queue = new Queue({ concurrency: getConfig('parallelism') });
ctx.failed = {};
for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid)(ctx));
const scores = await Promise.all(tasks);
for (const sid in ctx.config.subtasks) {
let effective = true;
for (const required of ctx.config.subtasks[sid].if || []) {
if (ctx.failed[required.toString()]) effective = false;
}
if (effective) ctx.total_score += scores[sid];
}
ctx.stat.done = new Date();
if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) });
ctx.end({
status: ctx.total_status,
score: ctx.total_score,
time_ms: 0,
memory_kb: 0,
});
};