You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/packages/hydrojudge/src/cases.ts

238 lines
8.0 KiB
TypeScript

import fs from 'fs-extra';
import path from 'path';
import yaml from 'js-yaml';
import { sum } from 'lodash';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
import { changeErrorType } from '@hydrooj/utils/lib/utils';
import { FormatError, SystemError } from './error';
import { parseTimeMS, parseMemoryMB, ensureFile } from './utils';
import { getConfig } from './config';
interface Re0 {
reg: RegExp,
output: (a: RegExpExecArray) => string,
id: (a: RegExpExecArray) => number,
}
interface Re1 extends Re0 {
subtask: (a: RegExpExecArray) => number,
}
const RE0: Re0[] = [
{
reg: /^([a-z+_\-A-Z]*)([0-9]+).in$/,
output: (a) => `${a[1] + a[2]}.out`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^([a-z+_\-A-Z]*)([0-9]+).in$/,
output: (a) => `${a[1] + a[2]}.ans`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^([a-z+_\-A-Z0-9]*)\.in([0-9]+)$/,
output: (a) => `${a[1]}.ou${a[2]}`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^(input)([0-9]+).txt$/,
output: (a) => `output${a[2]}.txt`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^input\/([a-z+_\-A-Z]*)([0-9]+).in$/,
output: (a) => `output/${a[1] + a[2]}.out`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^input\/([a-z+_\-A-Z]*)([0-9]+).in$/,
output: (a) => `output/${a[1] + a[2]}.ans`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^input\/([a-z+_\-A-Z0-9]*)\.in([0-9]+)$/,
output: (a) => `output/${a[1]}.ou${a[2]}`,
id: (a) => parseInt(a[2], 10),
},
{
reg: /^input\/(input)([0-9]+).txt$/,
output: (a) => `output/output${a[2]}.txt`,
id: (a) => parseInt(a[2], 10),
},
];
const RE1: Re1[] = [
{
reg: /^([a-z+_\-A-Z]*)([0-9]+)-([0-9]+).in$/,
output: (a) => `${a[1] + a[2]}-${a[3]}.out`,
subtask: (a) => parseInt(a[2], 10),
id: (a) => parseInt(a[3], 10),
},
];
async function read0(folder: string, files: string[], checkFile, cfg) {
const cases = [];
for (const file of files) {
for (const REG of RE0) {
if (REG.reg.test(file)) {
const data = REG.reg.exec(file);
const c = { input: file, output: REG.output(data), id: REG.id(data) };
if (fs.existsSync(path.resolve(folder, c.output))) {
cases.push(c);
break;
}
}
}
}
cases.sort((a, b) => (a.id - b.id));
const extra = cases.length - (100 % cases.length);
const config = {
count: 0,
subtasks: [{
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
type: 'sum',
cases: [],
score: Math.floor(100 / cases.length),
}],
};
for (let i = 0; i < extra; i++) {
config.count++;
config.subtasks[0].cases.push({
id: config.count,
input: checkFile(cases[i].input),
output: checkFile(cases[i].output),
});
}
if (extra < cases.length) {
config.subtasks.push({
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
type: 'sum',
cases: [],
score: Math.floor(100 / cases.length) + 1,
});
for (let i = extra; i < cases.length; i++) {
config.count++;
config.subtasks[1].cases.push({
id: config.count,
input: checkFile(cases[i].input),
output: checkFile(cases[i].output),
});
}
}
return config;
}
async function read1(folder: string, files: string[], checkFile, cfg) {
const subtask = {};
const subtasks = [];
for (const file of files) {
for (const REG of RE1) {
if (REG.reg.test(file)) {
const data = REG.reg.exec(file);
const c = { input: file, output: REG.output(data), id: REG.id(data) };
if (fs.existsSync(path.resolve(folder, c.output))) {
if (!subtask[REG.subtask(data)]) {
subtask[REG.subtask(data)] = [{
time: parseTimeMS(cfg.time || '1s'),
memory: parseMemoryMB(cfg.memory || '256m'),
type: 'min',
cases: [c],
}];
} else subtask[REG.subtask(data)].cases.push(c);
break;
}
}
}
}
for (const i in subtask) {
subtask[i].cases.sort((a, b) => (a.id - b.id));
subtasks.push(subtask[i]);
}
const base = Math.floor(100 / subtasks.length);
const extra = subtasks.length - (100 % subtasks.length);
const config = { count: 0, subtasks };
const keys = Object.keys(subtask);
for (let i = 0; i < keys.length; i++) {
if (extra < i) subtask[keys[i]].score = base;
else subtask[keys[i]].score = base + 1;
for (const j of subtask[keys[i]].cases) {
config.count++;
j.input = checkFile(j.input);
j.output = checkFile(j.output);
j.id = config.count;
}
}
return config;
}
async function readAutoCases(folder, { next }, cfg) {
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}`));
}
let result = await read0(folder, files, checkFile, cfg);
if (!result.count) result = await read1(folder, files, checkFile, cfg);
Object.assign(config, result);
next({ message: { message: 'Found {0} testcases.', params: [config.count] } });
} catch (e) {
throw new SystemError('Cannot parse testdata.', [e]);
}
return config;
}
function isValidConfig(config) {
if (config.count > (getConfig('testcases_max') || 100)) {
throw new FormatError('Too many testcases. Cancelled.');
}
const total_time = sum(config.subtasks.map((subtask) => subtask.time * subtask.cases.length));
if (total_time > (getConfig('total_time_limit') || 60) * 1000) {
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
}
}
export default async function readCases(folder: string, cfg: Record<string, any> = {}, args) {
const iniConfig = path.resolve(folder, 'config.ini');
const yamlConfig = path.resolve(folder, 'config.yaml');
const ymlConfig = path.resolve(folder, 'config.yml');
let config;
if (fs.existsSync(yamlConfig)) {
config = { ...yaml.load(fs.readFileSync(yamlConfig).toString()) as object, ...cfg };
} else if (fs.existsSync(ymlConfig)) {
config = { ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object, ...cfg };
} else if (fs.existsSync(iniConfig)) {
try {
config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg };
} catch (e) {
throw changeErrorType(e, FormatError);
}
} else config = cfg;
let result;
try {
result = await readYamlCases(config, ensureFile(folder));
} catch (e) {
throw changeErrorType(e, FormatError);
}
if (!(result.outputs || result.subtasks.length)) {
const c = await readAutoCases(folder, args, cfg);
config.subtasks = c.subtasks;
config.count = c.count;
}
isValidConfig(result);
return result;
}