mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
judge: add multi-pass problem support (#1167)
This commit is contained in:
1 parent
01771a43e5
commit
809d1ac6ad
11 files changed
+166
-33
No files matched your search
@@ -56,6 +56,7 @@ export interface ProblemConfigFile {
|
|||||||
cases?: TestCaseConfig[];
|
cases?: TestCaseConfig[];
|
||||||
subtasks?: SubtaskConfig[];
|
subtasks?: SubtaskConfig[];
|
||||||
langs?: string[];
|
langs?: string[];
|
||||||
|
multi_pass?: number;
|
||||||
checker?: CompilableSource;
|
checker?: CompilableSource;
|
||||||
interactor?: CompilableSource;
|
interactor?: CompilableSource;
|
||||||
manager?: CompilableSource;
|
manager?: CompilableSource;
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ function isValidConfig(config) {
|
|||||||
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
|
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
|
||||||
throw new FormatError('You did not specify a 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) {
|
async function collectFiles(folder: string) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Checker = (config: CheckConfig) => Promise<{
|
|||||||
status: number;
|
status: number;
|
||||||
score: number;
|
score: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
nextPass?: { input: CopyInFile, state?: Record<string, CopyInFile> };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function parseDiffMsg(msg: string) {
|
function parseDiffMsg(msg: string) {
|
||||||
@@ -236,7 +237,7 @@ const checkers: Record<string, Checker> = new Proxy({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async testlib(config) {
|
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: {
|
copyIn: {
|
||||||
in: config.input,
|
in: config.input,
|
||||||
user_out: config.user_stdout,
|
user_out: config.user_stdout,
|
||||||
@@ -245,6 +246,7 @@ const checkers: Record<string, Checker> = new Proxy({
|
|||||||
...config.copyIn,
|
...config.copyIn,
|
||||||
},
|
},
|
||||||
env: config.env,
|
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)) {
|
if ([STATUS.STATUS_SYSTEM_ERROR, STATUS.STATUS_TIME_LIMIT_EXCEEDED, STATUS.STATUS_MEMORY_LIMIT_EXCEEDED].includes(status)) {
|
||||||
const message = {
|
const message = {
|
||||||
@@ -265,12 +267,22 @@ const checkers: Record<string, Checker> = new Proxy({
|
|||||||
message: `Checker exited with code ${code}`,
|
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
|
// https://www.kattis.com/problem-package-format/spec/2023-07-draft.html#output-validator
|
||||||
async kattis(config) {
|
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: {
|
copyIn: {
|
||||||
input: config.input,
|
input: config.input,
|
||||||
answer_file: config.output,
|
answer_file: config.output,
|
||||||
@@ -284,6 +296,10 @@ const checkers: Record<string, Checker> = new Proxy({
|
|||||||
'feedback_dir/teammessage.txt?',
|
'feedback_dir/teammessage.txt?',
|
||||||
'feedback_dir/judgeerror.txt?',
|
'feedback_dir/judgeerror.txt?',
|
||||||
],
|
],
|
||||||
|
copyOutCached: [
|
||||||
|
'feedback_dir/nextpass.in?',
|
||||||
|
'feedback_dir/state.txt?',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = code === 42
|
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'] || ''
|
? 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 };
|
return { status, score, message };
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function judgeCase(c: NormalizedCase) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
execute[0].execute += managerArgs;
|
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];
|
const resManager = res[0];
|
||||||
let time = 0;
|
let time = 0;
|
||||||
let memory = 0;
|
let memory = 0;
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ import checkers from '../checkers';
|
|||||||
import { runFlow } from '../flow';
|
import { runFlow } from '../flow';
|
||||||
import { runQueued } from '../sandbox';
|
import { runQueued } from '../sandbox';
|
||||||
import signals from '../signals';
|
import signals from '../signals';
|
||||||
import { Context, ContextSubTask } from './interface';
|
import { Context, ContextSubTask, MultiPassContext } from './interface';
|
||||||
|
|
||||||
function judgeCase(c: NormalizedCase) {
|
function judgeCase(c: NormalizedCase) {
|
||||||
|
const mp: MultiPassContext = { i: 0 };
|
||||||
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
|
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
|
||||||
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
|
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(
|
await using res = await runQueued(
|
||||||
ctx.execute.execute,
|
ctx.execute.execute,
|
||||||
{
|
{
|
||||||
stdin: { src: c.input },
|
stdin: mp.input ?? { src: c.input },
|
||||||
copyIn: ctx.execute.copyIn,
|
copyIn: { ...ctx.execute.copyIn, ...mp.state },
|
||||||
filename: ctx.config.filename,
|
filename: ctx.config.filename,
|
||||||
time: c.time,
|
time: c.time,
|
||||||
memory: c.memory,
|
memory: c.memory,
|
||||||
@@ -20,25 +23,29 @@ function judgeCase(c: NormalizedCase) {
|
|||||||
addressSpaceLimit: address_space_limit,
|
addressSpaceLimit: address_space_limit,
|
||||||
processLimit: process_limit,
|
processLimit: process_limit,
|
||||||
},
|
},
|
||||||
`judgeCase[${c.id}]<${ctx.rid}>`,
|
`judgeCase[${c.id}]${mp.i ? `[pass=${mp.i}]` : ''}<${ctx.rid}>`,
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
code, signalled, time, memory, fileIds,
|
code, signalled, time, memory, fileIds,
|
||||||
} = res;
|
} = res;
|
||||||
let { status } = res;
|
let { status } = res;
|
||||||
let message: any = '';
|
let message: any = mp.i ? `[Pass ${mp.i}] ` : '';
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
let nextPass: any;
|
||||||
if (status === STATUS.STATUS_ACCEPTED) {
|
if (status === STATUS.STATUS_ACCEPTED) {
|
||||||
if (time > c.time) {
|
if (time > c.time) {
|
||||||
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||||
} else if (memory > c.memory * 1024) {
|
} else if (memory > c.memory * 1024) {
|
||||||
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||||
} else {
|
} else {
|
||||||
({ status, score, message } = await checkers[ctx.config.checker_type]({
|
const resChecker = await checkers[ctx.config.checker_type]({
|
||||||
execute: ctx.checker.execute,
|
execute: ctx.checker.execute,
|
||||||
copyIn: ctx.checker.copyIn || {},
|
copyIn: {
|
||||||
|
...ctx.checker.copyIn,
|
||||||
|
...mp.state ?? {},
|
||||||
|
},
|
||||||
code: ctx.code,
|
code: ctx.code,
|
||||||
input: { src: c.input },
|
input: mp.input ?? { src: c.input },
|
||||||
output: { src: c.output },
|
output: { src: c.output },
|
||||||
user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' },
|
user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' },
|
||||||
user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' },
|
user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' },
|
||||||
@@ -49,16 +56,30 @@ function judgeCase(c: NormalizedCase) {
|
|||||||
HYDRO_TESTCASE: c.id.toString(),
|
HYDRO_TESTCASE: c.id.toString(),
|
||||||
HYDRO_TIME_USAGE: time.toString(),
|
HYDRO_TIME_USAGE: time.toString(),
|
||||||
HYDRO_MEMORY_USAGE: Math.floor(memory / 1024).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') {
|
} else if (status === STATUS.STATUS_RUNTIME_ERROR && code && ctx.config.detail === 'full') {
|
||||||
if (code < 32 && signalled) message = signals[code];
|
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) {
|
if (runner && ctx.rerun && c.time <= 5000 && status === STATUS.STATUS_TIME_LIMIT_EXCEEDED) {
|
||||||
ctx.rerun--;
|
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)) {
|
if (!ctx.request.rejudged && !ctx.analysis && [STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) {
|
||||||
ctx.analysis = true;
|
ctx.analysis = true;
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ import { runFlow } from '../flow';
|
|||||||
import { runPiped } from '../sandbox';
|
import { runPiped } from '../sandbox';
|
||||||
import signals from '../signals';
|
import signals from '../signals';
|
||||||
import { parse } from '../testlib';
|
import { parse } from '../testlib';
|
||||||
import { Context, ContextSubTask } from './interface';
|
import { Context, ContextSubTask, MultiPassContext } from './interface';
|
||||||
|
|
||||||
function judgeCase(c: NormalizedCase) {
|
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);
|
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
|
||||||
|
if (ctx.config.multi_pass && !mp.i) mp.i = 1;
|
||||||
|
|
||||||
const [{
|
const [{
|
||||||
code, signalled, time, memory,
|
code, signalled, time, memory,
|
||||||
}, resInteractor] = await runPiped([
|
}, resInteractor] = await runPiped([
|
||||||
{
|
{
|
||||||
execute: ctx.executeUser.execute,
|
execute: ctx.executeUser.execute,
|
||||||
copyIn: ctx.executeUser.copyIn,
|
copyIn: { ...ctx.executeUser.copyIn, ...mp.state },
|
||||||
time: c.time,
|
time: c.time,
|
||||||
memory: c.memory,
|
memory: c.memory,
|
||||||
addressSpaceLimit: address_space_limit,
|
addressSpaceLimit: address_space_limit,
|
||||||
@@ -22,23 +25,29 @@ function judgeCase(c: NormalizedCase) {
|
|||||||
{
|
{
|
||||||
execute: `${ctx.executeInteractor.execute} /w/in /w/tout /w/out`,
|
execute: `${ctx.executeInteractor.execute} /w/in /w/tout /w/out`,
|
||||||
copyIn: {
|
copyIn: {
|
||||||
in: c.input ? { src: c.input } : { content: '' },
|
in: mp.input ?? { src: c.input },
|
||||||
out: c.output ? { src: c.output } : { content: '' },
|
out: c.output ? { src: c.output } : { content: '' },
|
||||||
...ctx.executeInteractor.copyIn,
|
...ctx.executeInteractor.copyIn,
|
||||||
|
...mp.state ?? {},
|
||||||
},
|
},
|
||||||
time: c.time * 2,
|
time: c.time * 2,
|
||||||
memory: c.memory * 2,
|
memory: c.memory * 2,
|
||||||
copyOut: ['/w/tout?'],
|
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: 0, fd: 1 }, out: { index: 1, fd: 0 }, name: 'userToInteractor' },
|
||||||
{ in: { index: 1, fd: 1 }, out: { index: 0, fd: 0 }, name: 'interactorToUser' },
|
{ 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?)
|
// TODO handle tout (maybe pass to checker?)
|
||||||
let status: number;
|
let status: number;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let message: any = '';
|
let message: any = mp.i ? `[Pass ${mp.i}] ` : '';
|
||||||
if (time > c.time) {
|
if (time > c.time) {
|
||||||
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||||
} else if (memory > c.memory * 1024) {
|
} 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))) {
|
} else if (ctx.config.detail === 'full' && ((code && code !== 13/* Broken Pipe */) || (code === 13 && !resInteractor.code))) {
|
||||||
status = STATUS.STATUS_RUNTIME_ERROR;
|
status = STATUS.STATUS_RUNTIME_ERROR;
|
||||||
if (code < 32 && signalled) message = signals[code];
|
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 {
|
} else {
|
||||||
const result = parse(resInteractor.stderr, c.score, ctx.config.detail);
|
const result = parse(resInteractor.stderr, c.score, ctx.config.detail);
|
||||||
status = result.status;
|
status = result.status;
|
||||||
score = result.score;
|
score = result.score;
|
||||||
message = result.message;
|
message += result.message;
|
||||||
if (resInteractor.code && !(resInteractor.stderr || '').trim().length) message += ` (Interactor exited with code ${resInteractor.code})`;
|
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 {
|
return {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { NormalizedSubtask } from '@hydrooj/common';
|
import { NormalizedSubtask } from '@hydrooj/common';
|
||||||
import { Execute } from '../interface';
|
import { Execute } from '../interface';
|
||||||
|
import { CopyInFile } from '../sandbox';
|
||||||
import { JudgeTask } from '../task';
|
import { JudgeTask } from '../task';
|
||||||
|
|
||||||
export type Context = JudgeTask & RuntimeContext;
|
export type Context = JudgeTask & RuntimeContext;
|
||||||
@@ -29,3 +30,9 @@ export interface ContextSubTask {
|
|||||||
score: number;
|
score: number;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MultiPassContext {
|
||||||
|
i: number;
|
||||||
|
input?: CopyInFile;
|
||||||
|
state?: Record<string, CopyInFile>;
|
||||||
|
}
|
||||||
@@ -79,7 +79,8 @@ const problemConfigSchema: JSONSchema7 = {
|
|||||||
},
|
},
|
||||||
interactor: { $ref: '#/definitions/compilableFile' },
|
interactor: { $ref: '#/definitions/compilableFile' },
|
||||||
manager: { $ref: '#/definitions/compilableFile' },
|
manager: { $ref: '#/definitions/compilableFile' },
|
||||||
num_processes: { type: 'number', minimum: 1, maximum: 5 },
|
num_processes: { type: 'integer', maximum: 5, minimum: 1 },
|
||||||
|
multi_pass: { type: 'integer', maximum: 20, minimum: 2 },
|
||||||
validator: { $ref: '#/definitions/compilableFile' },
|
validator: { $ref: '#/definitions/compilableFile' },
|
||||||
user_extra_files: { type: 'array', items: { type: 'string' } },
|
user_extra_files: { type: 'array', items: { type: 'string' } },
|
||||||
judge_extra_files: { type: 'array', items: { type: 'string' } },
|
judge_extra_files: { type: 'array', items: { type: 'string' } },
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ interface Props {
|
|||||||
const configKey = [
|
const configKey = [
|
||||||
'type', 'subType', 'target', 'score', 'time',
|
'type', 'subType', 'target', 'score', 'time',
|
||||||
'memory', 'filename', 'checker_type', 'checker', 'interactor',
|
'memory', 'filename', 'checker_type', 'checker', 'interactor',
|
||||||
'manager', 'num_processes', 'validator', 'user_extra_files', 'judge_extra_files',
|
'manager', 'num_processes', 'multi_pass', 'validator', 'user_extra_files',
|
||||||
'detail', 'outputs', 'redirect', 'cases', 'subtasks',
|
'judge_extra_files', 'detail', 'outputs', 'redirect', 'cases',
|
||||||
'langs', 'key', 'time_limit_rate', 'memory_limit_rate',
|
'subtasks', 'langs', 'key', 'time_limit_rate', 'memory_limit_rate',
|
||||||
];
|
];
|
||||||
|
|
||||||
const subtasksKey = [
|
const subtasksKey = [
|
||||||
@@ -50,6 +50,7 @@ export function configYamlFormat(config: ProblemConfigFile) {
|
|||||||
if (key === 'checker'
|
if (key === 'checker'
|
||||||
&& (['default', 'strict'].includes(formatConfig.checker_type) || !formatConfig.checker_type)) return;
|
&& (['default', 'strict'].includes(formatConfig.checker_type) || !formatConfig.checker_type)) return;
|
||||||
if (key === 'interactor' && config.type !== 'interactive') return;
|
if (key === 'interactor' && config.type !== 'interactive') return;
|
||||||
|
if (key === 'multi_pass' && (!Number.isInteger(config.multi_pass) || config.multi_pass <= 1 || config.multi_pass > 20)) return;
|
||||||
if (key === 'subtasks') {
|
if (key === 'subtasks') {
|
||||||
formatConfig[key] = [];
|
formatConfig[key] = [];
|
||||||
config[key].forEach((subtask) => {
|
config[key].forEach((subtask) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Card, Text, TextInput } from '@mantine/core';
|
import { Card, Switch, Text, TextInput } from '@mantine/core';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
@@ -13,10 +13,10 @@ function FileIOConfig() {
|
|||||||
const filename = useSelector((state: RootState) => state.config.filename);
|
const filename = useSelector((state: RootState) => state.config.filename);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="FileIOConfig" disableLabel>
|
<FormItem columns={6} label="FileIOConfig" disableLabel>
|
||||||
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="FileIO">
|
<FormItem columns={12} label="FileIO">
|
||||||
<TextInput
|
<TextInput
|
||||||
rightSection={<Text size="sm">.in/.out</Text>}
|
rightSection={<Text size="sm">.in/.out</Text>}
|
||||||
rightSectionWidth={100}
|
rightSectionWidth={100}
|
||||||
@@ -34,6 +34,42 @@ function FileIOConfig() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MultiPassConfig() {
|
||||||
|
const multiPass = useSelector((state: RootState) => state.config.multi_pass);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [enabled, setEnabled] = useState(multiPass > 1);
|
||||||
|
return (
|
||||||
|
<FormItem columns={6} label="Multi-pass" disableLabel>
|
||||||
|
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
||||||
|
<div className="row">
|
||||||
|
<FormItem columns={6} label="Multi Pass">
|
||||||
|
<Switch
|
||||||
|
styles={{ body: { display: 'flex', height: '36px', alignItems: 'center' } }}
|
||||||
|
checked={enabled}
|
||||||
|
label={i18n('Enabled')}
|
||||||
|
onChange={() => {
|
||||||
|
setEnabled(!enabled);
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'multi_pass', value: enabled ? 0 : 2 });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
{enabled && (
|
||||||
|
<FormItem columns={6} label={i18n('Max Passes')}>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
min={2}
|
||||||
|
max={10}
|
||||||
|
value={multiPass}
|
||||||
|
onChange={(ev) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'multi_pass', value: +ev.currentTarget.value })}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ExtraFilesConfig() {
|
function ExtraFilesConfig() {
|
||||||
const Files = useSelector((state: RootState) => state.testdata);
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files || [], isEqual);
|
const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files || [], isEqual);
|
||||||
@@ -92,6 +128,7 @@ export default function ProblemConfigForm() {
|
|||||||
<div className="row problem-config-form">
|
<div className="row problem-config-form">
|
||||||
<ProblemType />
|
<ProblemType />
|
||||||
{Type === 'default' && <FileIOConfig />}
|
{Type === 'default' && <FileIOConfig />}
|
||||||
|
{['default', 'interactive'].includes(Type) && <MultiPassConfig />}
|
||||||
{!['submit_answer', 'objective'].includes(Type) && (
|
{!['submit_answer', 'objective'].includes(Type) && (
|
||||||
<>
|
<>
|
||||||
<ExtraFilesConfig />
|
<ExtraFilesConfig />
|
||||||
|
|||||||
@@ -422,7 +422,6 @@ Forgot Password and/or Username: 忘记密码和/或用户名
|
|||||||
Forgot password or username?: 忘记密码或者用户名?
|
Forgot password or username?: 忘记密码或者用户名?
|
||||||
Formula blocks: 公式区块
|
Formula blocks: 公式区块
|
||||||
Freeze scoreboard with N minutes remaining: 在赛事剩余N分钟时封榜
|
Freeze scoreboard with N minutes remaining: 在赛事剩余N分钟时封榜
|
||||||
Keep scoreboard hidden even after contest ends: 即使比赛结束后也保持成绩表隐藏
|
|
||||||
fs_upload: 上传文件
|
fs_upload: 上传文件
|
||||||
Gender Visibility: 性别可见性
|
Gender Visibility: 性别可见性
|
||||||
Gender: 性别
|
Gender: 性别
|
||||||
@@ -523,6 +522,7 @@ Judged By: 评测机
|
|||||||
Judging Queue: 评测队列
|
Judging Queue: 评测队列
|
||||||
just now: 刚刚
|
just now: 刚刚
|
||||||
Keep current expiration: 保持当前过期设置
|
Keep current expiration: 保持当前过期设置
|
||||||
|
Keep scoreboard hidden even after contest ends: 即使比赛结束后也保持成绩表隐藏
|
||||||
Langs Config: 语言设置
|
Langs Config: 语言设置
|
||||||
langs: 语言
|
langs: 语言
|
||||||
Language: 语言
|
Language: 语言
|
||||||
@@ -572,6 +572,7 @@ Mark Green: 标绿
|
|||||||
Mark Red: 标红
|
Mark Red: 标红
|
||||||
Mark user as joined using admin privilege: 使用管理员权限将用户标记为已加入
|
Mark user as joined using admin privilege: 使用管理员权限将用户标记为已加入
|
||||||
Mark Yellow: 标黄
|
Mark Yellow: 标黄
|
||||||
|
Max Passes: 最大轮次
|
||||||
Memory Cost: 内存占用
|
Memory Cost: 内存占用
|
||||||
Memory Info: 内存信息
|
Memory Info: 内存信息
|
||||||
Memory: 内存
|
Memory: 内存
|
||||||
|
|||||||
Reference in new issue
Block a user