mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
judge: bug fix
This commit is contained in:
1 parent
511d08c850
commit
07ab4f1bbd
12 files changed
+111
-79
No files matched your search
@@ -7,7 +7,6 @@
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
"dependencies": {
|
||||
"@hydrooj/utils": "workspace:*",
|
||||
"axios": "^0.27.2",
|
||||
"cac": "^6.7.14",
|
||||
"fs-extra": "^10.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -15,6 +14,7 @@
|
||||
"p-queue": "^7.3.0",
|
||||
"schemastery": "^3.5.3",
|
||||
"shell-quote": "^1.7.3",
|
||||
"superagent": "^8.0.0",
|
||||
"ws": "^8.8.1"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
|
||||
@@ -4,11 +4,13 @@ import { findFileSync } from '@hydrooj/utils/lib/utils';
|
||||
import checkers from './checkers';
|
||||
import { CompileError, FormatError } from './error';
|
||||
import { Execute } from './interface';
|
||||
import { CopyInFile, del, run } from './sandbox';
|
||||
import {
|
||||
CopyIn, CopyInFile, del, run,
|
||||
} from './sandbox';
|
||||
import { compilerText } from './utils';
|
||||
|
||||
export default async function compile(
|
||||
lang: LangConfig, code: CopyInFile, copyIn: Record<string, CopyInFile> = {}, next?: Function,
|
||||
lang: LangConfig, code: CopyInFile, copyIn: CopyIn = {}, next?: Function,
|
||||
): Promise<Execute> {
|
||||
const target = lang.target || 'foo';
|
||||
const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute;
|
||||
@@ -46,8 +48,8 @@ const testlibFile = {
|
||||
src: findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'),
|
||||
};
|
||||
|
||||
const guessLanguage = (filename: string, getLang: Function) => {
|
||||
const s = filename.replace('@', '.').split('.');
|
||||
async function _compile(src: string, type: 'checker' | 'validator' | 'interactor', getLang, copyIn, withTestlib = true) {
|
||||
const s = src.replace('@', '.').split('.');
|
||||
let lang;
|
||||
let langId = s.pop();
|
||||
while (s.length) {
|
||||
@@ -55,31 +57,24 @@ const guessLanguage = (filename: string, getLang: Function) => {
|
||||
if (lang) break;
|
||||
langId = `${s.pop()}.${langId}`;
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
if (!lang) throw new FormatError(`Unknown ${type} language.`);
|
||||
if (withTestlib) copyIn = { ...copyIn, 'testlib.h': testlibFile };
|
||||
// TODO cache compiled binary
|
||||
return await compile(lang, { src }, copyIn);
|
||||
}
|
||||
|
||||
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise<Execute> {
|
||||
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: CopyIn) {
|
||||
if (['default', 'strict'].includes(checkerType)) {
|
||||
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
||||
}
|
||||
if (!checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
|
||||
if (checkerType === 'testlib') copyIn['testlib.h'] = testlibFile;
|
||||
const lang = guessLanguage(checker, getLang);
|
||||
if (!lang) throw new FormatError('Unknown checker language.');
|
||||
// TODO cache compiled checker
|
||||
return await compile(lang, { src: checker }, copyIn);
|
||||
return _compile(checker, 'checker', getLang, copyIn, checkerType === 'testlib');
|
||||
}
|
||||
|
||||
export async function compileInteractor(getLang: Function, interactor: string, copyIn: any): Promise<Execute> {
|
||||
const lang = guessLanguage(interactor, getLang);
|
||||
if (!lang) throw new FormatError('Unknown interactor language.');
|
||||
// TODO cache compiled checker
|
||||
return await compile(lang, { src: interactor }, { ...copyIn, 'testlib.h': testlibFile });
|
||||
export async function compileInteractor(getLang: Function, interactor: string, copyIn: CopyIn) {
|
||||
return _compile(interactor, 'interactor', getLang, copyIn);
|
||||
}
|
||||
|
||||
export async function compileValidator(getLang: Function, validator: string, copyIn: any): Promise<Execute> {
|
||||
const lang = guessLanguage(validator, getLang);
|
||||
if (!lang) throw new FormatError('Unknown validator language.');
|
||||
// TODO cache compiled checker
|
||||
return await compile(lang, { src: validator }, { ...copyIn, 'testlib.h': testlibFile });
|
||||
export async function compileValidator(getLang: Function, validator: string, copyIn: CopyIn) {
|
||||
return _compile(validator, 'validator', getLang, copyIn);
|
||||
}
|
||||
@@ -20,11 +20,13 @@ const JudgeSettings = Schema.object({
|
||||
processLimit: Schema.number().default(32),
|
||||
parallelism: Schema.number().default(2),
|
||||
singleTaskParallelism: Schema.number().default(2),
|
||||
rerun: Schema.number().default(0),
|
||||
rerun: Schema.number().description('Re-Run testcase if time-limit-exceeded (max per submission)').default(0),
|
||||
rate: Schema.number().default(1),
|
||||
env: Schema.string().default('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nHOME=/w'),
|
||||
host: Schema.any(),
|
||||
secret: Schema.string().default(String.random(32)),
|
||||
secret: Schema.string().description('Judge Token Secret').default(String.random(32)),
|
||||
disable: Schema.boolean().description('Disable builtin judge').default(false),
|
||||
detail: Schema.boolean().description('Show diff detail').default(true),
|
||||
});
|
||||
|
||||
const config = global.Hydro
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { JudgeResultBody } from 'hydrooj';
|
||||
import { end, next } from 'hydrooj/src/handler/judge';
|
||||
import { Logger } from 'hydrooj/src/logger';
|
||||
import * as setting from 'hydrooj/src/model/setting';
|
||||
import storage from 'hydrooj/src/model/storage';
|
||||
import * as system from 'hydrooj/src/model/system';
|
||||
import task from 'hydrooj/src/model/task';
|
||||
import storage from 'hydrooj/src/service/storage';
|
||||
import { processTestdata } from '../cases';
|
||||
import { getConfig } from '../config';
|
||||
import { FormatError, SystemError } from '../error';
|
||||
@@ -26,7 +26,7 @@ const session = {
|
||||
},
|
||||
getNext(t: Context) {
|
||||
return (data: Partial<JudgeResultBody>) => {
|
||||
logger.debug('Next: %d %o', data);
|
||||
logger.debug('Next: %o', data);
|
||||
data.rid = t.rid as any;
|
||||
if (data.case) data.case.message ||= '';
|
||||
next(data);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import { ObjectID } from 'bson';
|
||||
import fs from 'fs-extra';
|
||||
import PQueue from 'p-queue';
|
||||
import superagent from 'superagent';
|
||||
import WebSocket from 'ws';
|
||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||
@@ -20,13 +20,10 @@ function removeNixPath(text: string) {
|
||||
}
|
||||
|
||||
export default class Hydro {
|
||||
config: any;
|
||||
axios: any;
|
||||
ws: WebSocket;
|
||||
language: Record<string, LangConfig>;
|
||||
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
constructor(public config) {
|
||||
this.config.detail = this.config.detail ?? true;
|
||||
this.config.cookie = this.config.cookie || '';
|
||||
this.config.last_update_at = this.config.last_update_at || 0;
|
||||
@@ -35,10 +32,20 @@ export default class Hydro {
|
||||
this.getLang = this.getLang.bind(this);
|
||||
}
|
||||
|
||||
get(url) {
|
||||
return superagent.get(this.config.server_url + url).set('Cookie', this.config.cookie);
|
||||
}
|
||||
|
||||
post(url, data) {
|
||||
return superagent.post(this.config.server_url + url).send(data)
|
||||
.set('Cookie', this.config.cookie)
|
||||
.set('Accept', 'application/json');
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.setCookie(this.config.cookie || '');
|
||||
await this.ensureLogin();
|
||||
setInterval(() => { this.axios.get(''); }, 30000000); // Cookie refresh only
|
||||
setInterval(() => { this.get(''); }, 30000000); // Cookie refresh only
|
||||
}
|
||||
|
||||
async cacheOpen(source: string, files: any[], next?) {
|
||||
@@ -74,18 +81,15 @@ export default class Hydro {
|
||||
log.info(`Getting problem data: ${this.config.host}/${source}`);
|
||||
if (next) next({ message: 'Syncing testdata, please wait...' });
|
||||
await this.ensureLogin();
|
||||
const res = await this.axios.post(`/d/${domainId}/judge/files`, {
|
||||
const res = await this.post(`/d/${domainId}/judge/files`, {
|
||||
pid: +pid,
|
||||
files: filenames,
|
||||
});
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
async function download(name: string) {
|
||||
if (name.includes('/')) await fs.ensureDir(path.join(filePath, name.split('/')[0]));
|
||||
const f = await this.axios.get(res.data.links[name], { responseType: 'stream' })
|
||||
.catch((e) => new Error(`DownloadFail(${name}): ${e.message}`));
|
||||
if (f instanceof Error) throw f;
|
||||
const w = fs.createWriteStream(path.join(filePath, name));
|
||||
f.data.pipe(w);
|
||||
this.get(res.body.links[name]).pipe(w);
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('finish', resolve);
|
||||
w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`)));
|
||||
@@ -93,7 +97,7 @@ export default class Hydro {
|
||||
}
|
||||
const tasks = [];
|
||||
const queue = new PQueue({ concurrency: 10 });
|
||||
for (const name in res.data.links) {
|
||||
for (const name in res.body.links) {
|
||||
tasks.push(queue.add(() => download.call(this, name)));
|
||||
}
|
||||
queue.start();
|
||||
@@ -106,13 +110,10 @@ export default class Hydro {
|
||||
}
|
||||
|
||||
async fetchFile(name: string) {
|
||||
const res = await this.axios.post('judge/code', { id: name });
|
||||
const f = await this.axios.get(res.data.url, { responseType: 'stream' })
|
||||
.catch((e) => new Error(`DownloadFail(${name}): ${e.message}`));
|
||||
if (f instanceof Error) throw f;
|
||||
const res = await this.post('judge/code', { id: name });
|
||||
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
|
||||
const w = fs.createWriteStream(target);
|
||||
f.data.pipe(w);
|
||||
this.get(res.body.url).pipe(w);
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('finish', resolve);
|
||||
w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`)));
|
||||
@@ -127,7 +128,7 @@ export default class Hydro {
|
||||
return null;
|
||||
}
|
||||
|
||||
send(rid: string, key: 'next' | 'end', data: Partial<JudgeResultBody>) {
|
||||
send(rid: string | ObjectID, key: 'next' | 'end', data: Partial<JudgeResultBody>) {
|
||||
data.rid = new ObjectID(rid);
|
||||
data.key = key;
|
||||
if (data.case) data.case.message ||= '';
|
||||
@@ -193,20 +194,11 @@ export default class Hydro {
|
||||
|
||||
async setCookie(cookie: string) {
|
||||
this.config.cookie = cookie;
|
||||
this.axios = axios.create({
|
||||
baseURL: this.config.server_url,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
cookie: this.config.cookie,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
log.info('[%s] Updating session', this.config.host);
|
||||
const res = await this.axios.post('login', {
|
||||
const res = await this.post('login', {
|
||||
uname: this.config.uname, password: this.config.password, rememberme: 'on',
|
||||
});
|
||||
await this.setCookie(res.headers['set-cookie'].join(';'));
|
||||
@@ -214,9 +206,9 @@ export default class Hydro {
|
||||
|
||||
async ensureLogin() {
|
||||
try {
|
||||
const res = await this.axios.get('judge/files');
|
||||
const res = await this.get('judge/files');
|
||||
// Redirected to /login
|
||||
if (res.data.url) await this.login();
|
||||
if (res.body.url) await this.login();
|
||||
} catch (e) {
|
||||
await this.login();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FormatError, SystemError } from './error';
|
||||
import { Logger } from './log';
|
||||
import { SandboxClient } from './sandbox/client';
|
||||
import {
|
||||
Cmd, CopyInFile, SandboxResult, SandboxStatus,
|
||||
Cmd, CopyIn, CopyInFile, SandboxResult, SandboxStatus,
|
||||
} from './sandbox/interface';
|
||||
import { cmd, parseMemoryMB } from './utils';
|
||||
|
||||
@@ -36,7 +36,7 @@ interface Parameter {
|
||||
execute?: string;
|
||||
memory?: number;
|
||||
processLimit?: number;
|
||||
copyIn?: Record<string, CopyInFile>;
|
||||
copyIn?: CopyIn;
|
||||
copyOut?: string[];
|
||||
copyOutCached?: string[];
|
||||
cacheStdoutAndStderr?: boolean;
|
||||
@@ -162,7 +162,8 @@ export async function runPiped(execute0: Parameter, execute1: Parameter): Promis
|
||||
res = await new SandboxClient(getConfig('sandbox_host')).run(body);
|
||||
if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res));
|
||||
} catch (e) {
|
||||
if (e instanceof FormatError) throw e;
|
||||
if (e instanceof FormatError || e instanceof SystemError) throw e;
|
||||
console.error(e);
|
||||
throw new SystemError('Sandbox Error', [e]);
|
||||
}
|
||||
return await Promise.all(res.map((r) => adaptResult(r, {}))) as [SandboxAdaptedResult, SandboxAdaptedResult];
|
||||
@@ -188,7 +189,8 @@ export async function run(execute: string, params?: Parameter): Promise<SandboxA
|
||||
if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res));
|
||||
[result] = res;
|
||||
} catch (e) {
|
||||
if (e instanceof FormatError) throw e;
|
||||
if (e instanceof FormatError || e instanceof SystemError) throw e;
|
||||
console.error(e);
|
||||
// FIXME request body larger than maxBodyLength limit
|
||||
throw new SystemError('Sandbox Error', e.message);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import superagent from 'superagent';
|
||||
import { SandboxRequest, SandboxResult, SandboxVersion } from './interface';
|
||||
|
||||
export class SandboxClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.client = axios.create({ baseURL });
|
||||
constructor(public baseURL: string) {
|
||||
if (baseURL.endsWith('/')) baseURL = baseURL.substring(0, baseURL.length - 1);
|
||||
}
|
||||
|
||||
public run(req: SandboxRequest): Promise<SandboxResult[]> {
|
||||
return this.client.post('/run', req).then((res) => res.data);
|
||||
return superagent.post(`${this.baseURL}/run`).send(req).then((res) => res.body);
|
||||
}
|
||||
|
||||
public getFile(fileId: string): Promise<Buffer> {
|
||||
return this.client.get(`/file/${fileId}`).then((res) => res.data);
|
||||
return superagent.get(`${this.baseURL}/file/${fileId}`).then((res) => res.body);
|
||||
}
|
||||
|
||||
public deleteFile(fileId: string): Promise<never> {
|
||||
return this.client.delete(`/file/${fileId}`);
|
||||
public deleteFile(fileId: string): Promise<void> {
|
||||
return superagent.delete(`${this.baseURL}/file/${fileId}`).then((res) => res.body);
|
||||
}
|
||||
|
||||
public listFiles(): Promise<Record<string, string>> {
|
||||
return this.client.get('/file').then((res) => res.data);
|
||||
return superagent.get(`${this.baseURL}/file`).then((res) => res.body);
|
||||
}
|
||||
|
||||
public version(): Promise<SandboxVersion> {
|
||||
return this.client.get('/version').then((res) => res.data);
|
||||
return superagent.get(`${this.baseURL}/version`).then((res) => res.body);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export interface Collector {
|
||||
}
|
||||
|
||||
export type CopyInFile = LocalFile | MemoryFile | PreparedFile;
|
||||
export type CopyIn = Record<string, CopyInFile>;
|
||||
export type CmdFile = LocalFile | MemoryFile | PreparedFile | Collector | null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,9 @@ import fs from 'fs-extra';
|
||||
import { noop } from 'lodash';
|
||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||
import type { JudgeResultBody } from 'hydrooj';
|
||||
import type {
|
||||
FileInfo, JudgeRequest, JudgeResultBody, ProblemConfigFile,
|
||||
} from 'hydrooj';
|
||||
import { Logger } from 'hydrooj/src/logger';
|
||||
import readCases from './cases';
|
||||
import { getConfig } from './config';
|
||||
@@ -34,14 +36,14 @@ export class JudgeTask {
|
||||
tmpdir: string;
|
||||
input?: string;
|
||||
clean: (() => Promise<any>)[];
|
||||
data: any[];
|
||||
data: FileInfo[];
|
||||
folder: string;
|
||||
config: any;
|
||||
config: ProblemConfigFile & { problemOwner: number };
|
||||
next: (data: Partial<JudgeResultBody>) => void;
|
||||
end: (data: Partial<JudgeResultBody>) => void;
|
||||
env: Record<string, string>;
|
||||
|
||||
constructor(public session: Session, public request) {
|
||||
constructor(public session: Session, public request: JudgeRequest) {
|
||||
this.stat = {};
|
||||
this.stat.receive = new Date();
|
||||
logger.debug('%o', request);
|
||||
@@ -75,7 +77,9 @@ export class JudgeTask {
|
||||
fs.ensureDirSync(this.tmpdir);
|
||||
tmpfs.mount(this.tmpdir, getConfig('tmpfs_size'));
|
||||
logger.info('Submission: %s/%s/%s', host, this.source, this.rid);
|
||||
await this.doSubmission();
|
||||
this.stat.cache_start = new Date();
|
||||
if (tid === '000000000000000000000001') await this.doHack();
|
||||
else await this.doSubmission();
|
||||
} catch (e) {
|
||||
if (e instanceof CompileError) {
|
||||
this.next({ compilerText: compilerText(e.stdout, e.stderr) });
|
||||
@@ -105,7 +109,6 @@ export class JudgeTask {
|
||||
}
|
||||
|
||||
async doSubmission() {
|
||||
this.stat.cache_start = new Date();
|
||||
this.folder = await this.session.cacheOpen(this.source, this.data, this.next);
|
||||
if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) {
|
||||
const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0];
|
||||
@@ -129,4 +132,29 @@ export class JudgeTask {
|
||||
if (!judge[type]) throw new FormatError('Unrecognized problemType: {0}', [type]);
|
||||
await judge[type].judge(this);
|
||||
}
|
||||
|
||||
async doHack() {
|
||||
this.folder = await this.session.cacheOpen(this.source, this.data, this.next);
|
||||
if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) {
|
||||
const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0];
|
||||
if (!id) throw new SystemError('Submission File Not Found');
|
||||
const target = await this.session.fetchFile(id);
|
||||
this.code = { src: target };
|
||||
this.clean.push(() => fs.remove(target));
|
||||
}
|
||||
this.stat.read_cases = new Date();
|
||||
this.config = await readCases(
|
||||
this.folder,
|
||||
{
|
||||
detail: this.session.config.detail,
|
||||
isSelfSubmission: this.config.problemOwner === this.request.uid,
|
||||
...this.config,
|
||||
},
|
||||
{ next: this.next, key: md5(`${this.source}/${getConfig('secret')}`) },
|
||||
);
|
||||
this.stat.judge = new Date();
|
||||
const type = typeof this.input === 'string' ? 'run' : this.config.type || 'default';
|
||||
if (type !== 'default') throw new FormatError('Cannot hack: only type=default allowed');
|
||||
await judge.hack.judge(this);
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,7 @@ export interface ProblemConfigFile {
|
||||
cases?: TestCaseConfig[];
|
||||
subtasks?: SubtaskConfig[];
|
||||
langs?: string[];
|
||||
validator?: string;
|
||||
}
|
||||
|
||||
export interface ProblemConfig {
|
||||
@@ -298,6 +299,15 @@ export interface RecordDoc {
|
||||
contest?: ObjectID;
|
||||
}
|
||||
|
||||
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
|
||||
priority: number;
|
||||
type: 'judge';
|
||||
rid: ObjectID;
|
||||
config: ProblemConfigFile & { problemOwner: number };
|
||||
data: FileInfo[];
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface ScoreboardNode {
|
||||
type: 'string' | 'rank' | 'user' | 'email' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score';
|
||||
value: string;
|
||||
|
||||
@@ -49,6 +49,7 @@ const problemConfigSchema = {
|
||||
checker_type: { enum: ['default', 'lemon', 'syzoj', 'hustoj', 'testlib', 'strict', 'qduoj'] },
|
||||
checker: { type: 'string', pattern: '\\.' },
|
||||
interactor: { type: 'string', pattern: '\\.' },
|
||||
validator: { type: 'string', pattern: '\\.' },
|
||||
user_extra_files: { type: 'array', items: { type: 'string' } },
|
||||
judge_extra_files: { type: 'array', items: { type: 'string' } },
|
||||
cases: { $ref: '#/def/cases' },
|
||||
|
||||
@@ -8,7 +8,9 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
user_extra_files: [],
|
||||
};
|
||||
if (cfg.type === 'objective') {
|
||||
if (cfg.checker || cfg.interactor) throw new Error('You cannot use checker or interactor for objective questions');
|
||||
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('.')) {
|
||||
@@ -17,6 +19,7 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
|
||||
}
|
||||
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
|
||||
if (cfg.validator) config.validator = checkFile(cfg.validator, 'Cannot find validator {0}.');
|
||||
['judge', 'user'].forEach((n) => {
|
||||
const conf = cfg[`${n}_extra_files`];
|
||||
if (!conf) return;
|
||||
|
||||
Reference in new issue
Block a user