judge: change config.detail to enum (#1000)

This commit is contained in:
undefined authored and GitHub committed 2025-06-26 02:55:38 +01:00
1 parent fb04aaacb9
commit ed830e119e
13 files changed
+41 -29

No files matched your search

+3 -1
View File
@@ -36,6 +36,8 @@ export interface SubtaskConfig {
cases?: TestCaseConfig[]; cases?: TestCaseConfig[];
} }
export type DetailType = 'full' | 'case' | 'none';
export interface ProblemConfigFile { export interface ProblemConfigFile {
type?: ProblemType; type?: ProblemType;
subType?: string; subType?: string;
@@ -48,7 +50,7 @@ export interface ProblemConfigFile {
num_processes?: number; num_processes?: number;
user_extra_files?: string[]; user_extra_files?: string[];
judge_extra_files?: string[]; judge_extra_files?: string[];
detail?: boolean; detail?: DetailType | boolean;
answers?: Record<string, [string | string[], number]>; answers?: Record<string, [string | string[], number]>;
redirect?: string; redirect?: string;
cases?: TestCaseConfig[]; cases?: TestCaseConfig[];
+3
View File
@@ -87,6 +87,9 @@ export default async function readCases(folder: string, cfg: ProblemConfigFile =
throw new SystemError('Cannot parse testdata.', [e.message, ...(e.params || [])]); throw new SystemError('Cannot parse testdata.', [e.message, ...(e.params || [])]);
} }
} }
if (result.detail === true) result.detail = 'full';
else if (result.detail === false) result.detail = 'case';
else result.detail ||= 'full';
result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory, false, timeRate, memoryRate); result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory, false, timeRate, memoryRate);
if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key'); if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key');
if (!result.key && !args.trusted) isValidConfig(result); if (!result.key && !args.trusted) isValidConfig(result);
+12 -10
View File
@@ -1,4 +1,4 @@
import { STATUS } from '@hydrooj/common'; import { DetailType, STATUS } from '@hydrooj/common';
import { FormatError, SystemError } from './error'; import { FormatError, SystemError } from './error';
import { CopyInFile, runQueued } from './sandbox'; import { CopyInFile, runQueued } from './sandbox';
import { parse } from './testlib'; import { parse } from './testlib';
@@ -12,7 +12,7 @@ export interface CheckConfig {
code: CopyInFile; code: CopyInFile;
copyIn: Record<string, CopyInFile>; copyIn: Record<string, CopyInFile>;
score: number; score: number;
detail: boolean; detail: DetailType;
env?: Record<string, string>; env?: Record<string, string>;
} }
@@ -85,7 +85,7 @@ elif [ -n "$result" ]; then
fi fi
`; `;
const getDefaultChecker = (strict: boolean) => async (config) => { const getDefaultChecker = (strict: boolean) => async (config: CheckConfig) => {
const { code, stdout } = await runQueued(`/bin/bash compare.sh${strict ? '' : ' BZ'}`, { const { code, stdout } = await runQueued(`/bin/bash compare.sh${strict ? '' : ' BZ'}`, {
copyIn: { copyIn: {
usrout: config.user_stdout, usrout: config.user_stdout,
@@ -102,7 +102,7 @@ const getDefaultChecker = (strict: boolean) => async (config) => {
message = `Checker returned with status ${code}`; message = `Checker returned with status ${code}`;
} else if (stdout) { } else if (stdout) {
status = STATUS.STATUS_WRONG_ANSWER; status = STATUS.STATUS_WRONG_ANSWER;
if (config.detail && !strict) message = parseDiffMsg(stdout); if (config.detail === 'full') message = parseDiffMsg(stdout);
} else status = STATUS.STATUS_ACCEPTED; } else status = STATUS.STATUS_ACCEPTED;
if (message.length > 1024000) message = ''; if (message.length > 1024000) message = '';
return { return {
@@ -135,7 +135,7 @@ const checkers: Record<string, Checker> = new Proxy({
return { return {
status, status,
score: status === STATUS.STATUS_ACCEPTED ? config.score : 0, score: status === STATUS.STATUS_ACCEPTED ? config.score : 0,
message: stdout, message: config.detail === 'full' ? stdout : '',
}; };
}, },
@@ -168,7 +168,7 @@ const checkers: Record<string, Checker> = new Proxy({
const score = Math.floor(+files.score) || 0; const score = Math.floor(+files.score) || 0;
return { return {
score, score,
message: files.message, message: config.detail === 'full' ? files.message : '',
status: score === config.score status: score === config.score
? STATUS.STATUS_ACCEPTED ? STATUS.STATUS_ACCEPTED
: STATUS.STATUS_WRONG_ANSWER, : STATUS.STATUS_WRONG_ANSWER,
@@ -193,7 +193,7 @@ const checkers: Record<string, Checker> = new Proxy({
return { return {
status: st, status: st,
score: (status === STATUS.STATUS_ACCEPTED) ? config.score : 0, score: (status === STATUS.STATUS_ACCEPTED) ? config.score : 0,
message: stdout, message: config.detail === 'full' ? stdout : '',
}; };
}, },
@@ -219,7 +219,7 @@ const checkers: Record<string, Checker> = new Proxy({
if (status !== STATUS.STATUS_ACCEPTED) throw new SystemError('Checker returned {0}.', [status]); if (status !== STATUS.STATUS_ACCEPTED) throw new SystemError('Checker returned {0}.', [status]);
const score = +stdout; const score = +stdout;
status = score === 100 ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER; status = score === 100 ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER;
return { status, score: Math.floor((score * config.score) / 100), message: stderr }; return { status, score: Math.floor((score * config.score) / 100), message: config.detail === 'full' ? stderr : '' };
}, },
async testlib(config) { async testlib(config) {
@@ -252,7 +252,7 @@ const checkers: Record<string, Checker> = new Proxy({
message: `Checker exited with code ${code}`, message: `Checker exited with code ${code}`,
}; };
} }
return parse(stderr, config.score); return parse(stderr, config.score, config.detail);
}, },
// https://www.kattis.com/problem-package-format/spec/2023-07-draft.html#output-validator // https://www.kattis.com/problem-package-format/spec/2023-07-draft.html#output-validator
@@ -285,7 +285,9 @@ const checkers: Record<string, Checker> = new Proxy({
const message = status === STATUS.STATUS_SYSTEM_ERROR const message = status === STATUS.STATUS_SYSTEM_ERROR
? files['feedback_dir/judgeerror.txt'] || `Checker exited with code ${code}` ? files['feedback_dir/judgeerror.txt'] || `Checker exited with code ${code}`
: files['feedback_dir/teammessage.txt'] || files['feedback_dir/judgemessage.txt'] || ''; : config.detail === 'full'
? files['feedback_dir/teammessage.txt'] || files['feedback_dir/judgemessage.txt'] || ''
: '';
return { status, score, message }; return { status, score, message };
}, },
+6 -1
View File
@@ -32,7 +32,12 @@ export const JudgeSettings = Schema.object({
host: Schema.any(), host: Schema.any(),
secret: Schema.string().description('Judge Token Secret').default(String.random(32)), secret: Schema.string().description('Judge Token Secret').default(String.random(32)),
disable: Schema.boolean().description('Disable builtin judge').default(false), disable: Schema.boolean().description('Disable builtin judge').default(false),
detail: Schema.boolean().description('Show diff detail').default(true), detail: Schema.union([
Schema.const('full'),
Schema.const('case'),
Schema.const('none'),
Schema.transform(Schema.boolean(), (v) => (v ? 'full' : 'case')),
]).description('Show diff detail').default('full'),
performance: Schema.boolean().description('Performance mode').default(false), performance: Schema.boolean().description('Performance mode').default(false),
}); });
+3 -1
View File
@@ -53,7 +53,9 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['
ctx.total_time += res.time; ctx.total_time += res.time;
ctx.total_memory = Math.max(ctx.total_memory, res.memory); ctx.total_memory = Math.max(ctx.total_memory, res.memory);
} }
ctx.next({ ...res ? { case: res } : {}, addProgress: 100 / ctx.config.count }); if (ctx.config.detail !== 'none') {
ctx.next({ ...res ? { case: res } : {}, addProgress: 100 / ctx.config.count });
}
})); }));
} }
try { try {
+4 -3
View File
@@ -1,5 +1,5 @@
import { import {
JudgeResultBody, type LangConfig, NormalizedSubtask, ProblemConfigFile, DetailType, JudgeResultBody, type LangConfig, NormalizedSubtask, ProblemConfigFile,
} from '@hydrooj/common'; } from '@hydrooj/common';
import { CopyInFile } from './sandbox'; import { CopyInFile } from './sandbox';
import type { JudgeTask } from './task'; import type { JudgeTask } from './task';
@@ -14,11 +14,12 @@ export interface Execute {
export type NextFunction = (body: Partial<JudgeResultBody>) => Promise<void> | void; export type NextFunction = (body: Partial<JudgeResultBody>) => Promise<void> | void;
export interface ParsedConfig extends Omit<ProblemConfigFile, 'time' | 'memory' | 'subtasks'> { export interface ParsedConfig extends Omit<ProblemConfigFile, 'time' | 'memory' | 'subtasks' | 'detail'> {
count: number; count: number;
time: number; time: number;
memory: number; memory: number;
subtasks: NormalizedSubtask[]; subtasks: NormalizedSubtask[];
detail: DetailType;
} }
export { JudgeRequest } from '@hydrooj/common'; export { JudgeRequest } from '@hydrooj/common';
@@ -28,5 +29,5 @@ export interface Session {
getReporter: (task: JudgeTask) => { next: NextFunction, end: NextFunction }; getReporter: (task: JudgeTask) => { next: NextFunction, end: NextFunction };
fetchFile: <T extends null | string>(namespace: T, files: Record<string, string>) => Promise<T extends null ? string : null>; fetchFile: <T extends null | string>(namespace: T, files: Record<string, string>) => Promise<T extends null ? string : null>;
postFile: (target: string, filename: string, file: string) => Promise<void>; postFile: (target: string, filename: string, file: string) => Promise<void>;
config: { detail: boolean, host?: string, trusted?: boolean }; config: { detail: DetailType, host?: string, trusted?: boolean };
} }
@@ -46,7 +46,6 @@ function judgeCase(c: NormalizedCase) {
let score = 0; let score = 0;
let status = STATUS.STATUS_ACCEPTED; let status = STATUS.STATUS_ACCEPTED;
let message: any; let message: any;
const detail = ctx.config.detail ?? true;
for (let i = 0; i < ctx.config.num_processes; i++) { for (let i = 0; i < ctx.config.num_processes; i++) {
const result = res[i + 1]; const result = res[i + 1];
time += result.time; time += result.time;
@@ -55,7 +54,7 @@ function judgeCase(c: NormalizedCase) {
else if (result.memory > c.memory * 1024) status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; else if (result.memory > c.memory * 1024) status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
else if ((result.code && result.code !== 13 /* Broken Pipe */) || (result.code === 13 && !resManager.code)) { else if ((result.code && result.code !== 13 /* Broken Pipe */) || (result.code === 13 && !resManager.code)) {
status = STATUS.STATUS_RUNTIME_ERROR; status = STATUS.STATUS_RUNTIME_ERROR;
if (detail) { if (ctx.config.detail === 'full') {
if (result.code < 32 && result.signalled) message = signals[result.code]; if (result.code < 32 && result.signalled) message = signals[result.code];
else message = { message: 'Your program returned {0}.', params: [result.code] }; else message = { message: 'Your program returned {0}.', params: [result.code] };
} }
+2 -3
View File
@@ -28,7 +28,6 @@ function judgeCase(c: NormalizedCase) {
let { status } = res; let { status } = res;
let message: any = ''; let message: any = '';
let score = 0; let score = 0;
const detail = ctx.config.detail ?? true;
if (status === STATUS.STATUS_ACCEPTED) { if (status === STATUS.STATUS_ACCEPTED) {
if (time > c.time) { if (time > c.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
@@ -44,7 +43,7 @@ function judgeCase(c: NormalizedCase) {
user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' }, user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' },
user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' }, user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' },
score: c.score, score: c.score,
detail, detail: ctx.config.detail,
env: { env: {
...ctx.env, ...ctx.env,
HYDRO_TESTCASE: c.id.toString(), HYDRO_TESTCASE: c.id.toString(),
@@ -53,7 +52,7 @@ function judgeCase(c: NormalizedCase) {
}, },
})); }));
} }
} else if (status === STATUS.STATUS_RUNTIME_ERROR && code && detail) { } else if (status === STATUS.STATUS_RUNTIME_ERROR && code && ctx.config.detail === 'full') {
if (code < 32 && signalled) message = signals[code]; if (code < 32 && signalled) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] }; else message = { message: 'Your program returned {0}.', params: [code] };
} }
+1 -1
View File
@@ -66,7 +66,7 @@ export async function judge(ctx: Context) {
user_stderr: { fileId: res.fileIds.stderr }, user_stderr: { fileId: res.fileIds.stderr },
code: ctx.code, code: ctx.code,
score: 100, score: 100,
detail: ctx.config.detail ?? true, detail: ctx.config.detail ?? 'full',
env: { ...ctx.env, HYDRO_TESTCASE: '0' }, env: { ...ctx.env, HYDRO_TESTCASE: '0' },
})); }));
} }
+1 -2
View File
@@ -39,12 +39,11 @@ function judgeCase(c: NormalizedCase) {
let status: number; let status: number;
let score = 0; let score = 0;
let message: any = ''; let message: any = '';
const detail = ctx.config.detail ?? true;
if (time > c.time) { if (time > c.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > c.memory * 1024) { } else if (memory > c.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
} else if (detail && ((code && code !== 13/* Broken Pipe */) || (code === 13 && !resInteractor.code))) { } else if (ctx.config.detail === 'full' && ((code && code !== 13/* Broken Pipe */) || (code === 13 && !resInteractor.code))) {
status = STATUS.STATUS_RUNTIME_ERROR; status = STATUS.STATUS_RUNTIME_ERROR;
if (code < 32 && signalled) message = signals[code]; if (code < 32 && signalled) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] }; else message = { message: 'Your program returned {0}.', params: [code] };
@@ -50,7 +50,7 @@ function judgeCase(c: NormalizedCase) {
user_stderr: { content: '' }, user_stderr: { content: '' },
code: ctx.code, code: ctx.code,
score: c.score, score: c.score,
detail: ctx.config.detail ?? true, detail: ctx.config.detail,
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
})); }));
} }
+3 -3
View File
@@ -1,8 +1,8 @@
import { STATUS } from '@hydrooj/common'; import { DetailType, STATUS } from '@hydrooj/common';
const operation = /^\s*(status|score)\((\d+)\)\s*(.*)$/m; const operation = /^\s*(status|score)\((\d+)\)\s*(.*)$/m;
export function parse(output: string, fullscore: number) { export function parse(output: string, fullscore: number, detail: DetailType) {
let status = STATUS.STATUS_WRONG_ANSWER; let status = STATUS.STATUS_WRONG_ANSWER;
let score = 0; let score = 0;
let builder = (msg: string) => msg; let builder = (msg: string) => msg;
@@ -51,5 +51,5 @@ export function parse(output: string, fullscore: number) {
score = +val; score = +val;
} }
} }
return { status, score, message: builder(message) }; return { status, score, message: builder(detail === 'full' ? message : '') };
} }
@@ -86,7 +86,7 @@ const problemConfigSchema: JSONSchema7 = {
cases: { $ref: '#/definitions/cases' }, cases: { $ref: '#/definitions/cases' },
subtasks: { type: 'array', items: { $ref: '#/definitions/subtask' } }, subtasks: { type: 'array', items: { $ref: '#/definitions/subtask' } },
filename: { type: 'string' }, filename: { type: 'string' },
detail: { type: 'boolean' }, detail: { type: 'string', enum: ['full', 'case', 'none'] },
time: { $ref: '#/definitions/time' }, time: { $ref: '#/definitions/time' },
memory: { $ref: '#/definitions/memory' }, memory: { $ref: '#/definitions/memory' },
score: { $ref: '#/definitions/score' }, score: { $ref: '#/definitions/score' },