mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
judge: add type: submit_answer support
This commit is contained in:
1 parent
476457ac23
commit
20a1542bc3
9 files changed
+210
-46
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "2.16.0",
|
"version": "2.17.0",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ async function readAutoCases(folder: string, { next }, cfg, rst) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isValidConfig(config) {
|
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)) {
|
if (config.count > (getConfig('testcases_max') || 100)) {
|
||||||
throw new FormatError('Too many testcases. Cancelled.');
|
throw new FormatError('Too many testcases. Cancelled.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as def from './default';
|
import * as def from './default';
|
||||||
import * as interactive from './interactive';
|
import * as interactive from './interactive';
|
||||||
import { Context } from './interface';
|
import { Context } from './interface';
|
||||||
|
import * as objective from './objective';
|
||||||
import * as run from './run';
|
import * as run from './run';
|
||||||
import * as submit_answer from './submit_answer';
|
import * as submit_answer from './submit_answer';
|
||||||
|
|
||||||
export = {
|
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> }>;
|
} 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,41 +1,156 @@
|
|||||||
import { readFile } from 'fs-extra';
|
import Queue from 'p-queue';
|
||||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
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
|
const Score = {
|
||||||
// NOT RECOMMENDED TO USE
|
sum: (a: number, b: number) => (a + b),
|
||||||
export async function judge({
|
max: Math.max,
|
||||||
next, end, config, code,
|
min: Math.min,
|
||||||
}: Context) {
|
};
|
||||||
next({ status: STATUS.STATUS_JUDGING, progress: 0 });
|
|
||||||
const answer = ('src' in code)
|
function judgeCase(c: Case, sid: string) {
|
||||||
? await readFile(code.src, 'utf-8')
|
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
|
||||||
: ('content' in code)
|
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|
||||||
? code.content.toString().replace(/\r/g, '')
|
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
|
||||||
: '';
|
|| ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length)
|
||||||
const outputs = answer.split('\n');
|
) {
|
||||||
let totalScore = 0;
|
ctx.next({
|
||||||
let totalStatus = 0;
|
case: {
|
||||||
for (const i in config.outputs) {
|
status: STATUS.STATUS_CANCELED,
|
||||||
const c = config.outputs[i];
|
score: 0,
|
||||||
outputs[i] = outputs[i] || '';
|
time_ms: 0,
|
||||||
let status = STATUS.STATUS_WRONG_ANSWER;
|
memory_kb: 0,
|
||||||
let score = 0;
|
message: '',
|
||||||
if (outputs[i].trim() === (c.output || c[0]).trim()) {
|
},
|
||||||
score = c.score || c[1];
|
addProgress: 100 / ctx.config.count,
|
||||||
status = STATUS.STATUS_ACCEPTED;
|
}, c.id);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
totalScore += score;
|
|
||||||
totalStatus = Math.max(status, totalStatus);
|
const res = await run(
|
||||||
next({
|
ctx.config.subType === 'multi'
|
||||||
status: totalStatus,
|
? '/bin/bash -c "unzip foo.zip >/dev/null && read line && cat $line"'
|
||||||
progress: (100 * (+i + 1)) / config.outputs.length,
|
: '/bin/cat foo.zip',
|
||||||
case: {
|
{
|
||||||
status, score, time_ms: 0, memory_kb: 0, message: '',
|
stdin: c.input,
|
||||||
|
copyIn: { 'foo.zip': ctx.code },
|
||||||
|
copyOutCached: [],
|
||||||
|
time: 1000,
|
||||||
|
memory: 128,
|
||||||
|
cacheStdoutAndStderr: true,
|
||||||
},
|
},
|
||||||
}, +i + 1);
|
);
|
||||||
}
|
const { code } = res;
|
||||||
return end({
|
let { status } = res;
|
||||||
status: totalStatus, score: totalScore, time_ms: 0, memory_kb: 0,
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
"graphql": "^16.3.0",
|
"graphql": "^16.3.0",
|
||||||
"graphql-scalars": "1.17.0",
|
"graphql-scalars": "1.17.0",
|
||||||
|
"isbinaryfile": "^5.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"koa": "^2.13.4",
|
"koa": "^2.13.4",
|
||||||
"koa-body": "^4.2.0",
|
"koa-body": "^4.2.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import AdmZip from 'adm-zip';
|
import AdmZip from 'adm-zip';
|
||||||
import { statSync } from 'fs-extra';
|
import { readFile, statSync } from 'fs-extra';
|
||||||
|
import { isBinaryFile } from 'isbinaryfile';
|
||||||
import { intersection, isSafeInteger } from 'lodash';
|
import { intersection, isSafeInteger } from 'lodash';
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
@@ -481,10 +482,18 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
const file = this.request.files?.file;
|
const file = this.request.files?.file;
|
||||||
if (!file) throw new ValidationError('code');
|
if (!file) throw new ValidationError('code');
|
||||||
if (file.size > 128 * 1024 * 1024) throw new ValidationError('file');
|
const config = this.response.body.pdoc.config;
|
||||||
const id = nanoid();
|
if (typeof config !== 'object' || config === null) throw new BadRequestError('Incorrect problem config');
|
||||||
await storage.put(`submission/${this.user._id}/${id}`, file.path);
|
const sizeLimit = config.type === 'submit_answer' ? 128 * 1024 * 1024 : 65535;
|
||||||
code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.name}`;
|
if (file.size > sizeLimit) throw new ValidationError('file');
|
||||||
|
if (file.size > 65535 || await isBinaryFile(file.path, file.size)) {
|
||||||
|
const id = nanoid();
|
||||||
|
await storage.put(`submission/${this.user._id}/${id}`, file.path);
|
||||||
|
code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.name}`;
|
||||||
|
} else {
|
||||||
|
// TODO auto detect & convert encoding
|
||||||
|
code = await readFile(file.path, 'utf-8');
|
||||||
|
}
|
||||||
} else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code');
|
} else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code');
|
||||||
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
||||||
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, pretest ? input : tid, tid && !pretest);
|
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, pretest ? input : tid, tid && !pretest);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
|||||||
}
|
}
|
||||||
if (cfg.time) config.time = parseTimeMS(cfg.time);
|
if (cfg.time) config.time = parseTimeMS(cfg.time);
|
||||||
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
||||||
if ((config.type === 'objective' || cfg.type === 'submit_answer') && !cfg.outputs?.length) throw new Error('outputs config not found');
|
if (config.type === 'objective' && !cfg.outputs?.length) throw new Error('outputs config not found');
|
||||||
return Object.assign(cfg, config);
|
return Object.assign(cfg, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/utils",
|
"name": "@hydrooj/utils",
|
||||||
"version": "1.1.16",
|
"version": "1.1.17",
|
||||||
"description": "hydrooj utils",
|
"description": "hydrooj utils",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
Reference in new issue
Block a user