judge: support binary output (#301)

This commit is contained in:
Yang Gao authored and GitHub committed 2022-02-05 15:36:23 +08:00
1 parent 24cd356f78
commit 2c01eaa967
8 files changed
+89 -72

No files matched your search

+5 -5
View File
@@ -11,12 +11,12 @@ const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h');
interface CheckConfig {
checker_type: string;
stdin: string,
stdout: string,
user_stdout: string,
user_stderr: string,
score: number,
stdin: CopyInFile,
stdout: CopyInFile,
user_stdout: CopyInFile,
user_stderr: CopyInFile,
copyIn?: Record<string, CopyInFile>,
score: number,
detail: boolean,
}
+32 -30
View File
@@ -6,12 +6,12 @@ import { CopyInFile } from './sandbox/interface';
import { parse } from './testlib';
interface CheckConfig {
input: string,
output: string,
user_stdout: string,
user_stderr: string,
score: number,
input: CopyInFile,
output: CopyInFile,
user_stdout: CopyInFile,
user_stderr: CopyInFile,
copyIn: Record<string, CopyInFile>,
score: number,
detail: boolean,
}
@@ -28,8 +28,8 @@ const checkers: Record<string, Checker> = {
async default(config) {
const { stdout } = await run('/usr/bin/diff -BZ usrout answer', {
copyIn: {
usrout: { src: config.user_stdout },
answer: { src: config.output },
usrout: config.user_stdout,
answer: config.output,
...config.copyIn,
},
});
@@ -41,22 +41,24 @@ const checkers: Record<string, Checker> = {
try {
const pt = stdout.split('---');
const u = pt[0].split('\n')[1];
let usr = u.substr(2, u.length - 2).trim().split(' ');
const usr = u.substring(2).trim().split(' ');
const t = pt[1].split('\n')[1];
let std = t.substr(2, t.length - 2).trim().split(' ');
const std = t.substring(2).trim().split(' ');
if (usr.length < std.length) message = 'Standard answer longer than user output.';
else if (usr.length > std.length) message = 'User output longer than standard answer.';
else {
let usrString = usr[0];
let stdString = std[0];
for (const i in usr) {
if (usr[i] !== std[i]) {
usr = usr[i];
std = std[i];
usrString = usr[i];
stdString = std[i];
break;
}
}
if (usr.length > 20) usr = `${usr.substring(0, 16)}...`;
if (std.length > 20) std = `${std.substring(0, 16)}...`;
message = { message: 'Read {0}, expect {1}.', params: [usr, std] };
if (usr.length > 20) usrString = `${usrString.substring(0, 16)}...`;
if (std.length > 20) stdString = `${stdString.substring(0, 16)}...`;
message = { message: 'Read {0}, expect {1}.', params: [usrString, stdString] };
}
} catch (e) {
message = stdout.substring(0, stdout.length - 1 <= 30 ? stdout.length - 1 : 30);
@@ -74,8 +76,8 @@ const checkers: Record<string, Checker> = {
async strict(config) {
const { stdout } = await run('/usr/bin/diff usrout answer', {
copyIn: {
usrout: { src: config.user_stdout },
answer: { src: config.output },
usrout: config.user_stdout,
answer: config.output,
...config.copyIn,
},
});
@@ -96,9 +98,9 @@ const checkers: Record<string, Checker> = {
async hustoj(config) {
const { code, stdout } = await run('${dir}/checker input answer usrout', {
copyIn: {
usrout: { src: config.user_stdout },
answer: { src: config.output },
input: { src: config.input },
usrout: config.user_stdout,
answer: config.output,
input: config.input,
...config.copyIn,
},
});
@@ -121,9 +123,9 @@ const checkers: Record<string, Checker> = {
async lemon(config) {
const { files } = await run(`\${dir}/checker input usrout answer ${config.score} score message`, {
copyIn: {
usrout: { src: config.user_stdout },
answer: { src: config.output },
input: { src: config.input },
usrout: config.user_stdout,
answer: config.output,
input: config.input,
...config.copyIn,
},
copyOut: ['score', 'message'],
@@ -147,8 +149,8 @@ const checkers: Record<string, Checker> = {
async qduoj(config) {
const { status, stdout } = await run('${dir}/checker input usrout', {
copyIn: {
usrout: { src: config.user_stdout },
input: { src: config.input },
usrout: config.user_stdout,
input: config.input,
...config.copyIn,
},
});
@@ -173,9 +175,9 @@ const checkers: Record<string, Checker> = {
// eslint-disable-next-line prefer-const
let { status, stdout, stderr } = await run('${dir}/checker', {
copyIn: {
input: { src: config.input },
user_out: { src: config.user_stdout },
answer: { src: config.output },
input: config.input,
user_out: config.user_stdout,
answer: config.output,
code: { content: '' },
...config.copyIn,
},
@@ -189,9 +191,9 @@ const checkers: Record<string, Checker> = {
async testlib(config) {
const { stderr, status } = await run('${dir}/checker ${dir}/in ${dir}/user_out ${dir}/answer', {
copyIn: {
in: { src: config.input },
user_out: { src: config.user_stdout },
answer: { src: config.output },
in: config.input,
user_out: config.user_stdout,
answer: config.output,
...config.copyIn,
},
});
+2 -1
View File
@@ -3,10 +3,11 @@ import { STATUS } from '@hydrooj/utils/lib/status';
import { CompileError } from './error';
import { Execute } from './interface';
import { del, run } from './sandbox';
import { CopyInFile } from './sandbox/interface';
import { compilerText } from './utils';
export = async function compile(
lang: LangConfig, code: string, target: string, copyIn: any = {}, next?: Function,
lang: LangConfig, code: string, target: string, copyIn: Record<string, CopyInFile> = {}, next?: Function,
): Promise<Execute> {
target = lang.target || target;
copyIn[lang.code_file] = { content: code };
+1 -1
View File
@@ -8,5 +8,5 @@ export interface Execute {
export interface CompileErrorInfo {
stdout?: string,
stderr?: string,
status?: string
status?: number
}
+13 -17
View File
@@ -1,12 +1,10 @@
import path from 'path';
import fs from 'fs-extra';
import Queue from 'p-queue';
import { STATUS } from '@hydrooj/utils/lib/status';
import { check, compileChecker } from '../check';
import compile from '../compile';
import { getConfig } from '../config';
import { CompileError, FormatError } from '../error';
import { run } from '../sandbox';
import { del, run } from '../sandbox';
import signals from '../signals';
import { parseFilename } from '../utils';
import {
@@ -40,26 +38,25 @@ function judgeCase(c: Case, sid: string) {
const { filename } = ctx.config;
const copyIn = { ...ctx.execute.copyIn };
if (filename) copyIn[`${filename}.in`] = c.input ? { src: c.input } : { content: '' };
const copyOut = filename ? [`${filename}.out?`] : [];
const copyOutCached = filename ? [`${filename}.out?`] : [];
const stdin = filename ? null : c.input;
const stdout = path.resolve(ctx.tmpdir, `${c.id}.out`);
const stderr = path.resolve(ctx.tmpdir, `${c.id}.err`);
const res = await run(
ctx.execute.execute.replace(/\$\{name\}/g, 'code'),
{
stdin,
stdout: filename ? null : stdout,
stderr,
copyIn,
copyOut,
copyOutCached,
time: ctxSubtask.subtask.time * ctx.execute.time,
memory: ctxSubtask.subtask.memory,
cacheStdoutAndStderr: true,
},
);
const { code, time_usage_ms, memory_usage_kb } = res;
let { status } = res;
if (res.files[`${filename}.out`] || !fs.existsSync(stdout)) {
fs.writeFileSync(stdout, res.files[`${filename}.out`] || '');
let stdout = { fileId: res.fileIds['stdout'] };
const stderr = { fileId: res.fileIds['stderr'] };
if (res.fileIds[`${filename}.out`]) {
stdout = { fileId: res.fileIds[`${filename}.out`] };
}
let message: any = '';
let score = 0;
@@ -71,8 +68,8 @@ function judgeCase(c: Case, sid: string) {
} else {
[status, score, message] = await check({
copyIn: ctx.checker.copyIn,
stdin: c.input,
stdout: c.output,
stdin: { src: c.input },
stdout: { src: c.output },
user_stdout: stdout,
user_stderr: stderr,
checker_type: ctx.config.checker_type,
@@ -84,10 +81,9 @@ function judgeCase(c: Case, sid: string) {
if (code < 32) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] };
}
await Promise.all([
fs.remove(stdout),
fs.remove(stderr),
]).catch(() => { /* Ignore file doesn't exist */ });
await Promise.all(
Object.values(res.fileIds).map((id) => del(id)),
).catch(() => { /* Ignore file doesn't exist */ });
if (runner && ctx.rerun && status === STATUS.STATUS_TIME_LIMIT_EXCEEDED) {
ctx.rerun--;
await runner(ctx, ctxSubtask);
+4 -4
View File
@@ -97,10 +97,10 @@ export const judge = async (ctx: Context) => {
} else {
[status, , message] = await check({
copyIn: copyInDir(path.resolve(ctx.tmpdir, 'checker')),
stdin: input,
stdout,
user_stdout: stdout,
user_stderr: stderr,
stdin: { src: input },
stdout: { src: stdout },
user_stdout: { src: stdout },
user_stderr: { src: stderr },
checker_type: ctx.config.checker_type,
score: 100,
detail: ctx.config.detail,
+5 -4
View File
@@ -3,7 +3,7 @@ import Queue from 'p-queue';
import { STATUS } from '@hydrooj/utils/lib/status';
import compile from '../compile';
import { getConfig } from '../config';
import { run } from '../sandbox';
import { runPiped } from '../sandbox';
import signals from '../signals';
import { parse } from '../testlib';
import { findFileSync, parseFilename } from '../utils';
@@ -22,20 +22,21 @@ function judgeCase(c: Case) {
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
ctx.executeInteractor.copyIn.in = c.input ? { src: c.input } : { content: '' };
ctx.executeInteractor.copyIn.out = c.output ? { src: c.output } : { content: '' };
const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await run([
const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await runPiped(
{
execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'),
copyIn: ctx.executeUser.copyIn,
time: ctxSubtask.subtask.time * ctx.executeUser.time,
memory: ctxSubtask.subtask.memory,
}, {
},
{
execute: `${ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'interactor')} /w/in /w/tout /w/out`,
copyIn: ctx.executeInteractor.copyIn,
time: ctxSubtask.subtask.time * 2 * ctx.executeInteractor.time,
memory: ctxSubtask.subtask.memory * 2,
copyOut: ['/w/tout?'],
},
]);
);
// TODO handle tout (maybe pass to checker?)
let status: number;
let score = 0;
+27 -10
View File
@@ -38,6 +38,19 @@ interface Parameter {
copyIn?: Record<string, CopyInFile>;
copyOut?: string[];
copyOutCached?: string[];
cacheStdoutAndStderr?: boolean;
}
interface SandboxAdaptedResult {
status: number;
code: number;
time_usage_ms: number;
memory_usage_kb?: number;
files: Record<string, string>;
fileIds?: Record<string, string>;
stdout?: string;
stderr?: string;
error?: string;
}
function checkStringArray(args: ParseEntry[]): args is string[] {
@@ -58,12 +71,17 @@ function proc({
memory = parseMemoryMB(getConfig('memoryMax')),
processLimit = getConfig('processLimit'),
stdin = '', copyIn = {}, copyOut = [], copyOutCached = [],
cacheStdoutAndStderr = false,
}: Parameter = {}): Cmd {
if (!supportOptional) {
copyOut = (copyOut as string[]).map((i) => (i.endsWith('?') ? i.substr(0, i.length - 1) : i));
}
const size = parseMemoryMB(getConfig('stdio_size'));
const rate = getConfig('rate');
const copyOutCachedCopy = [...copyOutCached];
if (cacheStdoutAndStderr) {
copyOutCachedCopy.push('stdout', 'stderr');
}
return {
args: parseArgs(execute),
env: getConfig('env').split('\n'),
@@ -80,14 +98,14 @@ function proc({
procLimit: processLimit,
copyIn,
copyOut,
copyOutCached,
copyOutCached: copyOutCachedCopy,
};
}
async function adaptResult(result: SandboxResult, params: Parameter) {
const rate = getConfig('rate');
async function adaptResult(result: SandboxResult, params: Parameter): Promise<SandboxAdaptedResult> {
const rate = getConfig('rate') as number;
// FIXME: Signalled?
const ret: any = {
const ret: SandboxAdaptedResult = {
status: statusMap.get(result.status) || STATUS.STATUS_ACCEPTED,
time_usage_ms: result.time / 1000000 / rate,
memory_usage_kb: result.memory / 1024,
@@ -107,14 +125,14 @@ async function adaptResult(result: SandboxResult, params: Parameter) {
return ret;
}
export async function runMultiple(execute: Parameter[]) {
export async function runPiped(execute0: Parameter, execute1: Parameter): Promise<[SandboxAdaptedResult, SandboxAdaptedResult]> {
let res: SandboxResult[];
const size = parseMemoryMB(getConfig('stdio_size'));
try {
const body = {
cmd: [
proc(execute[0]),
proc(execute[1]),
proc(execute0),
proc(execute1),
],
pipeMapping: [{
in: { index: 0, fd: 1 },
@@ -142,16 +160,15 @@ export async function runMultiple(execute: Parameter[]) {
if (e instanceof FormatError) throw e;
throw new SystemError('Sandbox Error', [e]);
}
return await Promise.all(res.map((r) => adaptResult(r, {})));
return await Promise.all(res.map((r) => adaptResult(r, {}))) as [SandboxAdaptedResult, SandboxAdaptedResult];
}
export async function del(fileId: string) {
await new SandboxClient(getConfig('sandbox_host')).deleteFile(fileId);
}
export async function run(execute: string | Parameter[], params?: Parameter) {
export async function run(execute: string, params?: Parameter): Promise<SandboxAdaptedResult> {
let result: SandboxResult;
if (typeof execute === 'object') return await runMultiple(execute);
try {
const client = new SandboxClient(getConfig('sandbox_host'));
if (!supportOptional) {