feat(题目): 答案错误时拿公开样例重跑,并排显示正确输出和你的输出,按差异类型给中文提示
Deploy / deploy (push) Canceled after 0s

原来答案错误只有四个字。6901 条历史答案错误在公开样例上重跑过:73% 在样例上就错了,
其中约三分之一只差格式(没输出、input() 提示文字、空格换行、中英文标点、小数位数)。

- judge/run.ts 的 checkSamples:判出答案错误后以内联测试点重跑题目样例(output: true),
  记下第一个没过的样例和学生在它上面的输出,写进 statistic_info.sample_check;
  样例全过则只记 passed。全是公开信息,不碰隐藏测试点;比赛不跑(有期末考试)
- 前端按差异归类(数字先于「多了内容」判,免得 007 被当成多打了文字),
  并排显示输入 / 正确输出 / 你的输出,标出第一处不同的行;只差空白时把空格、换行画出来,
  末尾空白不画(判题本来就忽略)
- 提示给具体值(「正确输出是 84.10,你输出的是 84.1」),input("…") 单独提示

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 22:14:24 +08:00
1 parent db70ab62e8
commit 7e964fdbdd
6 files changed
+446 -2

No files matched your search

+77 -1
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto"
import { astRuleSchema, type ContestSubmissionInfo } from "@oj2/contract"
import { astRuleSchema, type ContestSubmissionInfo, type StatisticInfo } from "@oj2/contract"
import { and, eq, inArray } from "drizzle-orm"
import { config } from "../config"
@@ -377,6 +377,69 @@ async function diagnoseRuntimeError(
return parsePythonTraceback(output, row.submission.code, prependLines)
}
/** sample_check 里三段文本各自的上限。样例本身很短,截的主要是学生的输出(死循环打印) */
const SAMPLE_TEXT_LIMIT = 2000
type SampleCheck = NonNullable<StatisticInfo["sample_check"]>
/**
* 答案错误时拿题目的**公开样例**重跑一次,记下第一个没过的样例和学生在它上面的输出,
* 写进 `statistic_info.sample_check`。
*
* 2025 秋以来的 6901 条答案错误在样例上重跑过:73% 在样例上就已经错了。样例写在题面上,
* 所以「输入 700,正确输出 7,你的输出 007」给学生看不泄露任何隐藏数据,而这正是他最
* 缺的线索 —— 原来答案错误只有四个字,连错在哪一类都不知道。其中约三分之一只差格式
* (多了提示文字、空格换行、中英文标点、小数位数),前端按差异类型再补一句中文提示。
*
* 和运行时错误的诊断一样,主判题不开 output;这里的样例是内联传的,输出只回这几个点。
*/
async function checkSamples(
row: {
submission: typeof schema.submission.$inferSelect
problem: typeof schema.problem.$inferSelect
},
source: string,
): Promise<SampleCheck | null> {
const samples = (Array.isArray(row.problem.samples) ? row.problem.samples : [])
.map((item) => asRecord(item))
.filter(
(item): item is { input: string; output: string } =>
typeof item.input === "string" && typeof item.output === "string",
)
if (!samples.length) return null
const response = await requestJudge(
row.submission.language,
source,
row.problem.timeLimit,
row.problem.memoryLimit,
// 题面上的样例输入常常没有结尾换行,而 input() 读到文件末尾没有换行照样能读,
// C 的 scanf 也不在乎。补一个是为了和测试点文件的样子一致
samples.map((item) => ({
input: item.input.endsWith("\n") ? item.input : `${item.input}\n`,
output: item.output,
})),
true,
)
// 在样例上就编译不过不会发生(正式判题刚编译过同一份代码),真遇到了就当没检查
if (response.err || !Array.isArray(response.data)) return null
const cases = [...(response.data as JudgeCase[])].sort(
(left, right) => Number(left.test_case) - Number(right.test_case),
)
const index = cases.findIndex((item) => item.result !== JudgeStatus.ACCEPTED)
if (index < 0) return { passed: true }
const sample = samples[index]!
const output = cases[index]!.output
return {
passed: false,
index,
input: sample.input.slice(0, SAMPLE_TEXT_LIMIT),
expected: sample.output.slice(0, SAMPLE_TEXT_LIMIT),
output: typeof output === "string" ? output.slice(0, SAMPLE_TEXT_LIMIT) : "",
result: cases[index]!.result,
}
}
export async function judgeSubmission(job: JudgeJobData) {
const [row] = await db
.select({
@@ -473,6 +536,19 @@ export async function judgeSubmission(job: JudgeJobData) {
if (diagnosis) statisticInfo.runtime_error = diagnosis
}
// 同上,比赛不跑:比赛里有期末考试,格式提示在那里不该给
if (
result === JudgeStatus.WRONG_ANSWER &&
row.submission.contestId === null &&
row.submission.language !== "SQL"
) {
const check = await checkSamples(row, source).catch((error: unknown) => {
console.warn(`[judge] 样例重跑失败 ${row.submission.id}:`, error)
return null
})
if (check) statisticInfo.sample_check = check
}
if (result === JudgeStatus.ACCEPTED) {
const rules = astRulesForLanguage(row.problem.astRules, row.submission.language)
if (rules.length > 0) {