From 539a1c44ce42c407edc8acf494d130ae1e6507bb Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 5 May 2022 21:51:12 +0800 Subject: [PATCH] judge: use case.score & normalizeSubtask API --- packages/hydrojudge/src/cases.ts | 89 ++++++-------- packages/hydrojudge/src/judge/default.ts | 2 +- packages/hydrojudge/src/judge/interactive.ts | 2 +- packages/hydrojudge/src/judge/interface.ts | 1 + .../hydrojudge/src/judge/submit_answer.ts | 2 +- packages/hydrooj/src/interface.ts | 8 +- packages/hydrooj/src/lib/testdataConfig.ts | 31 ++--- .../problemconfig/ProblemConfigEditor.tsx | 6 +- .../components/problemconfig/SubtaskTable.tsx | 52 ++++++-- .../problemconfig/reducer/config.ts | 22 ++-- .../ui-default/pages/problem_config.page.styl | 8 +- .../ui-default/pages/problem_config.page.tsx | 26 ++-- packages/utils/lib/cases.ts | 68 ++--------- packages/utils/lib/common.ts | 115 ++++++++++++++---- 14 files changed, 231 insertions(+), 201 deletions(-) diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index d81ab2e17..ca824114b 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -3,40 +3,12 @@ import fs from 'fs-extra'; import yaml from 'js-yaml'; import { max, sum } from 'lodash'; 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 { getConfig } from './config'; import { FormatError, SystemError } from './error'; 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) { if (config.count > (getConfig('testcases_max') || 100)) { 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 = {}, args) { const iniConfig = path.resolve(folder, 'config.ini'); const yamlConfig = path.resolve(folder, 'config.yaml'); const ymlConfig = path.resolve(folder, 'config.yml'); - const config = { ...cfg }; - 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)) { - try { - Object.assign(config, convertIniConfig(fs.readFileSync(iniConfig).toString())); - } catch (e) { - throw changeErrorType(e, FormatError); - } - } - let result; + const config: Record = { + checker_type: 'default', + count: 0, + subtasks: [], + judge_extra_files: [], + user_extra_files: [], + ...cfg, + }; 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) { throw changeErrorType(e, FormatError); } - let cases = result.outputs?.length || 0; - cases += Math.sum((result.subtasks || []).map((subtask) => subtask.cases.length)); - if (!cases) { - const c = await readAutoCases(folder, args, config, result); - result.subtasks = c.subtasks; - result.count = c.count; + const checkFile = ensureFile(folder); + const result = await readYamlCases(config, checkFile) + .catch((e) => { throw changeErrorType(e, FormatError); }); + result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length)); + if (!result.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) isValidConfig(result); return result; diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index 14a589783..976cb5867 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -76,7 +76,7 @@ function judgeCase(c: Case, sid: string) { user_stdout: stdout, user_stderr: stderr, checker_type: ctx.config.checker_type, - score: ctxSubtask.subtask.score, + score: c.score, detail: ctx.config.detail ?? true, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, }); diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index e99b33713..cc7d492a4 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -50,7 +50,7 @@ function judgeCase(c: Case) { if (code < 32) message = signals[code]; else message = { message: 'Your program returned {0}.', params: [code] }; } else { - const result = parse(resInteractor.stderr, ctxSubtask.subtask.score); + const result = parse(resInteractor.stderr, c.score); status = result.status; score = result.score; message = result.message; diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts index b5f5f45d5..448ae7cef 100644 --- a/packages/hydrojudge/src/judge/interface.ts +++ b/packages/hydrojudge/src/judge/interface.ts @@ -105,4 +105,5 @@ export interface Case { id: number; input: string; output: string; + score: number; } diff --git a/packages/hydrojudge/src/judge/submit_answer.ts b/packages/hydrojudge/src/judge/submit_answer.ts index 67096be5c..d0623a795 100644 --- a/packages/hydrojudge/src/judge/submit_answer.ts +++ b/packages/hydrojudge/src/judge/submit_answer.ts @@ -74,7 +74,7 @@ function judgeCase(c: Case, sid: string) { user_stdout: file, user_stderr: { content: '' }, checker_type: ctx.config.checker_type, - score: ctxSubtask.subtask.score, + score: c.score, detail: ctx.config.detail ?? true, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, }); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index f2772bf57..dc33df041 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -129,8 +129,8 @@ export interface FileInfo { export interface TestCaseConfig { input: string; output: string; - time?: number; - memory?: number; + time?: string; + memory?: string; score?: number; } @@ -148,8 +148,8 @@ export enum SubtaskType { } export interface SubtaskConfig { - time?: number; - memory?: number; + time?: string; + memory?: string; score?: number; if?: number[]; id?: number; diff --git a/packages/hydrooj/src/lib/testdataConfig.ts b/packages/hydrooj/src/lib/testdataConfig.ts index 3607b5d06..c6a1fde45 100644 --- a/packages/hydrooj/src/lib/testdataConfig.ts +++ b/packages/hydrooj/src/lib/testdataConfig.ts @@ -1,13 +1,12 @@ import { load } from 'js-yaml'; import readYamlCases from '@hydrooj/utils/lib/cases'; import type { ProblemConfig, ProblemConfigFile } from '../interface'; +import { normalizeSubtasks, parseMemoryMB, parseTimeMS } from '../utils'; export async function parseConfig(config: string | ProblemConfigFile = {}) { - let cfg: ProblemConfigFile = {}; - if (typeof config === 'string') { - // TODO should validate here? - cfg = await readYamlCases(load(config) as Record); - } else if (typeof config === 'object') cfg = await readYamlCases(config); + const cfg: ProblemConfigFile = typeof config === 'string' + ? await readYamlCases(load(config) as Record) + : await readYamlCases(config); const result: ProblemConfig = { count: 0, 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.target) result.target = cfg.target; - if (cfg.subtasks.length) { - for (const subtask of cfg.subtasks) { - result.memoryMax = Math.max(result.memoryMax, subtask.memory); - result.memoryMin = Math.min(result.memoryMin, subtask.memory); - result.timeMax = Math.max(result.timeMax, subtask.time); - result.timeMin = Math.min(result.timeMin, subtask.time); + if (cfg.subtasks?.length) { + for (const subtask of normalizeSubtasks(cfg.subtasks as any || [], (i) => i, cfg.time, cfg.memory)) { + result.memoryMax = Math.max(result.memoryMax, ...subtask.cases.map((i) => parseMemoryMB(i.memory))); + result.memoryMin = Math.min(result.memoryMin, ...subtask.cases.map((i) => parseMemoryMB(i.memory))); + result.timeMax = Math.max(result.timeMax, ...subtask.cases.map((i) => parseTimeMS(i.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.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number; } - if (result.memoryMax < result.memoryMin) { - result.memoryMax = result.memoryMin = 256; - } - if (result.timeMax < result.timeMin) { - result.timeMax = result.timeMin = 1000; - } + if (result.memoryMax < result.memoryMin) result.memoryMax = result.memoryMin = 256; + 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') { diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx index 68774fc7c..6da3b1baf 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx @@ -38,14 +38,14 @@ const subtasksKey = [ const casesKey = ['time', 'memory', 'input', 'output']; -function configYamlFormat(config: ProblemConfigFile) { +export function configYamlFormat(config: ProblemConfigFile) { const formatConfig: ProblemConfigFile = {}; configKey.forEach((key) => { if (config[key] !== undefined) { if (key === 'subType' && ['single', 'multi'].includes(config[key]) && config.type !== 'submit_answer') return; if (key === 'checker_type' && config.type !== 'default') return; 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 === 'subtasks') { formatConfig[key] = []; @@ -62,7 +62,7 @@ function configYamlFormat(config: ProblemConfigFile) { formatConfig[key] = []; config[key].forEach((caseItem) => { const formatCase: TestCaseConfig = { - time: 1000, memory: 256, input: '', output: '', + time: '1000ms', memory: '256MB', input: '', output: '', }; casesKey.forEach((caseKey) => { if (caseItem[caseKey] !== undefined) formatCase[caseKey] = caseItem[caseKey]; diff --git a/packages/ui-default/components/problemconfig/SubtaskTable.tsx b/packages/ui-default/components/problemconfig/SubtaskTable.tsx index 0f3e68148..145b23e7f 100644 --- a/packages/ui-default/components/problemconfig/SubtaskTable.tsx +++ b/packages/ui-default/components/problemconfig/SubtaskTable.tsx @@ -5,7 +5,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { SubtaskConfig } from 'hydrooj/src/interface'; import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common'; import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete'; -import { FormItem, ManagedInput } from './BasicForm'; +import { FormItem } from './BasicForm'; import { RootState } from './reducer'; import { CasesTable } from './TestCasesTable'; @@ -71,7 +71,7 @@ export function SubtasksTable({ index }) { {['id', 'score'].map((key) => (