|
|
|
import fs from 'fs-extra';
|
|
|
|
import path from 'path';
|
|
|
|
import yaml from 'js-yaml';
|
|
|
|
import { max, 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: /^([^\d]*)(\d+).(in|txt)$/,
|
|
|
|
output: [
|
|
|
|
(a) => `${a[1] + a[2]}.out`,
|
|
|
|
(a) => `${a[1] + a[2]}.ans`,
|
|
|
|
(a) => `${a[1] + a[2]}.out`.replace(/input/g, 'output'),
|
|
|
|
(a) => `${a[1] + a[2]}.txt`.replace(/input/g, 'output'),
|
|
|
|
],
|
|
|
|
id: (a) => +a[2],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
reg: /^([^\d]*)\.in(\d+)$/,
|
|
|
|
output: [
|
|
|
|
(a) => `${a[1]}.ou${a[2]}`,
|
|
|
|
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
|
|
|
|
],
|
|
|
|
id: (a) => +a[2],
|
|
|
|
},
|
|
|
|
];
|
|
|
|
const RE1: Re1[] = [
|
|
|
|
{
|
|
|
|
reg: /^([^\d]*)([0-9]+)-([0-9]+).in$/,
|
|
|
|
output: [(a) => `${a[1] + a[2]}-${a[3]}.out`],
|
|
|
|
subtask: (a) => +a[2],
|
|
|
|
id: (a) => +a[3],
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
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: '', id: REG.id(data) };
|
|
|
|
for (const func of REG.output) {
|
|
|
|
if (cfg.noOutputFile) c.output = '/dev/null';
|
|
|
|
else c.output = func(data);
|
|
|
|
if (c.output === '/dev/null' || 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, rst) {
|
|
|
|
const subtask = {};
|
|
|
|
for (const s of rst.subtasks) if (s.id) subtask[s.id] = s;
|
|
|
|
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: '', id: REG.id(data) };
|
|
|
|
for (const func of REG.output) {
|
|
|
|
if (cfg.noOutputFile) c.output = '/dev/null';
|
|
|
|
else c.output = func(data);
|
|
|
|
if (c.output === '/dev/null' || 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 if (!subtask[REG.subtask(data)].cases) subtask[REG.subtask(data)].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, 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}`));
|
|
|
|
}
|
|
|
|
let result = await read0(folder, files, checkFile, cfg);
|
|
|
|
if (!result.count) result = await read1(folder, files, checkFile, cfg, rst);
|
|
|
|
Object.assign(config, result);
|
|
|
|
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.type === 'submit_answer' && !config.outputs.length) throw new FormatError('Problem data not found.');
|
|
|
|
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]);
|
|
|
|
}
|
|
|
|
const memMax = max(config.subtasks.map((subtask) => subtask.memory));
|
|
|
|
if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max');
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
let auto = !result.outputs?.length;
|
|
|
|
if (auto) {
|
|
|
|
if (result.subtasks.length && Math.sum(result.subtasks.map((subtask) => subtask.cases.length))) {
|
|
|
|
auto = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (auto) {
|
|
|
|
const c = await readAutoCases(folder, args, config, result);
|
|
|
|
result.subtasks = c.subtasks;
|
|
|
|
result.count = c.count;
|
|
|
|
}
|
|
|
|
if ((!args.key) || args.key !== result.key) isValidConfig(result);
|
|
|
|
return result;
|
|
|
|
}
|