mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
judge: use case.score & normalizeSubtask API
This commit is contained in:
1 parent
310189bc4d
commit
539a1c44ce
14 files changed
+231
-201
No files matched your search
@@ -3,40 +3,12 @@ import fs from 'fs-extra';
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { max, sum } from 'lodash';
|
import { max, sum } from 'lodash';
|
||||||
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
|
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
|
||||||
import { readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
||||||
import { changeErrorType } from '@hydrooj/utils/lib/utils';
|
import { changeErrorType } from '@hydrooj/utils/lib/utils';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { FormatError, SystemError } from './error';
|
import { FormatError, SystemError } from './error';
|
||||||
import { ensureFile, parseMemoryMB } from './utils';
|
import { ensureFile, parseMemoryMB } from './utils';
|
||||||
|
|
||||||
async function readAutoCases(folder: string, { next }, cfg, rst) {
|
|
||||||
const config = {
|
|
||||||
checker_type: 'default',
|
|
||||||
count: 0,
|
|
||||||
subtasks: [],
|
|
||||||
judge_extra_files: [],
|
|
||||||
user_extra_files: [],
|
|
||||||
};
|
|
||||||
const checkFile = ensureFile(folder);
|
|
||||||
try {
|
|
||||||
const files = await fs.readdir(folder);
|
|
||||||
if (await fs.pathExists(path.resolve(folder, 'input'))) {
|
|
||||||
const inputs = await fs.readdir(path.resolve(folder, 'input'));
|
|
||||||
files.push(...inputs.map((i) => `input/${i}`));
|
|
||||||
}
|
|
||||||
if (await fs.pathExists(path.resolve(folder, 'output'))) {
|
|
||||||
const outputs = await fs.readdir(path.resolve(folder, 'output'));
|
|
||||||
files.push(...outputs.map((i) => `output/${i}`));
|
|
||||||
}
|
|
||||||
const result = readSubtasksFromFiles(files, checkFile, cfg, rst);
|
|
||||||
Object.assign(config, result);
|
|
||||||
if (cfg.isSelfSubmission) next({ message: { message: 'Found {0} testcases.', params: [config.count] } });
|
|
||||||
} catch (e) {
|
|
||||||
throw new SystemError('Cannot parse testdata.', [e.message, ...e.params]);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidConfig(config) {
|
function isValidConfig(config) {
|
||||||
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.');
|
||||||
@@ -52,35 +24,54 @@ function isValidConfig(config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function collectFiles(folder: string) {
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
await Promise.all(['input', 'output'].map(async (t) => {
|
||||||
|
if (await fs.pathExists(path.resolve(folder, t))) {
|
||||||
|
const f = await fs.readdir(path.resolve(folder, t));
|
||||||
|
files.push(...f.map((i) => `${t}/${i}`));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function readCases(folder: string, cfg: Record<string, any> = {}, args) {
|
export default async function readCases(folder: string, cfg: Record<string, any> = {}, args) {
|
||||||
const iniConfig = path.resolve(folder, 'config.ini');
|
const iniConfig = path.resolve(folder, 'config.ini');
|
||||||
const yamlConfig = path.resolve(folder, 'config.yaml');
|
const yamlConfig = path.resolve(folder, 'config.yaml');
|
||||||
const ymlConfig = path.resolve(folder, 'config.yml');
|
const ymlConfig = path.resolve(folder, 'config.yml');
|
||||||
const config = { ...cfg };
|
const config: Record<string, any> = {
|
||||||
if (fs.existsSync(yamlConfig)) {
|
checker_type: 'default',
|
||||||
Object.assign(config, yaml.load(fs.readFileSync(yamlConfig).toString()));
|
count: 0,
|
||||||
} else if (fs.existsSync(ymlConfig)) {
|
subtasks: [],
|
||||||
Object.assign(config, yaml.load(fs.readFileSync(ymlConfig).toString()));
|
judge_extra_files: [],
|
||||||
} else if (fs.existsSync(iniConfig)) {
|
user_extra_files: [],
|
||||||
try {
|
...cfg,
|
||||||
Object.assign(config, convertIniConfig(fs.readFileSync(iniConfig).toString()));
|
};
|
||||||
} catch (e) {
|
|
||||||
throw changeErrorType(e, FormatError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let result;
|
|
||||||
try {
|
try {
|
||||||
result = await readYamlCases(config, ensureFile(folder));
|
if (fs.existsSync(yamlConfig)) {
|
||||||
|
Object.assign(config, yaml.load(fs.readFileSync(yamlConfig).toString()));
|
||||||
|
} else if (fs.existsSync(ymlConfig)) {
|
||||||
|
Object.assign(config, yaml.load(fs.readFileSync(ymlConfig).toString()));
|
||||||
|
} else if (fs.existsSync(iniConfig)) {
|
||||||
|
Object.assign(config, convertIniConfig(fs.readFileSync(iniConfig).toString()));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw changeErrorType(e, FormatError);
|
throw changeErrorType(e, FormatError);
|
||||||
}
|
}
|
||||||
let cases = result.outputs?.length || 0;
|
const checkFile = ensureFile(folder);
|
||||||
cases += Math.sum((result.subtasks || []).map((subtask) => subtask.cases.length));
|
const result = await readYamlCases(config, checkFile)
|
||||||
if (!cases) {
|
.catch((e) => { throw changeErrorType(e, FormatError); });
|
||||||
const c = await readAutoCases(folder, args, config, result);
|
result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
|
||||||
result.subtasks = c.subtasks;
|
if (!result.count) {
|
||||||
result.count = c.count;
|
try {
|
||||||
|
result.subtasks = readSubtasksFromFiles(await collectFiles(folder), checkFile, cfg);
|
||||||
|
result.count = Math.sum(result.subtasks.map((i) => i.cases.length));
|
||||||
|
if (cfg.isSelfSubmission) args.next?.({ message: { message: 'Found {0} testcases.', params: [result.count] } });
|
||||||
|
} catch (e) {
|
||||||
|
throw new SystemError('Cannot parse testdata.', [e.message, ...e.params]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory);
|
||||||
if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key');
|
if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key');
|
||||||
if (!result.key) isValidConfig(result);
|
if (!result.key) isValidConfig(result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
user_stdout: stdout,
|
user_stdout: stdout,
|
||||||
user_stderr: stderr,
|
user_stderr: stderr,
|
||||||
checker_type: ctx.config.checker_type,
|
checker_type: ctx.config.checker_type,
|
||||||
score: ctxSubtask.subtask.score,
|
score: c.score,
|
||||||
detail: ctx.config.detail ?? true,
|
detail: ctx.config.detail ?? true,
|
||||||
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function judgeCase(c: Case) {
|
|||||||
if (code < 32) message = signals[code];
|
if (code < 32) message = signals[code];
|
||||||
else message = { message: 'Your program returned {0}.', params: [code] };
|
else message = { message: 'Your program returned {0}.', params: [code] };
|
||||||
} else {
|
} else {
|
||||||
const result = parse(resInteractor.stderr, ctxSubtask.subtask.score);
|
const result = parse(resInteractor.stderr, c.score);
|
||||||
status = result.status;
|
status = result.status;
|
||||||
score = result.score;
|
score = result.score;
|
||||||
message = result.message;
|
message = result.message;
|
||||||
|
|||||||
@@ -105,4 +105,5 @@ export interface Case {
|
|||||||
id: number;
|
id: number;
|
||||||
input: string;
|
input: string;
|
||||||
output: string;
|
output: string;
|
||||||
|
score: number;
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
user_stdout: file,
|
user_stdout: file,
|
||||||
user_stderr: { content: '' },
|
user_stderr: { content: '' },
|
||||||
checker_type: ctx.config.checker_type,
|
checker_type: ctx.config.checker_type,
|
||||||
score: ctxSubtask.subtask.score,
|
score: c.score,
|
||||||
detail: ctx.config.detail ?? true,
|
detail: ctx.config.detail ?? true,
|
||||||
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ export interface FileInfo {
|
|||||||
export interface TestCaseConfig {
|
export interface TestCaseConfig {
|
||||||
input: string;
|
input: string;
|
||||||
output: string;
|
output: string;
|
||||||
time?: number;
|
time?: string;
|
||||||
memory?: number;
|
memory?: string;
|
||||||
score?: number;
|
score?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +148,8 @@ export enum SubtaskType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SubtaskConfig {
|
export interface SubtaskConfig {
|
||||||
time?: number;
|
time?: string;
|
||||||
memory?: number;
|
memory?: string;
|
||||||
score?: number;
|
score?: number;
|
||||||
if?: number[];
|
if?: number[];
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
import readYamlCases from '@hydrooj/utils/lib/cases';
|
import readYamlCases from '@hydrooj/utils/lib/cases';
|
||||||
import type { ProblemConfig, ProblemConfigFile } from '../interface';
|
import type { ProblemConfig, ProblemConfigFile } from '../interface';
|
||||||
|
import { normalizeSubtasks, parseMemoryMB, parseTimeMS } from '../utils';
|
||||||
|
|
||||||
export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
||||||
let cfg: ProblemConfigFile = {};
|
const cfg: ProblemConfigFile = typeof config === 'string'
|
||||||
if (typeof config === 'string') {
|
? await readYamlCases(load(config) as Record<string, any>)
|
||||||
// TODO should validate here?
|
: await readYamlCases(config);
|
||||||
cfg = await readYamlCases(load(config) as Record<string, any>);
|
|
||||||
} else if (typeof config === 'object') cfg = await readYamlCases(config);
|
|
||||||
const result: ProblemConfig = {
|
const result: ProblemConfig = {
|
||||||
count: 0,
|
count: 0,
|
||||||
memoryMin: Number.MAX_SAFE_INTEGER,
|
memoryMin: Number.MAX_SAFE_INTEGER,
|
||||||
@@ -18,23 +17,19 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
|||||||
};
|
};
|
||||||
if (cfg.subType) result.subType = cfg.subType;
|
if (cfg.subType) result.subType = cfg.subType;
|
||||||
if (cfg.target) result.target = cfg.target;
|
if (cfg.target) result.target = cfg.target;
|
||||||
if (cfg.subtasks.length) {
|
if (cfg.subtasks?.length) {
|
||||||
for (const subtask of cfg.subtasks) {
|
for (const subtask of normalizeSubtasks(cfg.subtasks as any || [], (i) => i, cfg.time, cfg.memory)) {
|
||||||
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
|
result.memoryMax = Math.max(result.memoryMax, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));
|
||||||
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
|
result.memoryMin = Math.min(result.memoryMin, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));
|
||||||
result.timeMax = Math.max(result.timeMax, subtask.time);
|
result.timeMax = Math.max(result.timeMax, ...subtask.cases.map((i) => parseTimeMS(i.time)));
|
||||||
result.timeMin = Math.min(result.timeMin, subtask.time);
|
result.timeMin = Math.min(result.timeMin, ...subtask.cases.map((i) => parseTimeMS(i.time)));
|
||||||
}
|
}
|
||||||
} else if (cfg.time || cfg.memory) {
|
} else {
|
||||||
if (cfg.time) result.timeMax = result.timeMin = cfg.time as unknown as number;
|
if (cfg.time) result.timeMax = result.timeMin = cfg.time as unknown as number;
|
||||||
if (cfg.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number;
|
if (cfg.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number;
|
||||||
}
|
}
|
||||||
if (result.memoryMax < result.memoryMin) {
|
if (result.memoryMax < result.memoryMin) result.memoryMax = result.memoryMin = 256;
|
||||||
result.memoryMax = result.memoryMin = 256;
|
if (result.timeMax < result.timeMin) result.timeMax = result.timeMin = 1000;
|
||||||
}
|
|
||||||
if (result.timeMax < result.timeMin) {
|
|
||||||
result.timeMax = result.timeMin = 1000;
|
|
||||||
}
|
|
||||||
if (cfg.langs) result.langs = cfg.langs;
|
if (cfg.langs) result.langs = cfg.langs;
|
||||||
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
||||||
if (cfg.filename && result.type === 'default') {
|
if (cfg.filename && result.type === 'default') {
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ const subtasksKey = [
|
|||||||
|
|
||||||
const casesKey = ['time', 'memory', 'input', 'output'];
|
const casesKey = ['time', 'memory', 'input', 'output'];
|
||||||
|
|
||||||
function configYamlFormat(config: ProblemConfigFile) {
|
export function configYamlFormat(config: ProblemConfigFile) {
|
||||||
const formatConfig: ProblemConfigFile = {};
|
const formatConfig: ProblemConfigFile = {};
|
||||||
configKey.forEach((key) => {
|
configKey.forEach((key) => {
|
||||||
if (config[key] !== undefined) {
|
if (config[key] !== undefined) {
|
||||||
if (key === 'subType' && ['single', 'multi'].includes(config[key]) && config.type !== 'submit_answer') return;
|
if (key === 'subType' && ['single', 'multi'].includes(config[key]) && config.type !== 'submit_answer') return;
|
||||||
if (key === 'checker_type' && config.type !== 'default') return;
|
if (key === 'checker_type' && config.type !== 'default') return;
|
||||||
if (key === 'checker'
|
if (key === 'checker'
|
||||||
&& (['default', 'strict'].includes(formatConfig.checker_type) || formatConfig.checker_type === undefined)) 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 === 'subtasks') {
|
if (key === 'subtasks') {
|
||||||
formatConfig[key] = [];
|
formatConfig[key] = [];
|
||||||
@@ -62,7 +62,7 @@ function configYamlFormat(config: ProblemConfigFile) {
|
|||||||
formatConfig[key] = [];
|
formatConfig[key] = [];
|
||||||
config[key].forEach((caseItem) => {
|
config[key].forEach((caseItem) => {
|
||||||
const formatCase: TestCaseConfig = {
|
const formatCase: TestCaseConfig = {
|
||||||
time: 1000, memory: 256, input: '', output: '',
|
time: '1000ms', memory: '256MB', input: '', output: '',
|
||||||
};
|
};
|
||||||
casesKey.forEach((caseKey) => {
|
casesKey.forEach((caseKey) => {
|
||||||
if (caseItem[caseKey] !== undefined) formatCase[caseKey] = caseItem[caseKey];
|
if (caseItem[caseKey] !== undefined) formatCase[caseKey] = caseItem[caseKey];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useSelector, useDispatch } from 'react-redux';
|
|||||||
import { SubtaskConfig } from 'hydrooj/src/interface';
|
import { SubtaskConfig } from 'hydrooj/src/interface';
|
||||||
import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
|
import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
|
||||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||||
import { FormItem, ManagedInput } from './BasicForm';
|
import { FormItem } from './BasicForm';
|
||||||
import { RootState } from './reducer';
|
import { RootState } from './reducer';
|
||||||
import { CasesTable } from './TestCasesTable';
|
import { CasesTable } from './TestCasesTable';
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export function SubtasksTable({ index }) {
|
|||||||
{['id', 'score'].map((key) => (
|
{['id', 'score'].map((key) => (
|
||||||
<td key={key}>
|
<td key={key}>
|
||||||
<input
|
<input
|
||||||
value={subtask[key] || ''}
|
value={subtask[key] ?? ''}
|
||||||
onChange={dispatcher(key)}
|
onChange={dispatcher(key)}
|
||||||
type="number"
|
type="number"
|
||||||
className="textbox"
|
className="textbox"
|
||||||
@@ -80,7 +80,7 @@ export function SubtasksTable({ index }) {
|
|||||||
))}
|
))}
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
value={subtask.type || ''}
|
value={subtask.type ?? 'min'}
|
||||||
onChange={dispatcher('type')}
|
onChange={dispatcher('type')}
|
||||||
className="select"
|
className="select"
|
||||||
>
|
>
|
||||||
@@ -116,20 +116,48 @@ export function SubtasksTable({ index }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GlobalTaskConfig() {
|
||||||
|
const time = useSelector((state: RootState) => state.config.time);
|
||||||
|
const memory = useSelector((state: RootState) => state.config.memory);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
||||||
|
let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
|
||||||
|
if (value === 0) value = '';
|
||||||
|
if (value && suffix) value += suffix;
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key, value });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormItem columns={6} label="Time">
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>ms</Tag>}
|
||||||
|
value={time ? parseTimeMS(time).toString() : ''}
|
||||||
|
placeholder={parseTimeMS('1000ms').toString()}
|
||||||
|
onValueChange={dispatcher('time', 'ms')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem columns={6} label="Memory">
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>MB</Tag>}
|
||||||
|
value={memory ? parseMemoryMB(memory).toString() : ''}
|
||||||
|
placeholder={parseMemoryMB('256m').toString()}
|
||||||
|
onValueChange={dispatcher('memory', 'MB')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function TaskConfig() {
|
export function TaskConfig() {
|
||||||
const len = useSelector((state: RootState) => state.config.subtasks?.length);
|
const len = useSelector((state: RootState) => state.config.subtasks?.length);
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="Task Settings">
|
<FormItem columns={12} label="Task Settings">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={4} label="Time">
|
<GlobalTaskConfig />
|
||||||
<ManagedInput placeholder="Time" formKey="time" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem columns={4} label="Memory">
|
|
||||||
<ManagedInput placeholder="Memory" formKey="memory" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem columns={4} label="Score">
|
|
||||||
<ManagedInput placeholder="Score" formKey="score" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem columns={12} label="TestCases" disableLabel>
|
<FormItem columns={12} label="TestCases" disableLabel>
|
||||||
{len && [...Array(len).keys()].map((i) => <SubtasksTable index={i} key={i} />)}
|
{len && [...Array(len).keys()].map((i) => <SubtasksTable index={i} key={i} />)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import type { ProblemConfigFile } from 'hydrooj/src/interface';
|
import type { ProblemConfigFile } from 'hydrooj/src/interface';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
||||||
|
|
||||||
type State = ProblemConfigFile & { __loaded: boolean };
|
type State = ProblemConfigFile & { __loaded: boolean };
|
||||||
|
|
||||||
@@ -25,23 +26,20 @@ export default function reducer(state = { type: 'default', __loaded: false } as
|
|||||||
}
|
}
|
||||||
case 'CONFIG_AUTOCASES_UPDATE': {
|
case 'CONFIG_AUTOCASES_UPDATE': {
|
||||||
const next = { ...state };
|
const next = { ...state };
|
||||||
const autocases = action.value;
|
const { subtasks } = action;
|
||||||
if (autocases.subtasks.length === 0) next.subtasks = [];
|
for (const subtask of subtasks) {
|
||||||
|
if (subtask.time === parseTimeMS(state.time || '1s')) delete subtask.time;
|
||||||
|
if (subtask.memory === parseMemoryMB(state.memory || '256m')) delete subtask.memory;
|
||||||
|
if (subtask.time) subtask.time += 'ms';
|
||||||
|
if (subtask.memory) subtask.memory += 'MB';
|
||||||
|
}
|
||||||
|
if (subtasks.length === 0) next.subtasks = [];
|
||||||
else {
|
else {
|
||||||
next.subtasks = autocases.subtasks.map((subtask) => (
|
next.subtasks = subtasks.map((subtask) => (
|
||||||
{ ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
|
{ ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
case 'CONFIG_SUBTASKS_SWITCH': {
|
|
||||||
const next = { ...state };
|
|
||||||
next.subtasks = next.cases.map((k, v) => ({
|
|
||||||
id: v, time: k.time, memory: k.memory, cases: [{ input: k.input, output: k.output }],
|
|
||||||
}));
|
|
||||||
if (!next.subtasks.length) next.subtasks.push({ id: 0 });
|
|
||||||
delete next.cases;
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
case 'CONFIG_SUBTASK_UPDATE': {
|
case 'CONFIG_SUBTASK_UPDATE': {
|
||||||
const subtasks = cloneDeep(state.subtasks);
|
const subtasks = cloneDeep(state.subtasks);
|
||||||
const subsubtasks = cloneDeep(state.subtasks[action.id]);
|
const subsubtasks = cloneDeep(state.subtasks[action.id]);
|
||||||
|
|||||||
@@ -52,8 +52,7 @@
|
|||||||
transition: outline-color 0.2s, border-color 0.2s
|
transition: outline-color 0.2s, border-color 0.2s
|
||||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1)
|
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1)
|
||||||
box-shadow: none
|
box-shadow: none
|
||||||
input::placeholder
|
|
||||||
color: inherit
|
|
||||||
|
|
||||||
.data-table
|
.data-table
|
||||||
tr
|
tr
|
||||||
@@ -68,4 +67,7 @@
|
|||||||
background: transparent
|
background: transparent
|
||||||
|
|
||||||
.hover-row.status-bar
|
.hover-row.status-bar
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
|
input::placeholder
|
||||||
|
color: rgba(95,107,124,.6)!important
|
||||||
@@ -7,10 +7,10 @@ import yaml from 'js-yaml';
|
|||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import Dialog, { ConfirmDialog } from 'vj/components/dialog/index';
|
import Dialog, { ConfirmDialog } from 'vj/components/dialog/index';
|
||||||
import download from 'vj/components/zipDownloader';
|
import download from 'vj/components/zipDownloader';
|
||||||
import {
|
import { size, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
||||||
size, readSubtasksFromFiles, parseTimeMS, parseMemoryMB,
|
|
||||||
} from '@hydrooj/utils/lib/common';
|
|
||||||
import tpl from 'vj/utils/tpl';
|
import tpl from 'vj/utils/tpl';
|
||||||
|
import { SubtaskType } from 'hydrooj/src/interface';
|
||||||
|
import { configYamlFormat } from 'vj/components/problemconfig/ProblemConfigEditor';
|
||||||
|
|
||||||
async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any>, type: string) {
|
async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any>, type: string) {
|
||||||
const $section = $(ev.currentTarget).closest('.section--problem-sidebar-testdata');
|
const $section = $(ev.currentTarget).closest('.section--problem-sidebar-testdata');
|
||||||
@@ -146,7 +146,7 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadConfig(config: object) {
|
async function uploadConfig(config: object) {
|
||||||
const configYaml = yaml.dump(config);
|
const configYaml = yaml.dump(configYamlFormat(config));
|
||||||
Notification.info(i18n('Saving file...'));
|
Notification.info(i18n('Saving file...'));
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('filename', 'config.yaml');
|
data.append('filename', 'config.yaml');
|
||||||
@@ -179,20 +179,20 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
// TODO set yaml schema
|
// TODO set yaml schema
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
if (!state.config.__loaded) return;
|
if (!state.config.__loaded) return;
|
||||||
if (state.config.subtasks || state.config.cases) return;
|
if (state.config.cases) {
|
||||||
|
const score = state.config.score * state.config.cases.length;
|
||||||
|
state.config.subtasks = [{ type: 'sum' as SubtaskType, score: score && score < 100 ? score : 100, cases: state.config.cases }];
|
||||||
|
delete state.config.cases;
|
||||||
|
delete state.config.score;
|
||||||
|
}
|
||||||
|
if (state.config.subtasks) return;
|
||||||
const testdata = (state.testdata || []).map((i) => i.name);
|
const testdata = (state.testdata || []).map((i) => i.name);
|
||||||
const checkFile = (file: string) => (testdata.includes(file) ? file : null);
|
const checkFile = (file: string) => (testdata.includes(file) ? file : null);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
const value = readSubtasksFromFiles(testdata, checkFile, state.config, {});
|
const subtasks = readSubtasksFromFiles(testdata, checkFile, state.config);
|
||||||
for (const subtask of value.subtasks) {
|
|
||||||
if (subtask.time === parseTimeMS(state.config.time || '1s')) delete subtask.time;
|
|
||||||
if (subtask.memory === parseMemoryMB(state.config.memory || '256m')) delete subtask.memory;
|
|
||||||
if (subtask.time) subtask.time += 'ms';
|
|
||||||
if (subtask.memory) subtask.memory += 'MB';
|
|
||||||
}
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'CONFIG_AUTOCASES_UPDATE',
|
type: 'CONFIG_AUTOCASES_UPDATE',
|
||||||
value,
|
subtasks,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
|
|||||||
+11
-57
@@ -3,13 +3,10 @@ import { findFileSync, parseMemoryMB, parseTimeMS } from './utils';
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
|
export default async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
|
||||||
const config: any = {
|
const config: any = {
|
||||||
checker_type: 'default',
|
checker_type: cfg.checker_type || 'default',
|
||||||
count: 0,
|
|
||||||
subtasks: [],
|
|
||||||
judge_extra_files: [],
|
judge_extra_files: [],
|
||||||
user_extra_files: [],
|
user_extra_files: [],
|
||||||
};
|
};
|
||||||
config.checker_type = cfg.checker_type || 'default';
|
|
||||||
if (cfg.checker) {
|
if (cfg.checker) {
|
||||||
if (!cfg.checker.includes('.')) {
|
if (!cfg.checker.includes('.')) {
|
||||||
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
||||||
@@ -17,62 +14,19 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
|||||||
if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
|
if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
|
||||||
}
|
}
|
||||||
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
|
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
|
||||||
if (cfg.judge_extra_files) {
|
['judge', 'user'].forEach((n) => {
|
||||||
if (typeof cfg.judge_extra_files === 'string') {
|
const conf = cfg[`${n}_extra_files`];
|
||||||
config.judge_extra_files = [checkFile(cfg.judge_extra_files, 'Cannot find judge extra file {0}.')];
|
if (!conf) return;
|
||||||
} else if (cfg.judge_extra_files instanceof Array) {
|
if (conf instanceof Array) {
|
||||||
for (const file of cfg.judge_extra_files) {
|
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
|
||||||
config.judge_extra_files.push(checkFile(file, 'Cannot find judge extra file {0}.'));
|
} else throw new Error(`Invalid ${n}_extra_files config.`);
|
||||||
}
|
});
|
||||||
} else throw new Error('Invalid judge_extra_files config.');
|
if (cfg.outputs) config.type = cfg.type || 'objective';
|
||||||
}
|
if (cfg.cases?.length) {
|
||||||
if (cfg.user_extra_files) {
|
|
||||||
if (typeof cfg.user_extra_files === 'string') {
|
|
||||||
config.user_extra_files = [checkFile(cfg.user_extra_files, 'Cannot find user extra file {0}.')];
|
|
||||||
} else if (cfg.user_extra_files instanceof Array) {
|
|
||||||
for (const file of cfg.user_extra_files) {
|
|
||||||
config.user_extra_files.push(checkFile(file, 'Cannot find user extra file {0}.'));
|
|
||||||
}
|
|
||||||
} else throw new Error('Invalid user_extra_files config.');
|
|
||||||
}
|
|
||||||
if (cfg.outputs) {
|
|
||||||
config.type = cfg.type || 'objective';
|
|
||||||
} else if (cfg.cases?.length) {
|
|
||||||
config.subtasks = [{
|
config.subtasks = [{
|
||||||
score: +cfg.score || Math.floor(100 / cfg.cases.length),
|
cases: cfg.cases,
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
cases: [],
|
|
||||||
type: 'sum',
|
type: 'sum',
|
||||||
}];
|
}];
|
||||||
for (const c of cfg.cases) {
|
|
||||||
config.count++;
|
|
||||||
config.subtasks[0].cases.push({
|
|
||||||
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
|
|
||||||
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
|
|
||||||
id: config.count,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (cfg.subtasks?.length) {
|
|
||||||
for (const subtask of cfg.subtasks) {
|
|
||||||
const cases = [];
|
|
||||||
for (const c of subtask.cases || []) {
|
|
||||||
config.count++;
|
|
||||||
cases.push({
|
|
||||||
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
|
|
||||||
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
|
|
||||||
id: config.count,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
config.subtasks.push({
|
|
||||||
score: Number.isSafeInteger(+subtask.score) ? +subtask.score : 100,
|
|
||||||
if: subtask.if || [],
|
|
||||||
cases,
|
|
||||||
type: subtask.type || 'min',
|
|
||||||
time: parseTimeMS(subtask.time || cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(subtask.memory || cfg.memory || '256m'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
|||||||
@@ -201,10 +201,45 @@ const SubtaskMatcher: MatchRule[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
|
function* getScore(totalScore: number, count: number) {
|
||||||
const subtask = {};
|
const base = Math.floor(totalScore / count);
|
||||||
for (const s of rst.subtasks || []) if (s.id) subtask[s.id] = s;
|
const extra = count - (100 % count);
|
||||||
const subtasks = [];
|
for (let i = 0; i < count; i++) {
|
||||||
|
if (i >= extra) yield base + 1;
|
||||||
|
else yield base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CaseInput {
|
||||||
|
id?: number;
|
||||||
|
time?: number | string;
|
||||||
|
memory?: number | string;
|
||||||
|
score?: number;
|
||||||
|
input?: string;
|
||||||
|
output?: string;
|
||||||
|
}
|
||||||
|
interface SubtaskInput {
|
||||||
|
cases: CaseInput[];
|
||||||
|
type: 'min' | 'max' | 'sum';
|
||||||
|
time?: number | string;
|
||||||
|
memory?: number | string;
|
||||||
|
score?: number;
|
||||||
|
id?: number;
|
||||||
|
if?: number[];
|
||||||
|
}
|
||||||
|
interface ParsedCase extends CaseInput {
|
||||||
|
time?: number;
|
||||||
|
memory?: number;
|
||||||
|
}
|
||||||
|
interface ParsedSubtask extends SubtaskInput {
|
||||||
|
cases: ParsedCase[];
|
||||||
|
time?: number;
|
||||||
|
memory?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readSubtasksFromFiles(files: string[], checkFile, config) {
|
||||||
|
const subtask: Record<number, ParsedSubtask> = {};
|
||||||
|
for (const s of config.subtasks || []) if (s.id) subtask[s.id] = s;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
for (const rule of SubtaskMatcher) {
|
for (const rule of SubtaskMatcher) {
|
||||||
const data = rule.regex.exec(file);
|
const data = rule.regex.exec(file);
|
||||||
@@ -212,13 +247,13 @@ export function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
|
|||||||
const sid = rule.subtask(data);
|
const sid = rule.subtask(data);
|
||||||
const c = { input: file, output: '', id: rule.id(data) };
|
const c = { input: file, output: '', id: rule.id(data) };
|
||||||
for (const func of rule.output) {
|
for (const func of rule.output) {
|
||||||
if (cfg.noOutputFile) c.output = '/dev/null';
|
if (config.noOutputFile) c.output = '/dev/null';
|
||||||
else c.output = func(data);
|
else c.output = func(data);
|
||||||
if (c.output === '/dev/null' || checkFile(c.output)) {
|
if (c.output === '/dev/null' || checkFile(c.output)) {
|
||||||
if (!subtask[sid]) {
|
if (!subtask[sid]) {
|
||||||
subtask[sid] = {
|
subtask[sid] = {
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
time: parseTimeMS(config.time || '1s'),
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
memory: parseMemoryMB(config.memory || '256m'),
|
||||||
type: rule.preferredScorerType,
|
type: rule.preferredScorerType,
|
||||||
cases: [c],
|
cases: [c],
|
||||||
};
|
};
|
||||||
@@ -229,24 +264,50 @@ export function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const i in subtask) {
|
const subtaskScore = getScore(100, Object.keys(subtask).length);
|
||||||
subtask[i].cases.sort((a, b) => (a.id - b.id));
|
return Object.keys(subtask).map((i) =>
|
||||||
subtasks.push(subtask[i]);
|
({ ...subtask[i], score: subtaskScore.next().value as number }),
|
||||||
}
|
);
|
||||||
subtasks.sort((a, b) => (a.id - b.id));
|
}
|
||||||
const base = Math.floor(100 / subtasks.length);
|
|
||||||
const extra = subtasks.length - (100 % subtasks.length);
|
type NormalizedCase = Required<ParsedCase>;
|
||||||
const config = { count: 0, subtasks };
|
interface NormalizedSubtask extends Required<ParsedSubtask> {
|
||||||
const keys = Object.keys(subtask);
|
cases: NormalizedCase[];
|
||||||
for (let i = 0; i < keys.length; i++) {
|
}
|
||||||
if (i >= extra) subtask[keys[i]].score = base + 1;
|
|
||||||
else subtask[keys[i]].score = base;
|
export function normalizeSubtasks(
|
||||||
for (const j of subtask[keys[i]].cases) {
|
subtasks: SubtaskInput[], checkFile: (name: string, errMsg: string) => string,
|
||||||
config.count++;
|
time: number | string, memory: number | string,
|
||||||
j.input = checkFile(j.input);
|
): NormalizedSubtask[] {
|
||||||
j.output = checkFile(j.output);
|
subtasks.sort((a, b) => (a.id - b.id));
|
||||||
j.id = config.count;
|
const subtaskScore = getScore(100, subtasks.length);
|
||||||
}
|
let id = 0;
|
||||||
}
|
let count = 0;
|
||||||
return config;
|
return subtasks.map((s) => {
|
||||||
|
id++;
|
||||||
|
s.cases.sort((a, b) => (a.id - b.id));
|
||||||
|
const score = subtaskScore.next().value as number;
|
||||||
|
const caseScore = getScore(score, s.cases.length);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
score,
|
||||||
|
type: 'min',
|
||||||
|
if: [],
|
||||||
|
...s,
|
||||||
|
time: parseTimeMS(s.time || time),
|
||||||
|
memory: parseMemoryMB(s.memory || memory),
|
||||||
|
cases: s.cases.map((c) => {
|
||||||
|
count++;
|
||||||
|
return {
|
||||||
|
id: count,
|
||||||
|
score: s.type === 'sum' ? caseScore.next().value as number : score,
|
||||||
|
...c,
|
||||||
|
time: parseTimeMS(c.time || s.time || time),
|
||||||
|
memory: parseMemoryMB(c.memory || s.memory || memory),
|
||||||
|
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null',
|
||||||
|
output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
|
||||||
|
};
|
||||||
|
}) as NormalizedCase[],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user