mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-21 01:46:28 +08:00
judge: support generate testdata (#695)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
b83b2e792e
commit
55b4e1aaf0
27 files changed
+454
-68
No files matched your search
@@ -50,8 +50,8 @@ const testlibFile = {
|
||||
src: findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'),
|
||||
};
|
||||
|
||||
export async function compileWithTestlib(
|
||||
src: string, type: 'checker' | 'validator' | 'interactor',
|
||||
export async function compileLocalFile(
|
||||
src: string, type: 'checker' | 'validator' | 'interactor' | 'generator' | 'std',
|
||||
getLang, copyIn: CopyIn, withTestlib = true, next?: any,
|
||||
) {
|
||||
const s = src.replace('@', '.').split('.');
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
JudgeResultBody, ObjectId, RecordModel, SettingModel,
|
||||
StorageModel, SystemModel, TaskModel,
|
||||
} from 'hydrooj';
|
||||
import { end, next } from 'hydrooj/src/handler/judge';
|
||||
import { end, next, processJudgeFileCallback } from 'hydrooj/src/handler/judge';
|
||||
import { getConfig } from '../config';
|
||||
import { FormatError, SystemError } from '../error';
|
||||
import { Context } from '../judge/interface';
|
||||
@@ -46,6 +46,9 @@ const session = {
|
||||
if (doThrow) throw new SystemError('Unsupported language {0}.', [lang]);
|
||||
return null;
|
||||
},
|
||||
async postFile(target: string, filename: string, filepath: string) {
|
||||
return await processJudgeFileCallback(new ObjectId(target), filename, filepath);
|
||||
},
|
||||
async cacheOpen(source: string, files: any[]) {
|
||||
const filePath = path.join(getConfig('cache_dir'), source);
|
||||
await fs.ensureDir(filePath);
|
||||
@@ -84,9 +87,10 @@ export async function postInit(ctx) {
|
||||
logger.debug('Record not found: %o', t);
|
||||
return;
|
||||
}
|
||||
await (new JudgeTask(session, Object.assign(rdoc, t))).handle().catch(logger.error);
|
||||
await (new JudgeTask(session, JSON.parse(JSON.stringify(Object.assign(rdoc, t))))).handle().catch(logger.error);
|
||||
};
|
||||
const parallelism = Math.max(getConfig('parallelism'), 2);
|
||||
for (let i = 1; i < parallelism; i++) TaskModel.consume({ type: 'judge' }, handle);
|
||||
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
||||
TaskModel.consume({ type: 'generate' }, handle);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||
import type { JudgeResultBody } from 'hydrooj';
|
||||
import { getConfig } from '../config';
|
||||
import { FormatError, SystemError } from '../error';
|
||||
import { Session } from '../interface';
|
||||
import log from '../log';
|
||||
import { JudgeTask } from '../task';
|
||||
import { Lock } from '../utils';
|
||||
@@ -18,7 +19,7 @@ function removeNixPath(text: string) {
|
||||
return text.replace(/\/nix\/store\/[a-z0-9]{32}-/g, '/nix/');
|
||||
}
|
||||
|
||||
export default class Hydro {
|
||||
export default class Hydro implements Session {
|
||||
ws: WebSocket;
|
||||
language: Record<string, LangConfig>;
|
||||
|
||||
@@ -134,6 +135,10 @@ export default class Hydro {
|
||||
return target;
|
||||
}
|
||||
|
||||
async postFile(target: string, file: string) {
|
||||
await this.post('judge/upload', { target }).attach('file', fs.createReadStream(file));
|
||||
}
|
||||
|
||||
getLang(name: string, doThrow = true) {
|
||||
if (this.language[name]) return this.language[name];
|
||||
if (name === 'cpp' && this.language.cc) return this.language.cc;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import { NormalizedSubtask } from '@hydrooj/utils/lib/utils';
|
||||
import type { JudgeRequest as OrigJudgeRequest, ObjectId } from 'hydrooj';
|
||||
import { JudgeResultBody, ProblemConfigFile } from 'hydrooj';
|
||||
import { CopyInFile } from './sandbox';
|
||||
import type { JudgeTask } from './task';
|
||||
|
||||
export interface Execute {
|
||||
execute: string;
|
||||
@@ -16,3 +19,18 @@ export interface ParsedConfig extends Omit<ProblemConfigFile, 'time' | 'memory'
|
||||
memory: number;
|
||||
subtasks: NormalizedSubtask[];
|
||||
}
|
||||
|
||||
// replace ObjectId to string
|
||||
export type JudgeRequest = {
|
||||
[K in keyof OrigJudgeRequest]: OrigJudgeRequest[K] extends ObjectId ? string : OrigJudgeRequest[K];
|
||||
};
|
||||
|
||||
export interface Session {
|
||||
getLang: (name: string) => LangConfig;
|
||||
getNext: (task: JudgeTask) => NextFunction;
|
||||
getEnd: (task: JudgeTask) => NextFunction;
|
||||
cacheOpen: (source: string, files: any[], next?: NextFunction) => Promise<string>;
|
||||
fetchFile: (target: string) => Promise<string>;
|
||||
postFile: (target: string, filename: string, file: string) => Promise<void>;
|
||||
config: { detail: boolean, host?: string };
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export const judge = async (ctx: Context) => await runFlow(ctx, {
|
||||
compile: async () => {
|
||||
[ctx.execute, ctx.checker] = await Promise.all([
|
||||
ctx.compile(ctx.lang, ctx.code),
|
||||
ctx.compileWithTestlib('checker', ctx.config.checker, ctx.config.checker_type),
|
||||
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
|
||||
]);
|
||||
},
|
||||
judgeCase,
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||
import { SystemError } from 'hydrooj';
|
||||
import { CopyInFile, runQueued } from '../sandbox';
|
||||
import client from '../sandbox/client';
|
||||
import signals from '../signals';
|
||||
import { JudgeTask } from '../task';
|
||||
import { parseMemoryMB, parseTimeMS } from '../utils';
|
||||
|
||||
export const judge = async (ctx: JudgeTask) => {
|
||||
ctx.stat.judge = new Date();
|
||||
ctx.next({ status: STATUS.STATUS_COMPILING });
|
||||
if (!('content' in ctx.code)) throw new SystemError('Unsupported input');
|
||||
const [generator, std] = ctx.code.content.toString().split('\n').map((i) => i.trim());
|
||||
if (generator.includes('/') || generator === '..') throw new SystemError('Invalid input');
|
||||
if (std.includes('/') || std === '..') throw new SystemError('Invalid input');
|
||||
const [executeGenerator, executeStd] = await Promise.all([
|
||||
ctx.compileLocalFile('generator', generator),
|
||||
ctx.compileLocalFile('std', std),
|
||||
]);
|
||||
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
|
||||
let totalTime = 0;
|
||||
let totalMemory = 0;
|
||||
let totalStatus = 0;
|
||||
|
||||
async function runGenerator(i: number) {
|
||||
const res = await runQueued(
|
||||
`${executeGenerator.execute} ${i}`,
|
||||
{
|
||||
stdin: { content: ctx.input || '' },
|
||||
copyIn: executeGenerator.copyIn,
|
||||
copyOut: ['stdout'],
|
||||
time: parseTimeMS('2s'),
|
||||
memory: parseMemoryMB('256m'),
|
||||
},
|
||||
1,
|
||||
);
|
||||
const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.in`);
|
||||
ctx.clean.push(() => {
|
||||
if (fs.existsSync(tmp)) fs.removeSync(tmp);
|
||||
return res.fileIds['stdout'] ? client.deleteFile(res.fileIds['stdout']) : Promise.resolve();
|
||||
});
|
||||
const {
|
||||
code, signalled, time, memory, fileIds,
|
||||
} = res;
|
||||
let { status } = res;
|
||||
const message: string[] = [];
|
||||
if (time > parseTimeMS(ctx.config.time || '2s')) {
|
||||
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||
} else if (memory > parseMemoryMB('256m') * 1024) {
|
||||
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||
} else if (code) {
|
||||
status = STATUS.STATUS_RUNTIME_ERROR;
|
||||
if (code < 32 && signalled) message.push(`ExitCode: ${code} (${signals[code]})`);
|
||||
else message.push(`ExitCode: ${code}`);
|
||||
}
|
||||
totalTime += time;
|
||||
totalMemory = Math.max(memory, totalMemory);
|
||||
totalStatus = Math.max(status, totalStatus);
|
||||
if (status === STATUS.STATUS_ACCEPTED) {
|
||||
await client.getFile(fileIds['output'], tmp);
|
||||
await ctx.session.postFile(ctx.request.rid.toString(), `${i}.in`, tmp);
|
||||
}
|
||||
ctx.next({
|
||||
case: {
|
||||
id: i,
|
||||
subtaskId: 1,
|
||||
status,
|
||||
score: 0,
|
||||
time,
|
||||
memory,
|
||||
message: message.join('\n').substring(0, 102400),
|
||||
},
|
||||
});
|
||||
return status === STATUS.STATUS_ACCEPTED ? tmp : null;
|
||||
}
|
||||
async function runStd(i: number, stdin: CopyInFile) {
|
||||
const res = await runQueued(
|
||||
`${executeStd.execute} ${i}`,
|
||||
{
|
||||
stdin,
|
||||
copyIn: executeStd.copyIn,
|
||||
copyOut: ['stdout'],
|
||||
time: parseTimeMS('2s'),
|
||||
memory: parseMemoryMB('256m'),
|
||||
},
|
||||
1,
|
||||
);
|
||||
const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.out`);
|
||||
ctx.clean.push(() => {
|
||||
if (fs.existsSync(tmp)) fs.removeSync(tmp);
|
||||
return res.fileIds['stdout'] ? client.deleteFile(res.fileIds['stdout']) : Promise.resolve();
|
||||
});
|
||||
const {
|
||||
code, signalled, time, memory, fileIds,
|
||||
} = res;
|
||||
let { status } = res;
|
||||
const message: string[] = [];
|
||||
if (time > parseTimeMS(ctx.config.time || '2s')) {
|
||||
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||
} else if (memory > parseMemoryMB('256m') * 1024) {
|
||||
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||
} else if (code) {
|
||||
status = STATUS.STATUS_RUNTIME_ERROR;
|
||||
if (code < 32 && signalled) message.push(`ExitCode: ${code} (${signals[code]})`);
|
||||
else message.push(`ExitCode: ${code}`);
|
||||
}
|
||||
totalTime += time;
|
||||
totalMemory = Math.max(memory, totalMemory);
|
||||
totalStatus = Math.max(status, totalStatus);
|
||||
if (status === STATUS.STATUS_ACCEPTED) {
|
||||
await client.getFile(fileIds['output'], tmp);
|
||||
await ctx.session.postFile(ctx.request.rid.toString(), `${i}.out`, tmp);
|
||||
}
|
||||
ctx.next({
|
||||
case: {
|
||||
id: i,
|
||||
subtaskId: 2,
|
||||
status,
|
||||
score: 0,
|
||||
time,
|
||||
memory,
|
||||
message: message.join('\n').substring(0, 102400),
|
||||
},
|
||||
});
|
||||
return status === STATUS.STATUS_ACCEPTED;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const result = await runGenerator(i);
|
||||
if (result) await runStd(i, { src: result });
|
||||
}
|
||||
ctx.stat.done = new Date();
|
||||
if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) });
|
||||
ctx.end({
|
||||
status: totalStatus,
|
||||
score: totalStatus === STATUS.STATUS_ACCEPTED ? 100 : 0,
|
||||
time: totalTime,
|
||||
memory: totalMemory,
|
||||
});
|
||||
};
|
||||
@@ -11,8 +11,8 @@ export async function judge(ctx: Context) {
|
||||
ctx.next({ status: STATUS.STATUS_COMPILING, progress: 0 });
|
||||
const [execute, checker, validator, input] = await Promise.all([
|
||||
ctx.compile(ctx.lang, ctx.code),
|
||||
ctx.compileWithTestlib('checker', ctx.config.checker, ctx.config.checker_type),
|
||||
ctx.compileWithTestlib('validator', ctx.config.validator),
|
||||
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
|
||||
ctx.compileLocalFile('validator', ctx.config.validator),
|
||||
ctx.session.fetchFile(ctx.files.hack),
|
||||
]);
|
||||
ctx.clean.push(() => fs.unlink(input));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as def from './default';
|
||||
import * as generate from './generate';
|
||||
import * as hack from './hack';
|
||||
import * as interactive from './interactive';
|
||||
import { Context } from './interface';
|
||||
@@ -7,5 +8,5 @@ import * as run from './run';
|
||||
import * as submit_answer from './submit_answer';
|
||||
|
||||
export = {
|
||||
default: def, interactive, run, submit_answer, objective, hack,
|
||||
default: def, generate, interactive, run, submit_answer, objective, hack,
|
||||
} as Record<string, { judge(ctx: Context): Promise<void> }>;
|
||||
@@ -68,7 +68,7 @@ export const judge = async (ctx: Context) => await runFlow(ctx, {
|
||||
compile: async () => {
|
||||
[ctx.executeUser, ctx.executeInteractor] = await Promise.all([
|
||||
ctx.compile(ctx.lang, ctx.code),
|
||||
ctx.compileWithTestlib('interactor', ctx.config.interactor),
|
||||
ctx.compileLocalFile('interactor', ctx.config.interactor),
|
||||
]);
|
||||
},
|
||||
judgeCase,
|
||||
|
||||
@@ -67,7 +67,7 @@ function judgeCase(c: NormalizedCase) {
|
||||
export const judge = async (ctx: Context) => {
|
||||
await runFlow(ctx, {
|
||||
compile: async () => {
|
||||
ctx.checker = await ctx.compileWithTestlib('checker', ctx.config.checker, ctx.config.checker_type);
|
||||
ctx.checker = await ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type);
|
||||
},
|
||||
judgeCase,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs';
|
||||
import superagent from 'superagent';
|
||||
import { getConfig } from '../config';
|
||||
import { SandboxRequest, SandboxResult, SandboxVersion } from './interface';
|
||||
@@ -8,7 +9,15 @@ const client = new Proxy({
|
||||
run(req: SandboxRequest): Promise<SandboxResult[]> {
|
||||
return superagent.post(`${url}/run`).send(req).then((res) => res.body);
|
||||
},
|
||||
getFile(fileId: string): Promise<Buffer> {
|
||||
getFile(fileId: string, dest?: string): Promise<Buffer> {
|
||||
if (dest) {
|
||||
const w = fs.createWriteStream(dest);
|
||||
superagent.get(`${url}/file/${fileId}`).pipe(w);
|
||||
return new Promise((resolve, reject) => {
|
||||
w.on('finish', () => resolve(null));
|
||||
w.on('error', reject);
|
||||
});
|
||||
}
|
||||
return superagent.get(`${url}/file/${fileId}`).responseType('arraybuffer').then((res) => res.body);
|
||||
},
|
||||
deleteFile(fileId: string): Promise<void> {
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
import { basename } from 'path';
|
||||
import { basename, join } from 'path';
|
||||
import { fs } from '@hydrooj/utils';
|
||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||
import type {
|
||||
FileInfo, JudgeMeta, JudgeRequest, JudgeResultBody, TestCase,
|
||||
FileInfo, JudgeMeta, JudgeResultBody, TestCase,
|
||||
} from 'hydrooj';
|
||||
import readCases from './cases';
|
||||
import checkers from './checkers';
|
||||
import compile, { compileWithTestlib } from './compile';
|
||||
import compile, { compileLocalFile } from './compile';
|
||||
import { getConfig } from './config';
|
||||
import { CompileError, FormatError } from './error';
|
||||
import { Execute, NextFunction, ParsedConfig } from './interface';
|
||||
import {
|
||||
Execute, JudgeRequest, ParsedConfig, Session,
|
||||
} from './interface';
|
||||
import judge from './judge';
|
||||
import { Logger } from './log';
|
||||
import { CopyIn, CopyInFile, runQueued } from './sandbox';
|
||||
import { compilerText, md5 } from './utils';
|
||||
|
||||
interface Session {
|
||||
getLang: (name: string) => LangConfig;
|
||||
getNext: (task: JudgeTask) => NextFunction;
|
||||
getEnd: (task: JudgeTask) => NextFunction;
|
||||
cacheOpen: (source: string, files: any[], next?: NextFunction) => Promise<string>;
|
||||
fetchFile: (target: string) => Promise<string>;
|
||||
config: { detail: boolean, host?: string };
|
||||
}
|
||||
|
||||
const logger = new Logger('judge');
|
||||
|
||||
export class JudgeTask {
|
||||
@@ -121,14 +113,15 @@ export class JudgeTask {
|
||||
isSelfSubmission: this.meta.problemOwner === this.request.uid,
|
||||
key: md5(`${this.source}/${getConfig('secret')}`),
|
||||
lang: this.lang,
|
||||
langConfig: ['objective', 'submit_answer'].includes(this.request.config.type) ? null : this.session.getLang(this.lang),
|
||||
langConfig: (this.request.type === 'generate' || ['objective', 'submit_answer'].includes(this.request.config.type))
|
||||
? null : this.session.getLang(this.lang),
|
||||
},
|
||||
);
|
||||
this.stat.judge = new Date();
|
||||
const type = this.request.contest?.toString() === '000000000000000000000000' ? 'run'
|
||||
: this.files?.hack
|
||||
? 'hack'
|
||||
: this.config.type || 'default';
|
||||
: this.request.type === 'generate' ? 'generate'
|
||||
: this.files?.hack ? 'hack'
|
||||
: this.config.type || 'default';
|
||||
if (!judge[type]) throw new FormatError('Unrecognized problemType: {0}', [type]);
|
||||
await judge[type].judge(this);
|
||||
}
|
||||
@@ -142,17 +135,19 @@ export class JudgeTask {
|
||||
return result;
|
||||
}
|
||||
|
||||
async compileWithTestlib(type: 'interactor' | 'validator' | 'checker', file: string, checkerType?: string) {
|
||||
async compileLocalFile(type: 'interactor' | 'validator' | 'checker' | 'generator' | 'std', file: string, checkerType?: string) {
|
||||
if (type === 'checker' && ['default', 'strict'].includes(checkerType)) return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
||||
if (!checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
|
||||
const withTestlib = type !== 'checker' || checkerType === 'testlib';
|
||||
if (type === 'checker' && !checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
|
||||
const withTestlib = type !== 'std' && (type !== 'checker' || checkerType === 'testlib');
|
||||
const extra = type === 'std' ? this.config.user_extra_files : this.config.judge_extra_files;
|
||||
const copyIn = {
|
||||
user_code: this.code,
|
||||
...Object.fromEntries(
|
||||
(this.config.judge_extra_files || []).map((i) => [basename(i), { src: i }]),
|
||||
(extra || []).map((i) => [basename(i), { src: i }]),
|
||||
),
|
||||
} as CopyIn;
|
||||
const result = await compileWithTestlib(file, type, this.session.getLang, copyIn, withTestlib, this.next);
|
||||
if (!file.startsWith('/')) file = join(this.folder, file);
|
||||
const result = await compileLocalFile(file, type, this.session.getLang, copyIn, withTestlib, this.next);
|
||||
this.clean.push(result.clean);
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user