mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +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;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import { omit } from 'lodash';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import {
|
||||
FileLimitExceededError, ForbiddenError, ProblemIsReferencedError, ValidationError,
|
||||
} from '../error';
|
||||
import {
|
||||
JudgeResultBody, ProblemConfigFile, RecordDoc, Task, TestCase,
|
||||
} from '../interface';
|
||||
import { Logger } from '../logger';
|
||||
import * as builtin from '../model/builtin';
|
||||
import { STATUS } from '../model/builtin';
|
||||
import { PERM, STATUS } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import domain from '../model/domain';
|
||||
import problem from '../model/problem';
|
||||
import record from '../model/record';
|
||||
import * as setting from '../model/setting';
|
||||
import storage from '../model/storage';
|
||||
import * as system from '../model/system';
|
||||
import task from '../model/task';
|
||||
import user from '../model/user';
|
||||
import * as bus from '../service/bus';
|
||||
import { updateJudge } from '../service/monitor';
|
||||
import {
|
||||
@@ -189,11 +196,46 @@ export class SubmissionDataDownloadHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
export async function processJudgeFileCallback(rid: ObjectId, filename: string, filePath: string) {
|
||||
const rdoc = await record.get(rid);
|
||||
const [pdoc, udoc] = await Promise.all([
|
||||
problem.get(rdoc.domainId, rdoc.pid),
|
||||
user.getById(rdoc.domainId, rdoc.uid),
|
||||
]);
|
||||
if (!udoc.own(pdoc, PERM.PERM_EDIT_PROBLEM_SELF) && !udoc.hasPerm(PERM.PERM_EDIT_PROBLEM)) throw new ForbiddenError();
|
||||
if (pdoc.reference) throw new ProblemIsReferencedError('edit files');
|
||||
const stat = await fs.stat(filePath);
|
||||
if ((pdoc.data?.length || 0)
|
||||
+ (pdoc.additional_file?.length || 0)
|
||||
>= system.get('limit.problem_files_max')) {
|
||||
throw new FileLimitExceededError('count');
|
||||
}
|
||||
const size = Math.sum(
|
||||
(pdoc.data || []).map((i) => i.size),
|
||||
(pdoc.additional_file || []).map((i) => i.size),
|
||||
stat.size,
|
||||
);
|
||||
if (size >= system.get('limit.problem_files_max_size')) {
|
||||
throw new FileLimitExceededError('size');
|
||||
}
|
||||
await problem.addTestdata(pdoc.domainId, pdoc.docId, sanitize(filename), fs.createReadStream(filePath), udoc._id);
|
||||
}
|
||||
|
||||
export class JudgeFileUpdateHandler extends Handler {
|
||||
@post('rid', Types.ObjectId)
|
||||
@post('name', Types.Filename)
|
||||
async post(domainId: string, rid: ObjectId, filename: string) {
|
||||
if (!this.request.files.file) throw new ValidationError('file');
|
||||
await processJudgeFileCallback(rid, filename, this.request.files.file.filepath);
|
||||
this.response.body = { ok: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
class JudgeConnectionHandler extends ConnectionHandler {
|
||||
category = '#judge';
|
||||
processing: Task[] = [];
|
||||
closed = false;
|
||||
query: any = { type: 'judge' };
|
||||
query: any = { type: { $in: ['judge', 'generate'] } };
|
||||
rdocs: Record<string, RecordDoc> = {};
|
||||
ip: string;
|
||||
concurrency = 1;
|
||||
@@ -282,6 +324,7 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
||||
|
||||
export async function apply(ctx) {
|
||||
ctx.Route('judge_files_download', '/judge/files', JudgeFilesDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
||||
ctx.Route('judge_files_upload', '/judge/upload', JudgeFileUpdateHandler, builtin.PRIV.PRIV_JUDGE);
|
||||
ctx.Route('judge_submission_download', '/judge/code', SubmissionDataDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
||||
ctx.Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
|
||||
}
|
||||
|
||||
@@ -519,7 +519,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
}
|
||||
if (pretest) {
|
||||
if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest as string;
|
||||
if (!['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
|
||||
if (!['default', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
|
||||
throw new ProblemNotAllowPretestError('type');
|
||||
}
|
||||
}
|
||||
@@ -543,7 +543,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
}
|
||||
const rid = await record.add(
|
||||
domainId, this.pdoc.docId, this.user._id, lang, code, true,
|
||||
pretest ? { input, type: 'pretest' } : { contest: tid, files, type: tid ? 'contest' : 'judge' },
|
||||
pretest ? { input, type: 'pretest' } : { contest: tid, files, type: 'judge' },
|
||||
);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (!pretest) {
|
||||
@@ -698,6 +698,12 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||
} else this.response.template = 'problem_files.html';
|
||||
}
|
||||
|
||||
async post() {
|
||||
if (this.args.operation === 'get_links') return;
|
||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit files');
|
||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
}
|
||||
|
||||
@post('files', Types.Set)
|
||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||
async postGetLinks(domainId: string, files: Set<string>, type = 'testdata') {
|
||||
@@ -730,10 +736,8 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||
@post('filename', Types.Filename, true)
|
||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
|
||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit files');
|
||||
if (!this.request.files.file) throw new ValidationError('file');
|
||||
filename ||= this.request.files.file.originalFilename || String.random(16);
|
||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
const files = [];
|
||||
if (filename.endsWith('.zip') && type === 'testdata') {
|
||||
let zip: AdmZip;
|
||||
@@ -744,7 +748,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||
}
|
||||
const entries = zip.getEntries();
|
||||
for (const entry of entries) {
|
||||
if (!entry.name) continue;
|
||||
if (!entry.name || entry.isDirectory) continue;
|
||||
files.push({
|
||||
type,
|
||||
name: sanitize(entry.name),
|
||||
@@ -788,9 +792,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||
@post('newNames', Types.ArrayOf(Types.Filename))
|
||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||
async postRenameFiles(domainId: string, files: string[], newNames: string[], type = 'testdata') {
|
||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('rename files');
|
||||
if (files.length !== newNames.length) throw new ValidationError('files', 'newNames');
|
||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
await Promise.all(files.map(async (file, index) => {
|
||||
const newName = newNames[index];
|
||||
if (type === 'testdata') await problem.renameTestdata(domainId, this.pdoc.docId, file, newName, this.user._id);
|
||||
@@ -802,12 +804,21 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||
@post('files', Types.ArrayOf(Types.Filename))
|
||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||
async postDeleteFiles(domainId: string, files: string[], type = 'testdata') {
|
||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('delete files');
|
||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
if (type === 'testdata') await problem.delTestdata(domainId, this.pdoc.docId, files, this.user._id);
|
||||
else await problem.delAdditionalFile(domainId, this.pdoc.docId, files, this.user._id);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@post('std', Types.Filename)
|
||||
@post('gen', Types.Filename)
|
||||
async postGenerateTestdata(domainId: string, std: string, gen: string) {
|
||||
if (!this.pdoc.data.find((i) => i.name === std)) throw new BadRequestError();
|
||||
if (!this.pdoc.data.find((i) => i.name === gen)) throw new BadRequestError();
|
||||
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, '_', `${gen}\n${std}`, true, {
|
||||
type: 'generate',
|
||||
});
|
||||
this.response.redirect = this.url('record_detail', { rid });
|
||||
}
|
||||
}
|
||||
|
||||
export class ProblemFileDownloadHandler extends ProblemDetailHandler {
|
||||
|
||||
@@ -342,11 +342,13 @@ export interface JudgeMeta {
|
||||
problemOwner: number;
|
||||
hackRejudge?: string;
|
||||
rejudge?: boolean;
|
||||
// FIXME stricter types
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
|
||||
priority: number;
|
||||
type: 'judge';
|
||||
type: 'judge' | 'generate';
|
||||
rid: ObjectId;
|
||||
config: ProblemConfigFile;
|
||||
meta: JudgeMeta;
|
||||
|
||||
@@ -33,10 +33,7 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
||||
if (result.timeMax < result.timeMin) result.timeMax = result.timeMin = 1000;
|
||||
if (cfg.langs) result.langs = cfg.langs;
|
||||
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
||||
if (cfg.filename && result.type === 'default') {
|
||||
result.type = 'fileio';
|
||||
result.subType = cfg.filename;
|
||||
}
|
||||
if (cfg.filename && result.type === 'default') result.subType = cfg.filename;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ export default class RecordModel {
|
||||
'files',
|
||||
];
|
||||
|
||||
static RECORD_PRETEST = new ObjectId('000000000000000000000000');
|
||||
static RECORD_GENERATE = new ObjectId('000000000000000000000001');
|
||||
|
||||
static async submissionPriority(uid: number, base: number = 0) {
|
||||
const timeRecent = await RecordModel.coll
|
||||
.find({ _id: { $gte: Time.getObjectID(moment().add(-30, 'minutes')) }, uid, rejudged: { $ne: true } })
|
||||
@@ -82,8 +85,9 @@ export default class RecordModel {
|
||||
}
|
||||
meta = { ...meta, problemOwner: pdoc.owner };
|
||||
if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
|
||||
const type = (pdoc.config.type === 'remote_judge' && rdoc.contest?.toHexString() !== '0'.repeat(24)) ? 'remotejudge' : 'judge';
|
||||
config.type = pdoc.config.type === 'fileio' ? 'default' : pdoc.config.type as any;
|
||||
let type = 'judge';
|
||||
if (pdoc.config.type === 'remote_judge' && rdoc.contest?.toHexString() !== '0'.repeat(24)) type = 'remotejudge';
|
||||
else if (meta?.type === 'generate') type = 'generate';
|
||||
return await task.addMany(rids.map((rid) => ({
|
||||
...(pdoc.config as any),
|
||||
priority,
|
||||
@@ -104,7 +108,7 @@ export default class RecordModel {
|
||||
contest?: ObjectId,
|
||||
input?: string,
|
||||
files?: Record<string, string>,
|
||||
type: 'judge' | 'rejudge' | 'contest' | 'pretest' | 'hack',
|
||||
type: 'judge' | 'rejudge' | 'pretest' | 'hack' | 'generate',
|
||||
} = { type: 'judge' },
|
||||
) {
|
||||
const data: RecordDoc = {
|
||||
@@ -125,6 +129,7 @@ export default class RecordModel {
|
||||
judgeAt: null,
|
||||
rejudged: false,
|
||||
};
|
||||
let isContest = !!args.contest;
|
||||
if (args.contest) data.contest = args.contest;
|
||||
if (args.files) data.files = args.files;
|
||||
if (args.type === 'rejudge') {
|
||||
@@ -132,12 +137,16 @@ export default class RecordModel {
|
||||
data.rejudged = true;
|
||||
} else if (args.type === 'pretest') {
|
||||
data.input = args.input || '';
|
||||
data.contest = new ObjectId('000000000000000000000000');
|
||||
isContest = false;
|
||||
data.contest = RecordModel.RECORD_PRETEST;
|
||||
}
|
||||
const res = await RecordModel.coll.insertOne(data);
|
||||
if (addTask) {
|
||||
const priority = await RecordModel.submissionPriority(uid, args.type === 'pretest' ? -20 : (args.type === 'contest' ? 50 : 0));
|
||||
await RecordModel.judge(domainId, res.insertedId, priority, args.type === 'contest' ? { detail: false } : {}, { rejudge: data.rejudged });
|
||||
const priority = await RecordModel.submissionPriority(uid, args.type === 'pretest' ? -20 : (isContest ? 50 : 0));
|
||||
await RecordModel.judge(domainId, res.insertedId, priority, isContest ? { detail: false } : {}, {
|
||||
type: args.type,
|
||||
rejudge: data.rejudged,
|
||||
});
|
||||
}
|
||||
return res.insertedId;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Schedule } from '../interface';
|
||||
import { Logger } from '../logger';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
import RecordModel from './record';
|
||||
|
||||
const logger = new Logger('model/schedule');
|
||||
const coll = db.collection('schedule');
|
||||
@@ -136,7 +137,7 @@ export async function apply(ctx: Context) {
|
||||
ctx.worker = Worker;
|
||||
|
||||
Worker.addHandler('task.daily', async () => {
|
||||
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectId('000000000000000000000000') });
|
||||
await RecordModel.coll.deleteMany({ contest: { $in: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } });
|
||||
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
||||
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
||||
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ObjectId } from 'mongodb';
|
||||
import Schema from 'schemastery';
|
||||
import { STATUS } from '../model/builtin';
|
||||
import * as document from '../model/document';
|
||||
import RecordModel from '../model/record';
|
||||
import db from '../service/db';
|
||||
|
||||
const sumStatus = (status) => ({ $sum: { $cond: [{ $eq: ['$status', status] }, 1, 0] } });
|
||||
@@ -10,7 +10,7 @@ const sumStatus = (status) => ({ $sum: { $cond: [{ $eq: ['$status', status] }, 1
|
||||
export async function udoc(report) {
|
||||
report({ message: 'Udoc' });
|
||||
const pipeline = [
|
||||
{ $match: { contest: { $ne: new ObjectId('000000000000000000000000') } } },
|
||||
{ $match: { contest: { $nin: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } } },
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||
@@ -49,7 +49,7 @@ export async function udoc(report) {
|
||||
export async function psdoc(report) {
|
||||
report({ message: 'Psdoc' });
|
||||
const pipeline = [
|
||||
{ $match: { contest: { $ne: new ObjectId('000000000000000000000000') } } },
|
||||
{ $match: { contest: { $nin: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } } },
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||
@@ -68,7 +68,7 @@ export async function pdoc(report) {
|
||||
const pipeline = [
|
||||
{
|
||||
$match: {
|
||||
contest: { $ne: new ObjectId('000000000000000000000000') },
|
||||
contest: { $nin: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] },
|
||||
status: { $ne: STATUS.STATUS_CANCELED },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { assign } from 'lodash';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||
import FileSelectAutoCompleteFC from './components/FileSelectAutoComplete';
|
||||
|
||||
interface FileSelectOptions {
|
||||
data: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const Component = React.forwardRef<any, any>((props, ref) => {
|
||||
const [value, setValue] = React.useState(props.value);
|
||||
return (
|
||||
<FileSelectAutoCompleteFC
|
||||
ref={ref as any}
|
||||
height="auto"
|
||||
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
props.onChange(v);
|
||||
}}
|
||||
data={props.data}
|
||||
multi={props.multi}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
||||
options: FileSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
classes: 'custom-select',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
value(): Multi extends true ? number[] : string {
|
||||
if (this.options.multi) return this.ref?.getSelectedItemKeys().map((i) => +i) ?? this.$dom.val();
|
||||
return this.ref?.getSelectedItems()[0] ?? null;
|
||||
}
|
||||
|
||||
attach() {
|
||||
const value = this.$dom.val();
|
||||
ReactDOM.createRoot(this.container).render(
|
||||
<Component
|
||||
ref={(ref) => { this.ref = ref; }}
|
||||
data={this.options.data}
|
||||
value={value}
|
||||
multi={this.options.multi}
|
||||
onChange={this.onChange}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assign(FileSelectAutoComplete, DOMAttachedObject);
|
||||
window.Hydro.components.FileSelectAutoComplete = FileSelectAutoComplete;
|
||||
@@ -100,7 +100,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
|
||||
}
|
||||
|
||||
render() {
|
||||
let canUsePretest = ['default', 'fileio'].includes(UiContext.pdoc.config?.type);
|
||||
let canUsePretest = UiContext.pdoc.config?.type === 'default';
|
||||
const langInfo = availableLangs[this.props.editorLang];
|
||||
if (UiContext.pdoc.config?.type === 'remote_judge' && langInfo) {
|
||||
if (langInfo.pretest) canUsePretest = true;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { i18n } from 'vj/utils';
|
||||
|
||||
export default function reducer(state = {
|
||||
pretest: {
|
||||
visible: ['default', 'fileio'].includes(UiContext.pdoc.config?.type)
|
||||
visible: UiContext.pdoc.config?.type === 'default'
|
||||
? localStorage.getItem('scratchpad/pretest') === 'true'
|
||||
: false,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
__langname: English
|
||||
'File IO: {0}': 'File IO: {0}'
|
||||
Can be Extended For: Extension
|
||||
Current Status: Status
|
||||
Handled Requests: Req
|
||||
@@ -27,7 +28,6 @@ problem_main: Problem Set
|
||||
problem_solution: Problem Solution
|
||||
problem_submit: Problem Submit
|
||||
problem_type.default: 'Type: Default'
|
||||
problem_type.fileio: 'Type: FileIO ({0})'
|
||||
problem_type.interactive: 'Type: Interactive'
|
||||
problem_type.objective: 'Type: Objective'
|
||||
problem_type.remote_judge: 'Type: RemoteJudge'
|
||||
|
||||
@@ -27,7 +27,9 @@ __langname: 简体中文
|
||||
'Current dataset: {0}': '当前测试数据: {0}'
|
||||
'Discussion: Terms Of Service': 讨论区服务条款
|
||||
'Drag and drop testcases here:': '拖拽测试点到此处:'
|
||||
'Enter a new name for the file: ': 请输入新的文件名:
|
||||
'Failed to get credential: {0}': '获取认证数据失败: {0}'
|
||||
'File IO: {0}': 文件IO:{0}
|
||||
'File upload failed: {0}': 文件上传失败:{0}
|
||||
'Format: category 1, category 2, ..., category n': 格式:分类1, 分类2, ..., 分类n
|
||||
'Hello, {0}! You can click following link to active your new email of your {1} account:': 您好,{0}!您可以点击以下链接来激活您 {1} 账户的新电子邮件地址:
|
||||
@@ -50,6 +52,8 @@ __langname: 简体中文
|
||||
(None): (无)
|
||||
(Not changed): 未更改
|
||||
6-Digit Code: 6位代码
|
||||
A data generator is a program that generates testdata. It should output result to stdout.: 数据生成器是一个生成测试数据的程序。它应该将结果输出到标准输出。
|
||||
A standard program is a program that solves the problem. It should output answer to stdout.: 标准程序是一个解决问题的程序。它应该将答案输出到标准输出。
|
||||
Aborted: 已放弃
|
||||
About {0}: 关于 {0}
|
||||
About Markdown: 关于 Markdown
|
||||
@@ -245,6 +249,7 @@ Current Status: 当前状态
|
||||
currently offline: 目前离线
|
||||
Daily update check: 每日自动检查更新
|
||||
Dark: 暗色
|
||||
Data Generator: 数据生成器
|
||||
Data of Problem {0} not found.: 题目 {0} 的数据缺失。
|
||||
Data of problem {1} not found.: 题目 {1} 的数据未找到。
|
||||
Data of record {0} not found.: 记录 {0} 的数据未找到。
|
||||
@@ -358,7 +363,6 @@ End Time: 结束时间
|
||||
Enroll Training: 参加训练
|
||||
Enrolled: 已参加
|
||||
Enrollees: 参加人数
|
||||
'Enter a new name for the file: ': 请输入新的文件名:
|
||||
error: 错误
|
||||
Errors detected in the config. Confirm save?: 配置中存在错误。确认保存?
|
||||
Evaluated difficulty: 估计的难度
|
||||
@@ -398,6 +402,9 @@ Freeze scoreboard with N minutes remaining: 在赛事剩余N分钟时封榜
|
||||
fs_upload: 上传文件
|
||||
Gender Visibility: 性别可见性
|
||||
Gender: 性别
|
||||
Generate testdata is not supported for this problem type.: 此题目类型不支持生成测试数据。
|
||||
Generate Testdata: 生成测试数据
|
||||
Generate: 生成
|
||||
Gravatar URL Prefix: Gravatar地址前缀
|
||||
Group / UID: 小组或 UID
|
||||
Group Name: 名称
|
||||
@@ -686,7 +693,6 @@ problem_main: 题库
|
||||
problem_solution: 题解
|
||||
problem_submit: 递交代码
|
||||
problem_type.default: 传统题
|
||||
problem_type.fileio: '文件IO ({0})'
|
||||
problem_type.interactive: 交互题
|
||||
problem_type.objective: 客观题
|
||||
problem_type.remote_judge: 远端评测题
|
||||
@@ -845,6 +851,7 @@ Sorry: 对不起
|
||||
Sort by: 排序
|
||||
Source type: 来源类型
|
||||
Standard mode: 标准模式
|
||||
Standard Program: 标准程序
|
||||
Star Topic: 收藏讨论
|
||||
Star: 收藏
|
||||
Starred Users: 关注用户
|
||||
|
||||
@@ -2,7 +2,8 @@ import $ from 'jquery';
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog/index';
|
||||
import FileSelectAutoComplete from 'vj/components/autocomplete/FileSelectAutoComplete';
|
||||
import { ActionDialog, ConfirmDialog, InfoDialog } from 'vj/components/dialog/index';
|
||||
import createHint from 'vj/components/hint';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { previewFile } from 'vj/components/preview/preview.page';
|
||||
@@ -318,6 +319,48 @@ const page = new NamedPage('problem_files', () => {
|
||||
handleClickUpload(type, files);
|
||||
}
|
||||
|
||||
async function handleGenerateTestdata(ev) {
|
||||
ev.preventDefault();
|
||||
const gen = $('[name=gen]').val();
|
||||
const std = $('[name=std]').val();
|
||||
if (!gen) {
|
||||
Notification.error(i18n('Please input the generator.'));
|
||||
return;
|
||||
}
|
||||
if (!std) {
|
||||
Notification.error(i18n('Please input the standard program.'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await request.post('', {
|
||||
operation: 'generate_testdata',
|
||||
gen,
|
||||
std,
|
||||
});
|
||||
if (res.error) Notification.error(res.error);
|
||||
else {
|
||||
Notification.success(i18n('Generating...'));
|
||||
await new InfoDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<iframe src="${res.url}"
|
||||
scrolling="yes" border="0" frameborder="no" framespacing="0" width="100%" style="height: 70vh;"></iframe>
|
||||
</div>`,
|
||||
width: `${window.innerWidth - 200}px`,
|
||||
height: `${window.innerHeight - 100}px`,
|
||||
}).open();
|
||||
}
|
||||
} catch (error) {
|
||||
Notification.error([error.message, ...error.params].join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
if (UiContext.pdoc) {
|
||||
FileSelectAutoComplete.getOrConstruct($('[name=gen]'), { data: UiContext.pdoc.data });
|
||||
FileSelectAutoComplete.getOrConstruct($('[name=std]'), { data: UiContext.pdoc.data });
|
||||
$('[name="generate_testdata"]').on('click', handleGenerateTestdata);
|
||||
}
|
||||
|
||||
if ($('[name="upload_testdata"]').length) {
|
||||
$(document).on('click', '[name="upload_testdata"]', () => handleClickUpload('testdata'));
|
||||
$(document).on('click', '[name="upload_file"]', () => handleClickUpload('additional_file'));
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
||||
{% endif %}
|
||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
||||
{% if pdoc.config.type == 'default' and pdoc.config.subType %}
|
||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-book">{{ _('File IO: {0}').format(pdoc.config.subType) }}</span>
|
||||
{% endif %}
|
||||
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-comparison">{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</span>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -27,6 +28,36 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not reference and (handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM)) %}
|
||||
{{ set(UiContext, {
|
||||
pdoc: pdoc
|
||||
}) }}
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Generate Testdata') }}(Beta)</h1>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
<div class="section__body generate">
|
||||
{% if pdoc.config.type === 'default' %}
|
||||
{{ form.form_text({
|
||||
columns: 12,
|
||||
label:'Data Generator',
|
||||
name:'gen',
|
||||
help_text:'A data generator is a program that generates testdata. It should output result to stdout.'
|
||||
}) }}
|
||||
{{ form.form_text({
|
||||
columns: 12,
|
||||
label:'Standard Program',
|
||||
name:'std',
|
||||
help_text:'A standard program is a program that solves the problem. It should output answer to stdout.'
|
||||
}) }}
|
||||
<button class="rounded button" name="generate_testdata">{{ _('Generate') }}</button>
|
||||
{% else %}
|
||||
{{ nothing.render('Generate testdata is not supported for this problem type.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="medium-4 columns">
|
||||
<div class="section">
|
||||
|
||||
Reference in new issue
Block a user