mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
move delarations to @hydrooj/common
This commit is contained in:
1 parent
100ccf718f
commit
4a33816a37
49 files changed
+496
-465
No files matched your search
@@ -1,60 +0,0 @@
|
||||
import { findFileSync, parseMemoryMB, parseTimeMS } from './utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
|
||||
const config: any = {
|
||||
checker_type: cfg.checker_type || 'default',
|
||||
judge_extra_files: [],
|
||||
user_extra_files: [],
|
||||
};
|
||||
if (cfg.type === 'objective') {
|
||||
if (cfg.checker || cfg.interactor || cfg.validator) {
|
||||
throw new Error('You cannot use checker, interactor or validator for objective questions');
|
||||
}
|
||||
} else {
|
||||
if (cfg.checker) {
|
||||
if (!cfg.checker.includes('.')) {
|
||||
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
||||
}
|
||||
config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.');
|
||||
}
|
||||
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
|
||||
if (cfg.manager) config.manager = checkFile(cfg.manager, 'Cannot find Manager {0}.');
|
||||
if (cfg.validator) config.validator = checkFile(cfg.validator, 'Cannot find validator {0}.');
|
||||
for (const n of ['judge', 'user']) {
|
||||
const conf = cfg[`${n}_extra_files`];
|
||||
if (!conf) continue;
|
||||
if (conf instanceof Array) {
|
||||
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
|
||||
} else throw new Error(`Invalid ${n}_extra_files config.`);
|
||||
}
|
||||
}
|
||||
if (cfg.cases?.length) {
|
||||
config.subtasks = [{
|
||||
cases: cfg.cases,
|
||||
type: 'sum',
|
||||
}];
|
||||
}
|
||||
if (cfg.time) config.time = parseTimeMS(cfg.time);
|
||||
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
||||
return Object.assign(cfg, config);
|
||||
}
|
||||
|
||||
export function convertIniConfig(ini: string) {
|
||||
const f = ini.split('\n');
|
||||
const count = parseInt(f[0], 10);
|
||||
const res = { subtasks: [] };
|
||||
for (let i = 1; i <= count; i++) {
|
||||
if (!f[i]?.trim()) throw new Error('Testdata count incorrect.');
|
||||
const [input, output, time, score, memory] = f[i].split('|');
|
||||
const cur = {
|
||||
cases: [{ input, output }],
|
||||
score: parseInt(score, 10),
|
||||
time: `${time}s`,
|
||||
memory: '256m',
|
||||
};
|
||||
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
|
||||
res.subtasks.push(cur);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -236,159 +236,3 @@ export function sortFiles(files: Record<string, any>[] | string[], key = '_id')
|
||||
});
|
||||
return result.map((x) => (isString ? x.name : (delete x._weights && x)));
|
||||
}
|
||||
|
||||
interface MatchRule {
|
||||
regex: RegExp;
|
||||
output: (a: RegExpExecArray) => string[];
|
||||
id: (a: RegExpExecArray) => number;
|
||||
subtask: (a: RegExpExecArray) => number;
|
||||
preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum';
|
||||
}
|
||||
|
||||
const SubtaskMatcher: MatchRule[] = [
|
||||
{
|
||||
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/,
|
||||
output: (a) => ['out', 'ans']
|
||||
.flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`])
|
||||
.flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')])
|
||||
.concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null),
|
||||
id: (a) => +a[4],
|
||||
subtask: (a) => +(a[3] || 1),
|
||||
preferredScorerType: (a) => (a[3] ? 'min' : 'sum'),
|
||||
},
|
||||
{
|
||||
regex: /^([^\d]*)\.(in|IN)(\d+)$/,
|
||||
output: (a) => [
|
||||
`${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`,
|
||||
`${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`,
|
||||
].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]),
|
||||
id: (a) => +a[2],
|
||||
subtask: () => 1,
|
||||
preferredScorerType: () => 'sum',
|
||||
},
|
||||
{
|
||||
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/,
|
||||
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`),
|
||||
id: (a) => +a[4],
|
||||
subtask: (a) => +a[2],
|
||||
preferredScorerType: () => 'min',
|
||||
},
|
||||
{
|
||||
regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/,
|
||||
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`),
|
||||
id: (a) => +a[2],
|
||||
subtask: () => 1,
|
||||
preferredScorerType: () => 'sum',
|
||||
},
|
||||
];
|
||||
|
||||
function* getScore(totalScore: number, count: number) {
|
||||
const base = Math.floor(totalScore / count);
|
||||
const extra = count - (totalScore % count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (i >= extra) yield base + 1;
|
||||
else yield base;
|
||||
}
|
||||
}
|
||||
|
||||
interface ParsedCase {
|
||||
id?: number;
|
||||
time?: number | string;
|
||||
memory?: number | string;
|
||||
score?: number;
|
||||
input?: string;
|
||||
output?: string;
|
||||
}
|
||||
interface ParsedSubtask {
|
||||
cases: ParsedCase[];
|
||||
type: 'min' | 'max' | 'sum';
|
||||
time?: number | string;
|
||||
memory?: number | string;
|
||||
score?: number;
|
||||
id?: number;
|
||||
if?: number[];
|
||||
}
|
||||
|
||||
export function readSubtasksFromFiles(files: string[], config) {
|
||||
const subtask: Record<number, ParsedSubtask> = {};
|
||||
for (const s of config.subtasks || []) if (s.id) subtask[s.id] = s;
|
||||
for (const file of files) {
|
||||
let match = false;
|
||||
for (const rule of SubtaskMatcher) {
|
||||
const data = rule.regex.exec(file);
|
||||
if (!data) continue;
|
||||
const sid = rule.subtask(data);
|
||||
const c = { input: file, output: '', id: rule.id(data) };
|
||||
const type = rule.preferredScorerType(data);
|
||||
const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i);
|
||||
for (const output of outputs) {
|
||||
if (output === file) continue;
|
||||
if (output === '/dev/null' || files.includes(output)) {
|
||||
match = true;
|
||||
c.output = output;
|
||||
if (!subtask[sid]) {
|
||||
subtask[sid] = {
|
||||
time: config.time,
|
||||
memory: config.memory,
|
||||
type,
|
||||
cases: [c],
|
||||
id: sid,
|
||||
};
|
||||
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
|
||||
else subtask[sid].cases.push(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) break;
|
||||
}
|
||||
}
|
||||
return Object.values(subtask);
|
||||
}
|
||||
|
||||
export interface NormalizedCase extends Required<ParsedCase> {
|
||||
time: number;
|
||||
memory: number;
|
||||
}
|
||||
export interface NormalizedSubtask extends Required<ParsedSubtask> {
|
||||
cases: NormalizedCase[];
|
||||
time: number;
|
||||
memory: number;
|
||||
}
|
||||
|
||||
export function normalizeSubtasks(
|
||||
subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string,
|
||||
time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false,
|
||||
timeRate = 1, memoryRate = 1,
|
||||
): NormalizedSubtask[] {
|
||||
subtasks.sort((a, b) => (a.id - b.id));
|
||||
const subtaskScore = getScore(
|
||||
Math.max(100 - Math.sum(subtasks.map((i) => i.score || 0)), 0),
|
||||
subtasks.filter((i) => !i.score).length,
|
||||
);
|
||||
return subtasks.map((s, id) => {
|
||||
s.cases.sort((a, b) => (a.id - b.id));
|
||||
const score = s.score || subtaskScore.next().value as number;
|
||||
const caseScore = getScore(
|
||||
Math.max(score - Math.sum(s.cases.map((i) => i.score || 0)), 0),
|
||||
s.cases.filter((i) => !i.score).length,
|
||||
);
|
||||
return {
|
||||
id: id + 1,
|
||||
type: 'min',
|
||||
if: [],
|
||||
...s,
|
||||
score,
|
||||
time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,
|
||||
memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate,
|
||||
cases: s.cases.map((c, index) => ({
|
||||
id: index + 1,
|
||||
...c,
|
||||
score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score),
|
||||
time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate,
|
||||
memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate,
|
||||
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[],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export interface LangConfig {
|
||||
disabled?: boolean;
|
||||
compile?: string;
|
||||
execute: string;
|
||||
code_file: string;
|
||||
highlight: string;
|
||||
monaco: string;
|
||||
time_limit_rate: number;
|
||||
memory_limit_rate: number;
|
||||
address_space_limit?: boolean;
|
||||
process_limit?: number;
|
||||
display: string;
|
||||
target?: string;
|
||||
key: string;
|
||||
hidden: boolean;
|
||||
isBinary?: boolean;
|
||||
analysis?: string;
|
||||
/** @deprecated */
|
||||
remote?: string;
|
||||
validAs?: Record<string, string>;
|
||||
/** @deprecated */
|
||||
pretest?: string | false;
|
||||
comment?: string | [string, string];
|
||||
compile_time_limit?: number;
|
||||
compile_memory_limit?: number;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export function parseLang(config: string): Record<string, LangConfig> {
|
||||
const file = yaml.load(config) as Record<string, LangConfig>;
|
||||
if (typeof file === 'undefined' || typeof file === 'string' || typeof file === 'number') throw new Error();
|
||||
for (const key of Object.keys(file)) if (key.startsWith('_')) delete file[key];
|
||||
for (const key in file) {
|
||||
const entry = file[key];
|
||||
if (key.includes('.')) {
|
||||
const base = key.split('.')[0];
|
||||
const baseCfg = file[base];
|
||||
for (const bkey in baseCfg || {}) {
|
||||
if (!(bkey in entry)) entry[bkey] = baseCfg[bkey];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key in file) {
|
||||
const entry = file[key];
|
||||
entry.highlight ||= key;
|
||||
entry.monaco ||= entry.highlight;
|
||||
entry.time_limit_rate ||= 1;
|
||||
entry.memory_limit_rate ||= 1;
|
||||
entry.code_file ||= `foo.${key}`;
|
||||
entry.execute ||= '/w/foo';
|
||||
entry.key = key;
|
||||
entry.hidden ||= false;
|
||||
entry.disabled ||= false;
|
||||
entry.isBinary ||= false;
|
||||
entry.validAs ||= {};
|
||||
}
|
||||
return file;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
export enum STATUS {
|
||||
STATUS_WAITING = 0,
|
||||
STATUS_ACCEPTED = 1,
|
||||
STATUS_WRONG_ANSWER = 2,
|
||||
STATUS_TIME_LIMIT_EXCEEDED = 3,
|
||||
STATUS_MEMORY_LIMIT_EXCEEDED = 4,
|
||||
STATUS_OUTPUT_LIMIT_EXCEEDED = 5,
|
||||
STATUS_RUNTIME_ERROR = 6,
|
||||
STATUS_COMPILE_ERROR = 7,
|
||||
STATUS_SYSTEM_ERROR = 8,
|
||||
STATUS_CANCELED = 9,
|
||||
STATUS_ETC = 10,
|
||||
STATUS_HACKED = 11,
|
||||
STATUS_JUDGING = 20,
|
||||
STATUS_COMPILING = 21,
|
||||
STATUS_FETCHED = 22,
|
||||
STATUS_IGNORED = 30,
|
||||
STATUS_FORMAT_ERROR = 31,
|
||||
STATUS_HACK_SUCCESSFUL = 32,
|
||||
STATUS_HACK_UNSUCCESSFUL = 33,
|
||||
}
|
||||
|
||||
export const STATUS_TEXTS: Record<STATUS, string> = {
|
||||
[STATUS.STATUS_WAITING]: 'Waiting',
|
||||
[STATUS.STATUS_ACCEPTED]: 'Accepted',
|
||||
[STATUS.STATUS_WRONG_ANSWER]: 'Wrong Answer',
|
||||
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Time Exceeded',
|
||||
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Memory Exceeded',
|
||||
[STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'Output Exceeded',
|
||||
[STATUS.STATUS_RUNTIME_ERROR]: 'Runtime Error',
|
||||
[STATUS.STATUS_COMPILE_ERROR]: 'Compile Error',
|
||||
[STATUS.STATUS_SYSTEM_ERROR]: 'System Error',
|
||||
[STATUS.STATUS_CANCELED]: 'Cancelled',
|
||||
[STATUS.STATUS_ETC]: 'Unknown Error',
|
||||
[STATUS.STATUS_HACKED]: 'Hacked',
|
||||
[STATUS.STATUS_JUDGING]: 'Running',
|
||||
[STATUS.STATUS_COMPILING]: 'Compiling',
|
||||
[STATUS.STATUS_FETCHED]: 'Fetched',
|
||||
[STATUS.STATUS_IGNORED]: 'Ignored',
|
||||
[STATUS.STATUS_FORMAT_ERROR]: 'Format Error',
|
||||
[STATUS.STATUS_HACK_SUCCESSFUL]: 'Hack Successful',
|
||||
[STATUS.STATUS_HACK_UNSUCCESSFUL]: 'Hack Unsuccessful',
|
||||
};
|
||||
|
||||
export const STATUS_SHORT_TEXTS: Partial<Record<STATUS, string>> = {
|
||||
[STATUS.STATUS_ACCEPTED]: 'AC',
|
||||
[STATUS.STATUS_WRONG_ANSWER]: 'WA',
|
||||
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'TLE',
|
||||
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'MLE',
|
||||
[STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'OLE',
|
||||
[STATUS.STATUS_RUNTIME_ERROR]: 'RE',
|
||||
[STATUS.STATUS_COMPILE_ERROR]: 'CE',
|
||||
[STATUS.STATUS_SYSTEM_ERROR]: 'SE',
|
||||
[STATUS.STATUS_CANCELED]: 'IGN',
|
||||
[STATUS.STATUS_HACKED]: 'HK',
|
||||
[STATUS.STATUS_IGNORED]: 'IGN',
|
||||
[STATUS.STATUS_FORMAT_ERROR]: 'FE',
|
||||
};
|
||||
|
||||
export const STATUS_CODES: Record<STATUS, string> = {
|
||||
[STATUS.STATUS_WAITING]: 'pending',
|
||||
[STATUS.STATUS_ACCEPTED]: 'pass',
|
||||
[STATUS.STATUS_WRONG_ANSWER]: 'fail',
|
||||
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'fail',
|
||||
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'fail',
|
||||
[STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'fail',
|
||||
[STATUS.STATUS_RUNTIME_ERROR]: 'fail',
|
||||
[STATUS.STATUS_COMPILE_ERROR]: 'fail',
|
||||
[STATUS.STATUS_SYSTEM_ERROR]: 'fail',
|
||||
[STATUS.STATUS_CANCELED]: 'ignored',
|
||||
[STATUS.STATUS_ETC]: 'fail',
|
||||
[STATUS.STATUS_HACKED]: 'fail',
|
||||
[STATUS.STATUS_JUDGING]: 'progress',
|
||||
[STATUS.STATUS_COMPILING]: 'progress',
|
||||
[STATUS.STATUS_FETCHED]: 'progress',
|
||||
[STATUS.STATUS_IGNORED]: 'ignored',
|
||||
[STATUS.STATUS_FORMAT_ERROR]: 'ignored',
|
||||
[STATUS.STATUS_HACK_SUCCESSFUL]: 'pass',
|
||||
[STATUS.STATUS_HACK_UNSUCCESSFUL]: 'fail',
|
||||
};
|
||||
|
||||
export function getScoreColor(score: number | string): string {
|
||||
if (score === null || score === undefined || !Number.isFinite(+score)) return '#000000';
|
||||
return [
|
||||
'#ff4f4f',
|
||||
'#ff694f',
|
||||
'#f8603a',
|
||||
'#fc8354',
|
||||
'#fa9231',
|
||||
'#f7bb3b',
|
||||
'#ecdb44',
|
||||
'#e2ec52',
|
||||
'#b0d628',
|
||||
'#93b127',
|
||||
'#25ad40',
|
||||
][Math.floor((Number(score) || 0) / 10)];
|
||||
}
|
||||
|
||||
export const USER_GENDER_MALE = 0;
|
||||
export const USER_GENDER_FEMALE = 1;
|
||||
export const USER_GENDER_OTHER = 2;
|
||||
export const USER_GENDERS = [USER_GENDER_MALE, USER_GENDER_FEMALE, USER_GENDER_OTHER];
|
||||
export const USER_GENDER_RANGE = {
|
||||
[USER_GENDER_MALE]: 'Boy ♂',
|
||||
[USER_GENDER_FEMALE]: 'Girl ♀',
|
||||
[USER_GENDER_OTHER]: 'Other',
|
||||
};
|
||||
export const USER_GENDER_ICONS = {
|
||||
[USER_GENDER_MALE]: '♂',
|
||||
[USER_GENDER_FEMALE]: '♀',
|
||||
[USER_GENDER_OTHER]: '?',
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'node:test';
|
||||
import { readSubtasksFromFiles } from '../lib/common';
|
||||
|
||||
interface Info {
|
||||
subtask: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
function shouldFindTestcase(files: string[], info?: Info) {
|
||||
const subtasks = readSubtasksFromFiles(files, {});
|
||||
if (files.includes('a2_1.in')) console.log(subtasks);
|
||||
expect(subtasks).to.be.lengthOf(1);
|
||||
if (info?.subtask) expect(subtasks[0].id).to.equal(info.subtask);
|
||||
const cases = subtasks.map((i) => i.cases).flat();
|
||||
expect(cases).to.be.lengthOf(1);
|
||||
if (info?.id) expect(cases[0].id).to.deep.equal(info.id);
|
||||
}
|
||||
|
||||
describe('single case', () => {
|
||||
it('1.in/1.out', () => {
|
||||
shouldFindTestcase(['1.in', '1.out']);
|
||||
});
|
||||
it('1.in/2.ans', () => {
|
||||
shouldFindTestcase(['1.in', '1.ans']);
|
||||
});
|
||||
it('file1.in/file1.out', () => {
|
||||
shouldFindTestcase(['file1.in', 'file1.out']);
|
||||
});
|
||||
it('file.in1/file.ou1', () => {
|
||||
shouldFindTestcase(['file.in1', 'file.ou1']);
|
||||
});
|
||||
it('input1.txt/output1.txt', () => {
|
||||
shouldFindTestcase(['input1.txt', 'output1.txt']);
|
||||
});
|
||||
it('data.1.in/data.1.out', () => {
|
||||
shouldFindTestcase(['data.1.in', 'data.1.out']);
|
||||
});
|
||||
});
|
||||
describe('subtask', () => {
|
||||
it('1_1.in/1_1.out', () => {
|
||||
shouldFindTestcase(['1_1.in', '1_1.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
it('a1_1.in/a1_1.out', () => {
|
||||
shouldFindTestcase(['a1_1.in', 'a1_1.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
it('a01_01.in/a01_01.out', () => {
|
||||
shouldFindTestcase(['a01_01.in', 'a01_01.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
it('1-1.in/1-1.out', () => {
|
||||
shouldFindTestcase(['1-1.in', '1-1.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
it('a1-1.in/a1-1.out', () => {
|
||||
shouldFindTestcase(['a1-1.in', 'a1-1.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
it('subtask_1_1.in/subtask_1_1.out', () => {
|
||||
shouldFindTestcase(['subtask_1_1.in', 'subtask_1_1.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
it('01sample-01.in/01sample-01.out', () => {
|
||||
shouldFindTestcase(['01sample-01.in', '01sample-01.out'], { subtask: 1, id: 1 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user