feat(judge): implement full testcase support and result recording

- Add testcase handling with proper input/output validation
- Modify run function to process multiple testcases sequentially
- Implement testcase result recording in database
- Improve error handling and status updates
- Add related type definitions for testcases
This commit is contained in:
cfngc4594 2025-04-11 16:01:44 +08:00
parent 4da3723195
commit a81be5c0f9

View File

@ -8,7 +8,8 @@ import { auth } from "@/lib/auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { Readable, Writable } from "stream"; import { Readable, Writable } from "stream";
import { Status } from "@/generated/client"; import { Status } from "@/generated/client";
import type { EditorLanguage, Submission } from "@/generated/client"; import type { ProblemWithTestcases, TestcaseWithDetails } from "@/types/prisma";
import type { EditorLanguage, Submission, TestcaseResult } from "@/generated/client";
const isRemote = process.env.DOCKER_HOST_MODE === "remote"; const isRemote = process.env.DOCKER_HOST_MODE === "remote";
@ -82,7 +83,14 @@ export async function judge(
try { try {
const problem = await prisma.problem.findUnique({ const problem = await prisma.problem.findUnique({
where: { id: problemId }, where: { id: problemId },
}); include: {
testcases: {
include: {
data: true,
},
},
},
}) as ProblemWithTestcases | null;
if (!problem) { if (!problem) {
submission = await prisma.submission.create({ submission = await prisma.submission.create({
@ -119,6 +127,22 @@ export async function judge(
return submission; return submission;
} }
const testcases = problem.testcases;
if (!testcases || testcases.length === 0) {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: "Testcases not found",
},
});
return submission;
}
const { const {
image, image,
tag, tag,
@ -133,6 +157,7 @@ export async function judge(
if (await prepareEnvironment(image, tag)) { if (await prepareEnvironment(image, tag)) {
container = await createContainer(image, tag, workingDir, problem.memoryLimit); container = await createContainer(image, tag, workingDir, problem.memoryLimit);
} else { } else {
console.error("Docker image not found:", image, ":", tag);
submission = await prisma.submission.create({ submission = await prisma.submission.create({
data: { data: {
language, language,
@ -168,7 +193,7 @@ export async function judge(
} }
// Run the code // Run the code
const runResult = await run(container, fileName, problem.timeLimit, runOutputLimit, submission.id); const runResult = await run(container, fileName, problem.timeLimit, runOutputLimit, submission.id, testcases);
return runResult; return runResult;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -305,7 +330,14 @@ async function run(
timeLimit: number = 1000, timeLimit: number = 1000,
maxOutput: number = 1 * 1024 * 1024, maxOutput: number = 1 * 1024 * 1024,
submissionId: string, submissionId: string,
testcases: TestcaseWithDetails,
): Promise<Submission> { ): Promise<Submission> {
let finalSubmission: Submission | null = null;
for (const testcase of testcases) {
const sortedData = testcase.data.sort((a, b) => a.index - b.index);
const inputData = sortedData.map(d => d.value).join("\n");
const runExec = await container.exec({ const runExec = await container.exec({
Cmd: [`./${fileName}`], Cmd: [`./${fileName}`],
AttachStdout: true, AttachStdout: true,
@ -313,56 +345,63 @@ async function run(
AttachStdin: true, AttachStdin: true,
}); });
return new Promise<Submission>((resolve, reject) => { const result = await new Promise<Submission | TestcaseResult>((resolve, reject) => {
const stdoutChunks: string[] = []; // Start the exec stream
let stdoutLength = 0; runExec.start({ hijack: true }, async (error, stream) => {
const stdoutStream = new Writable({ if (error || !stream) {
write(chunk, _encoding, callback) { const submission = await prisma.submission.update({
let text = chunk.toString(); where: { id: submissionId },
if (stdoutLength + text.length > maxOutput) { data: {
text = text.substring(0, maxOutput - stdoutLength); status: Status.SE,
stdoutChunks.push(text); message: "System Error",
stdoutLength = maxOutput;
callback();
return;
} }
})
return resolve(submission);
}
stream.write(inputData);
stream.end();
const stdoutChunks: string[] = [];
const stderrChunks: string[] = [];
let stdoutLength = 0;
let stderrLength = 0;
const stdoutStream = new Writable({
write: (chunk, _, callback) => {
const text = chunk.toString();
if (stdoutLength + text.length > maxOutput) {
stdoutChunks.push(text.substring(0, maxOutput - stdoutLength));
stdoutLength = maxOutput;
} else {
stdoutChunks.push(text); stdoutChunks.push(text);
stdoutLength += text.length; stdoutLength += text.length;
}
callback(); callback();
}, }
}); });
const stderrChunks: string[] = [];
let stderrLength = 0;
const stderrStream = new Writable({ const stderrStream = new Writable({
write(chunk, _encoding, callback) { write: (chunk, _, callback) => {
let text = chunk.toString(); const text = chunk.toString();
if (stderrLength + text.length > maxOutput) { if (stderrLength + text.length > maxOutput) {
text = text.substring(0, maxOutput - stderrLength); stderrChunks.push(text.substring(0, maxOutput - stderrLength));
stderrChunks.push(text);
stderrLength = maxOutput; stderrLength = maxOutput;
callback(); } else {
return;
}
stderrChunks.push(text); stderrChunks.push(text);
stderrLength += text.length; stderrLength += text.length;
callback();
},
});
// Start the exec stream
runExec.start({ hijack: true }, (error, stream) => {
if (error || !stream) {
return reject({ message: "System Error", status: Status.SE });
} }
callback();
stream.write("[2,7,11,15]\n9\n[3,2,4]\n6\n[3,3]\n6"); }
stream.end(); });
docker.modem.demuxStream(stream, stdoutStream, stderrStream); docker.modem.demuxStream(stream, stdoutStream, stderrStream);
const startTime = Date.now();
// Timeout mechanism // Timeout mechanism
const timeoutId = setTimeout(async () => { const timeoutId = setTimeout(async () => {
stream.destroy(); // Destroy the stream to stop execution
const updatedSubmission = await prisma.submission.update({ const updatedSubmission = await prisma.submission.update({
where: { id: submissionId }, where: { id: submissionId },
data: { data: {
@ -378,36 +417,58 @@ async function run(
const stdout = stdoutChunks.join(""); const stdout = stdoutChunks.join("");
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const exitCode = (await runExec.inspect()).ExitCode; const exitCode = (await runExec.inspect()).ExitCode;
const executionTime = Date.now() - startTime;
let updatedSubmission: Submission;
// Exit code 0 means successful execution // Exit code 0 means successful execution
if (exitCode === 0) { if (exitCode === 0) {
updatedSubmission = await prisma.submission.update({ const expectedOutput = testcase.expectedOutput;
where: { id: submissionId }, const testcaseResult = await prisma.testcaseResult.create({
data: { data: {
status: Status.AC, isCorrect: stdout.trim() === expectedOutput.trim(),
message: stdout, output: stdout,
executionTime,
submissionId,
testcaseId: testcase.id,
} }
}) })
resolve(testcaseResult);
} else if (exitCode === 137) { } else if (exitCode === 137) {
updatedSubmission = await prisma.submission.update({ await prisma.testcaseResult.create({
data: {
isCorrect: false,
output: stdout,
executionTime,
submissionId,
testcaseId: testcase.id,
}
})
const updatedSubmission = await prisma.submission.update({
where: { id: submissionId }, where: { id: submissionId },
data: { data: {
status: Status.MLE, status: Status.MLE,
message: stderr || "Memory Limit Exceeded", message: stderr || "Memory Limit Exceeded",
} }
}) })
resolve(updatedSubmission);
} else { } else {
updatedSubmission = await prisma.submission.update({ await prisma.testcaseResult.create({
data: {
isCorrect: false,
output: stdout,
executionTime,
submissionId,
testcaseId: testcase.id,
}
})
const updatedSubmission = await prisma.submission.update({
where: { id: submissionId }, where: { id: submissionId },
data: { data: {
status: Status.RE, status: Status.RE,
message: stderr || "Runtime Error", message: stderr || "Runtime Error",
} }
}) })
}
resolve(updatedSubmission); resolve(updatedSubmission);
}
}); });
stream.on("error", () => { stream.on("error", () => {
@ -416,4 +477,34 @@ async function run(
}); });
}); });
}); });
if ('status' in result) {
return result;
} else {
if (!result.isCorrect) {
finalSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.WA,
message: "Wrong Answer",
},
include: {
TestcaseResult: true,
}
});
return finalSubmission;
}
}
}
finalSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.AC,
message: "All testcases passed",
},
include: {
TestcaseResult: true,
}
});
return finalSubmission;
} }