mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: problemconfig: support new CompilableSource
This commit is contained in:
1 parent
4a33816a37
commit
5c9cecc197
17 files changed
+252
-228
No files matched your search
@@ -71,7 +71,6 @@ export interface HydroResponse {
|
|||||||
etag?: string;
|
etag?: string;
|
||||||
attachment: (name: string, stream?: any) => void;
|
attachment: (name: string, stream?: any) => void;
|
||||||
addHeader: (name: string, value: string) => void;
|
addHeader: (name: string, value: string) => void;
|
||||||
serializer: typeof serializer;
|
|
||||||
}
|
}
|
||||||
export type RendererFunction = (name: string, context: Record<string, any>) => string | Promise<string>;
|
export type RendererFunction = (name: string, context: Record<string, any>) => string | Promise<string>;
|
||||||
type HydroContext = {
|
type HydroContext = {
|
||||||
@@ -171,7 +170,7 @@ export class HandlerCommon {
|
|||||||
|
|
||||||
renderHTML(templateName: string, args: Record<string, any>) {
|
renderHTML(templateName: string, args: Record<string, any>) {
|
||||||
const type = templateName.split('.')[1];
|
const type = templateName.split('.')[1];
|
||||||
const engine = this.ctx.server.renderers[type] || (() => JSON.stringify(args, this.response.serializer(false, this)));
|
const engine = this.ctx.server.renderers[type] || (() => JSON.stringify(args, serializer(false, this)));
|
||||||
return engine(templateName, {
|
return engine(templateName, {
|
||||||
handler: this,
|
handler: this,
|
||||||
UserContext: this.user,
|
UserContext: this.user,
|
||||||
@@ -266,7 +265,7 @@ export class ConnectionHandler extends HandlerCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
send(data: any) {
|
send(data: any) {
|
||||||
let payload = JSON.stringify(data, this.response.serializer(false, this));
|
let payload = JSON.stringify(data, serializer(false, this));
|
||||||
if (this.compression) {
|
if (this.compression) {
|
||||||
if (this.counter > 1000) this.resetCompression();
|
if (this.counter > 1000) this.resetCompression();
|
||||||
payload = this.compression.deflate(payload);
|
payload = this.compression.deflate(payload);
|
||||||
@@ -328,7 +327,6 @@ export interface WebServiceConfig {
|
|||||||
host?: string;
|
host?: string;
|
||||||
xff?: string;
|
xff?: string;
|
||||||
xhost?: string;
|
xhost?: string;
|
||||||
serializer?: typeof serializer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebService extends Service {
|
export class WebService extends Service {
|
||||||
@@ -443,7 +441,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
func: (t) => this.handleHttp(t, NotFoundHandler, () => true),
|
func: (t) => this.handleHttp(t, NotFoundHandler, () => true),
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
this.addLayer('base', base(logger, this.config.xff, this.config.xhost, this.config.serializer));
|
this.addLayer('base', base(logger, this.config.xff, this.config.xhost));
|
||||||
wsServer.on('connection', async (socket, request) => {
|
wsServer.on('connection', async (socket, request) => {
|
||||||
socket.on('error', (err) => {
|
socket.on('error', (err) => {
|
||||||
logger.warn('Websocket Error: %s', err.message);
|
logger.warn('Websocket Error: %s', err.message);
|
||||||
|
|||||||
+5
-178
@@ -1,7 +1,8 @@
|
|||||||
import { findFileSync, parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
|
import { findFileSync, parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
|
||||||
|
import { CompilableSource, ProblemConfigFile } from './types';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
|
export async function readYamlCases(cfg: ProblemConfigFile = {}, checkFile = (s: CompilableSource, errMsg: string) => s) {
|
||||||
const config: any = {
|
const config: any = {
|
||||||
checker_type: cfg.checker_type || 'default',
|
checker_type: cfg.checker_type || 'default',
|
||||||
judge_extra_files: [],
|
judge_extra_files: [],
|
||||||
@@ -13,8 +14,9 @@ export async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cfg.checker) {
|
if (cfg.checker) {
|
||||||
if (!cfg.checker.includes('.')) {
|
const name = typeof cfg.checker === 'string' ? cfg.checker : cfg.checker.file;
|
||||||
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
if (!name.includes('.')) {
|
||||||
|
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${name}.cpp`, false);
|
||||||
}
|
}
|
||||||
config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.');
|
config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.');
|
||||||
}
|
}
|
||||||
@@ -39,178 +41,3 @@ export async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (
|
|||||||
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
||||||
return Object.assign(cfg, config);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,4 +1,4 @@
|
|||||||
export * from './lang';
|
export * from './lang';
|
||||||
export * from './status';
|
export * from './status';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './cases';
|
export * from './subtask';
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,6 +1,6 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import { readSubtasksFromFiles } from '../cases';
|
import { readSubtasksFromFiles } from '../subtask';
|
||||||
|
|
||||||
interface Info {
|
interface Info {
|
||||||
subtask: number;
|
subtask: number;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hydrooj/common": "workspace:^",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"adm-zip": "0.5.14",
|
"adm-zip": "0.5.14",
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import {
|
||||||
convertIniConfig, LangConfig,
|
convertIniConfig, LangConfig, normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles,
|
||||||
normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles, readYamlCases,
|
|
||||||
} from '@hydrooj/common';
|
} from '@hydrooj/common';
|
||||||
|
import { readYamlCases } from '@hydrooj/common/cases';
|
||||||
import {
|
import {
|
||||||
changeErrorType, fs, yaml,
|
changeErrorType, fs, yaml,
|
||||||
} from '@hydrooj/utils';
|
} from '@hydrooj/utils';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import path from 'path';
|
|||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import superagent from 'superagent';
|
import superagent from 'superagent';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
|
import type { LangConfig } from '@hydrooj/common';
|
||||||
import { fs, pipeRequest } from '@hydrooj/utils';
|
import { fs, pipeRequest } from '@hydrooj/utils';
|
||||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
|
||||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||||
import type { JudgeResultBody } from 'hydrooj';
|
import type { JudgeResultBody } from 'hydrooj';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { parse } from 'shell-quote';
|
import { parse } from 'shell-quote';
|
||||||
|
import { CompilableSource } from '@hydrooj/common';
|
||||||
import { fs } from '@hydrooj/utils';
|
import { fs } from '@hydrooj/utils';
|
||||||
import { FormatError } from './error';
|
import { FormatError } from './error';
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ function restrictFile(p: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ensureFile(folder: string) {
|
export function ensureFile(folder: string) {
|
||||||
return (file: string, message: string) => {
|
return (src: CompilableSource, message: string) => {
|
||||||
|
const file = typeof src === 'string' ? src : src?.file;
|
||||||
if (file === '/dev/null') return file;
|
if (file === '/dev/null') return file;
|
||||||
// Historical issue
|
// Historical issue
|
||||||
if (file.includes('/')) {
|
if (file.includes('/')) {
|
||||||
|
|||||||
@@ -12,11 +12,12 @@
|
|||||||
},
|
},
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.741.0",
|
"@aws-sdk/client-s3": "3.726.1",
|
||||||
"@aws-sdk/lib-storage": "3.741.0",
|
"@aws-sdk/lib-storage": "3.726.1",
|
||||||
"@aws-sdk/s3-presigned-post": "3.741.0",
|
"@aws-sdk/s3-presigned-post": "3.726.1",
|
||||||
"@aws-sdk/s3-request-presigner": "3.741.0",
|
"@aws-sdk/s3-request-presigner": "3.726.1",
|
||||||
"@graphql-tools/schema": "^10.0.16",
|
"@graphql-tools/schema": "^10.0.16",
|
||||||
|
"@hydrooj/common": "workspace:^",
|
||||||
"@hydrooj/framework": "workspace:^",
|
"@hydrooj/framework": "workspace:^",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"@simplewebauthn/server": "9.0.3",
|
"@simplewebauthn/server": "9.0.3",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
import {
|
import { normalizeSubtasks, ProblemConfigFile } from '@hydrooj/common';
|
||||||
normalizeSubtasks, ProblemConfigFile, readYamlCases,
|
import { readYamlCases } from '@hydrooj/common/cases';
|
||||||
} from '@hydrooj/common';
|
|
||||||
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
|
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
|
||||||
import type { ProblemConfig } from '../interface';
|
import type { ProblemConfig } from '../interface';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
getScoreColor, STATUS, STATUS_CODES, STATUS_SHORT_TEXTS,
|
getScoreColor, NORMAL_STATUS, STATUS, STATUS_CODES, STATUS_SHORT_TEXTS,
|
||||||
STATUS_TEXTS, USER_GENDER_FEMALE, USER_GENDER_ICONS, USER_GENDER_MALE,
|
STATUS_TEXTS, USER_GENDER_FEMALE, USER_GENDER_ICONS, USER_GENDER_MALE,
|
||||||
USER_GENDER_OTHER, USER_GENDER_RANGE, USER_GENDERS,
|
USER_GENDER_OTHER, USER_GENDER_RANGE, USER_GENDERS,
|
||||||
} from '@hydrooj/common';
|
} from '@hydrooj/common';
|
||||||
@@ -475,6 +475,7 @@ global.Hydro.model.builtin = {
|
|||||||
STATUS_TEXTS,
|
STATUS_TEXTS,
|
||||||
STATUS_SHORT_TEXTS,
|
STATUS_SHORT_TEXTS,
|
||||||
STATUS_CODES,
|
STATUS_CODES,
|
||||||
|
NORMAL_STATUS,
|
||||||
USER_GENDER_MALE,
|
USER_GENDER_MALE,
|
||||||
USER_GENDER_FEMALE,
|
USER_GENDER_FEMALE,
|
||||||
USER_GENDER_OTHER,
|
USER_GENDER_OTHER,
|
||||||
|
|||||||
@@ -8,8 +8,5 @@
|
|||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jsonwebtoken": "^9.0.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
|
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
import type { RootState } from './reducer/index';
|
import type { RootState } from './reducer/index';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export function FormItem({
|
|||||||
type KeyType<K, T = string | number> = {
|
type KeyType<K, T = string | number> = {
|
||||||
[Q in keyof K]: K[Q] extends T ? Q : never;
|
[Q in keyof K]: K[Q] extends T ? Q : never;
|
||||||
}[keyof K];
|
}[keyof K];
|
||||||
|
type FileSelectKey = 'checker' | 'interactor' | 'manager';
|
||||||
|
|
||||||
export function ManagedInput({ placeholder, formKey }: { placeholder?: string, formKey: KeyType<RootState['config']> }) {
|
export function ManagedInput({ placeholder, formKey }: { placeholder?: string, formKey: KeyType<RootState['config']> }) {
|
||||||
const value = useSelector((state: RootState) => state.config[formKey]);
|
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||||
@@ -54,16 +56,42 @@ export function ManagedSelect({ options, formKey }: { options: string[], formKey
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SingleFileSelect({ formKey }: { formKey: KeyType<RootState['config']> }) {
|
const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
||||||
|
const data = Object.keys(window.LANGS).filter((i) => !prefixes.has(i))
|
||||||
|
.map((i) => ({ name: `${i.includes('.') ? `${window.LANGS[i.split('.')[0]].display || ''}/` : ''}${window.LANGS[i].display}`, _id: i }));
|
||||||
|
data.unshift({ name: 'Auto', _id: 'auto' });
|
||||||
|
|
||||||
|
export function SingleFileSelect({ formKey, withLang = false, label = 'Checker' }: { formKey: FileSelectKey, withLang?: boolean, label?: string }) {
|
||||||
const value = useSelector((state: RootState) => state.config[formKey]);
|
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||||
const Files = useSelector((state: RootState) => state.testdata);
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
const selectedFile = typeof value === 'string' ? value : value?.file || '';
|
||||||
<FileSelectAutoComplete
|
const selectedLang = typeof value === 'string' ? 'auto' : value?.lang || 'auto';
|
||||||
width="100%"
|
const update = (file: string, lang: string) => {
|
||||||
data={Files}
|
if (file === selectedFile && lang === selectedLang) return;
|
||||||
selectedKeys={[value]}
|
if (withLang) dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: file ? { file, lang } : null });
|
||||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: val })}
|
else dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: file });
|
||||||
/>
|
};
|
||||||
);
|
return withLang ? (<>
|
||||||
|
<FormItem columns={5} label={label}>
|
||||||
|
<FileSelectAutoComplete
|
||||||
|
width="100%"
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={[selectedFile]}
|
||||||
|
onChange={(val) => update(val, selectedLang)}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem columns={3} label="Language">
|
||||||
|
<CustomSelectAutoComplete
|
||||||
|
data={data}
|
||||||
|
selectedKeys={[selectedLang]}
|
||||||
|
onChange={(val) => update(selectedFile, val)}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</>) : (<FileSelectAutoComplete
|
||||||
|
width="100%"
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={[selectedFile]}
|
||||||
|
onChange={(val) => update(val, selectedLang)}
|
||||||
|
/>);
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,8 @@ export default function ProblemType() {
|
|||||||
const dispatcher = (base) => (value) => dispatch({ ...base, value });
|
const dispatcher = (base) => (value) => dispatch({ ...base, value });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (category || !checker) return;
|
if (category || !checker) return;
|
||||||
if (checker.includes('.')) setCategory('custom');
|
const name = typeof checker === 'string' ? checker : checker.file;
|
||||||
|
if (name.includes('.')) setCategory('custom');
|
||||||
else setCategory('preset');
|
else setCategory('preset');
|
||||||
}, [checker]);
|
}, [checker]);
|
||||||
return (
|
return (
|
||||||
@@ -75,7 +76,7 @@ export default function ProblemType() {
|
|||||||
title="testlib"
|
title="testlib"
|
||||||
panel={(
|
panel={(
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="Type">
|
<FormItem columns={4} label="Type">
|
||||||
<select
|
<select
|
||||||
value={category}
|
value={category}
|
||||||
onChange={(ev) => {
|
onChange={(ev) => {
|
||||||
@@ -89,9 +90,9 @@ export default function ProblemType() {
|
|||||||
</select>
|
</select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
{category === 'preset'
|
{category === 'preset'
|
||||||
? <FormItem columns={6} label="Checker">
|
? <FormItem columns={8} label="Checker">
|
||||||
<select
|
<select
|
||||||
value={checker}
|
value={typeof checker === 'string' ? checker : checker.file}
|
||||||
onChange={(ev) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker', value: ev.currentTarget.value })}
|
onChange={(ev) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker', value: ev.currentTarget.value })}
|
||||||
className="select"
|
className="select"
|
||||||
>
|
>
|
||||||
@@ -100,9 +101,7 @@ export default function ProblemType() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
: <FormItem columns={6} label="Checker">
|
: <SingleFileSelect formKey="checker" label="Checker" withLang />}
|
||||||
<SingleFileSelect formKey="checker" />
|
|
||||||
</FormItem>}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -111,12 +110,10 @@ export default function ProblemType() {
|
|||||||
title="other"
|
title="other"
|
||||||
panel={(
|
panel={(
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="Interface">
|
<FormItem columns={4} label="Interface">
|
||||||
<ManagedSelect options={['syzoj', 'hustoj', 'qduoj', 'lemon']} formKey="checker_type" />
|
<ManagedSelect options={['syzoj', 'hustoj', 'qduoj', 'lemon']} formKey="checker_type" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem columns={6} label="Checker">
|
<SingleFileSelect formKey="checker" label="Checker" withLang />
|
||||||
<SingleFileSelect formKey="checker" />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -128,9 +125,7 @@ export default function ProblemType() {
|
|||||||
title={i18n('problem_type.interactive')}
|
title={i18n('problem_type.interactive')}
|
||||||
panel={(
|
panel={(
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="Interactor">
|
<SingleFileSelect formKey="interactor" label="Interactor" withLang />
|
||||||
<SingleFileSelect formKey="interactor" />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -139,10 +134,8 @@ export default function ProblemType() {
|
|||||||
title={i18n('problem_type.communication')}
|
title={i18n('problem_type.communication')}
|
||||||
panel={(
|
panel={(
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="Manager">
|
<SingleFileSelect formKey="manager" label="Manager" withLang />
|
||||||
<SingleFileSelect formKey="manager" />
|
<FormItem columns={4} label="Number of Processes">
|
||||||
</FormItem>
|
|
||||||
<FormItem columns={6} label="Number of Processes">
|
|
||||||
<input
|
<input
|
||||||
defaultValue={numProcesses || 2}
|
defaultValue={numProcesses || 2}
|
||||||
placeholder="2"
|
placeholder="2"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function reducer(state = {
|
|||||||
const next = { ...state, [action.key]: action.value };
|
const next = { ...state, [action.key]: action.value };
|
||||||
if (action.key === 'score' && action.value) next.score = +next.score;
|
if (action.key === 'score' && action.value) next.score = +next.score;
|
||||||
if (action.key === 'checker_type' && action.value === 'other') next.checker_type = 'syzoj';
|
if (action.key === 'checker_type' && action.value === 'other') next.checker_type = 'syzoj';
|
||||||
if (!action.value || (typeof action.value === 'object' && !action.value.join(''))) delete next[action.key];
|
if (!action.value || (action.value instanceof Array && !action.value.join(''))) delete next[action.key];
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
case 'CONFIG_CODE_UPDATE': {
|
case 'CONFIG_CODE_UPDATE': {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"webpackbar": "^7.0.0"
|
"webpackbar": "^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hydrooj/common": "workspace:^",
|
||||||
"ansi_up": "^6.0.2",
|
"ansi_up": "^6.0.2",
|
||||||
"cssfilter": "^0.0.11",
|
"cssfilter": "^0.0.11",
|
||||||
"esbuild": "0.24.0",
|
"esbuild": "0.24.0",
|
||||||
|
|||||||
Reference in new issue
Block a user