feat(题目): 「运行例子」「自己输入」改用和提交同一台判题机——例子上跑出什么样,提交在这几个例子上就是什么样
Deploy / deploy (push) Canceled after 0s

原来试运行走另一台 Judge0:C 是 GCC 9、编译参数也不一样,Python 的语法错误回来的是运行错误,
没有题目的时间内存限制和代码模板,例子上的结果常和提交对不上。现在走本站判题机
(POST /trial-runs),同一套编译命令、这道题的限制和模板,对不对也由判题机比。

- 不落库、不算提交、不进判题队列:api 进程里自己排队,全站同时 TRIAL_CONCURRENCY 批
  (服务器 2、机房 4),每人最多两批,排满或等超过 30 秒就请学生过一会儿再点
- 死循环不停打印时判题机回包会很大,超过 16MB 就掐断,告诉学生输出太多、停不下来
- Python 语法错误用和提交一样的说明:第几行、中文标点标红、一键换成英文
- 运行时错误和提交用同一个解析,说清楚第几行、为什么(下标越界、int() 转换失败、段错误……);
  比赛里照旧只给按结果码的那一句
- 语法没过、服务没跑成时,标题不再说「N 个例子没对上」
- 后台「生成测试点」一并迁过来,编译警告不会再写进 .out;Judge0 的地址和调用全部删掉

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-29 08:09:44 +08:00
1 parent 8834536135
commit eb32676de4
27 files changed
+691 -169

No files matched your search

@@ -13,10 +13,21 @@ import TrialErrorNote from "./TrialErrorNote.vue"
*/
const submissionStore = useSubmissionStore()
const { customInput, customOutput, customResult, customRunning } = submissionStore.trial
const {
customInput,
customOutput,
customResult,
customNote,
customRuntimeError,
customCode,
customRunning,
} = submissionStore.trial
const { problem } = storeToRefs(useProblemStore())
const codeStore = useCodeStore()
const theme = useThemeVars()
const route = useRoute()
// 比赛里出错只给按结果码的那一句,不说第几行、为什么(和「运行例子」一样)
const inContest = computed(() => !!route.params.contestID)
function fillSample(input: string) {
customInput.value = input
@@ -61,6 +72,10 @@ function fillSample(input: string) {
:result="customResult"
:output="customOutput"
:language="codeStore.code.language"
:note="customNote"
:runtime-error="customRuntimeError"
:code="customCode"
:plain="inContest"
/>
<pre v-else class="output" :class="{ empty: !customOutput }">{{
customResult === null ? "" : customOutput || "(什么都没有输出)"
@@ -51,9 +51,9 @@ const { isDesktop } = useBreakpoints()
const theme = useThemeVars()
/**
* 「运行例子」:用题目里的例子试跑(Judge0),结果在左栏「结果」页签。顶替原来题面里
* 「运行例子」:用题目里的例子试跑(本站判题机),结果在左栏「结果」页签。顶替原来题面里
* 每个例子旁的「测试」按钮 —— 那个在左栏、离编辑器远,只给通过 / 不通过、2 秒后复位。
* Judge0 跑不了 SQL,流程图也没得跑;没有例子的题不给。
* SQL 不走判题机,流程图也没得跑;没有例子的题不给。
*/
const submissionStore = useSubmissionStore()
const { samplesRunning } = submissionStore.trial
@@ -17,7 +17,7 @@ const submissionStore = useSubmissionStore()
const { samplesRunning } = submissionStore.trial
const { problem } = storeToRefs(useProblemStore())
// 和工具栏同一个条件:Judge0 跑不了 SQL,流程图没得跑,没有例子的题不给
// 和工具栏同一个条件:SQL 不走判题机、流程图没得跑,没有例子的题不给
const canRunSamples = computed(
() =>
!!problem.value?.samples.length &&
@@ -80,7 +80,7 @@ const codeSegments = computed<{ value: Segment; label: string; time: string }[]>
/** 正在画流程图:结果就是 AI 的点评,没有例子可跑 */
const drawing = computed(() => codeStore.code.language === "Flowchart")
// 试跑走 Judge0,它跑不了 SQL;流程图也没得跑。这两种只留「提交结果」
// 试跑走判题机,SQL 题不走那里;流程图也没得跑。这两种只留「提交结果」
const canTrial = computed(() => {
const languages = problem.value?.languages ?? []
return !languages.includes("SQL")
@@ -20,6 +20,39 @@ const failedCount = computed(
() => sampleRuns.value.filter((run) => run.result !== SubmissionStatus.accepted).length,
)
/**
* 一个例子都没跑起来:编译 / 语法没过,或者试跑服务本身没跑成(排队满了、判题机连不上)。
* 这时不能说「N 个例子没对上」—— 程序一行都没跑,每个例子也都是同一个原因,芯片不摆
*/
const header = computed(() => {
const runs = sampleRuns.value
const first = runs[0]
if (first && runs.every((run) => run.result === SubmissionStatus.compile_error)) {
return {
kind: "warning" as const,
title:
samplesCode.value?.language === "Python"
? "试跑:代码有语法错误,一个例子都没跑"
: "试跑:编译没通过,一个例子都没跑",
chips: false,
}
}
if (first?.note && runs.every((run) => run.note === first.note)) {
// 输出太多是程序自己的问题(多半死循环),其余带 note 的都是没跑成
const ran = first.result === SubmissionStatus.runtime_error
return {
kind: ran ? ("error" as const) : ("warning" as const),
title: ran ? "试跑:程序一直在输出,停不下来" : "试跑没跑成",
chips: false,
}
}
return {
kind: failedCount.value ? ("error" as const) : ("success" as const),
title: failedCount.value ? `试跑:${failedCount.value} 个例子没对上` : "试跑:例子都对上了",
chips: true,
}
})
/** 看的是哪一个例子:默认第一个没对上的 */
const selected = ref(0)
watch(sampleRuns, (runs) => {
@@ -43,12 +76,8 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
<n-flex v-else vertical :size="12">
<!-- 设计稿「运行例子:例子 2 没对上(不算提交)」:标题、「不算提交」、各例子的芯片在一行 -->
<ResultHeader
:kind="failedCount ? 'error' : 'success'"
:title="failedCount ? `试跑:${failedCount} 个例子没对上` : '试跑:例子都对上了'"
sub="(不算提交)"
>
<template #extra>
<ResultHeader :kind="header.kind" :title="header.title" sub="(不算提交)">
<template v-if="header.chips" #extra>
<div class="runs">
<button
v-for="run in sampleRuns"
@@ -134,6 +163,10 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
:result="current.result"
:output="current.output"
:language="samplesCode?.language ?? ''"
:note="current.note"
:runtime-error="current.runtimeError"
:code="samplesCode?.value ?? ''"
:plain="inContest"
/>
</template>
@@ -1,38 +1,79 @@
<script setup lang="ts">
import { SubmissionStatus } from "utils/constants"
import type { StatisticInfo } from "@oj2/contract"
import PythonErrorExplain from "./PythonErrorExplain.vue"
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
/**
* 试跑(Judge0)没跑完时给学生看的中文说明。Judge0 回来的是英文原文(Traceback、
* gcc 的报错),学生读不懂,原文收进折叠区。
* 试跑没跑完时给学生看的中文说明。判题机回来的是英文原文(Traceback、gcc 的报错),
* 学生读不懂,原文收进折叠区。Python 的语法错误是判题机上 CPython 的原文,和提交判出来的
* 一样,直接交给 PythonErrorExplain(中文说明 + 编辑器里标出位置 + 一键换中文标点)。
*/
const props = defineProps<{
result: SubmissionStatus
output: string
language: string
/** 调用方给的说明(没跑成的原因、输出太多),有它就不用下面按结果码挑的那句 */
note?: string
/** 运行时错误的诊断(后端和提交同一个解析),有它就说清楚第几行、为什么 */
runtimeError?: NonNullable<StatisticInfo["runtime_error"]> | null
/** 跑的那份代码,摆出出错的那一行 */
code?: string
/**
* 比赛里:不给具体到哪一行、什么原因的说明(设计文档第 8 节「运行例子只给对 / 没对上和
* 原样输出」;提交那边比赛里也只给判题状态),只留按结果码的那一句
*/
plain?: boolean
}>()
/** 运行时错误、而且诊断出了东西:用和提交一样的说明卡片(第几行 + 中文原因 + 编辑器标红) */
const runtimeExplain = computed(
() =>
!props.note &&
!props.plain &&
props.result === SubmissionStatus.runtime_error &&
!!props.runtimeError,
)
const pythonSyntaxError = computed(
() =>
!props.note &&
props.result === SubmissionStatus.compile_error &&
props.language === "Python" &&
!!props.output,
)
const message = computed(() => {
if (props.note) return props.note
switch (props.result) {
case SubmissionStatus.compile_error:
return props.language === "Python"
? "代码有语法错误,程序一行都没跑。最常见的是用了中文标点(,:()“”)。按「提交」会先检查一遍,告诉你错在哪一行,中文标点能一键换成英文。"
? "代码有语法错误,程序一行都没跑。最常见的是用了中文标点(,:()“”)。"
: "代码没能编译通过,写法有错误。看看是不是漏了分号、括号没配对、变量没声明。"
case SubmissionStatus.runtime_error:
return "程序运行到一半出错,停下来了。检查一下输入是怎么读的、下标有没有越界、除数是不是 0。"
case SubmissionStatus.cpu_time_limit_exceeded:
case SubmissionStatus.real_time_limit_exceeded:
return "运行超时了:程序跑了太久还没结束。看看是不是有死循环,或者在等一个例子里没有的输入。"
return "运行超时了:程序跑了太久还没结束。看看是不是有死循环(条件一直成立、循环变量忘了改),或者在等一个例子里没有的输入(多写了一个 input)。"
case SubmissionStatus.memory_limit_exceeded:
return "程序占的内存太多了。通常是列表、数组开得太大,或者在循环里不停地往里加东西、停不下来。"
default:
return "试跑的服务暂时连不上,稍后再试一次。"
}
})
/** 判题机的临时目录名(`/judger/run/<32 位随机串>/`)对学生没有意义,只剩文件名 */
const rawOutput = computed(() => props.output.replace(/\/judger\/run\/[^/"]+\//g, ""))
</script>
<template>
<n-flex vertical :size="8">
<n-alert type="error" :show-icon="false">{{ message }}</n-alert>
<n-collapse v-if="output">
<PythonErrorExplain v-if="pythonSyntaxError" :err-info="output" />
<n-flex v-else vertical :size="8">
<RuntimeErrorExplain v-if="runtimeExplain" :info="runtimeError!" :code="code ?? ''" />
<n-alert v-else type="error" :show-icon="false">{{ message }}</n-alert>
<n-collapse v-if="rawOutput">
<n-collapse-item title="原始输出(英文)" name="raw">
<pre class="raw">{{ output }}</pre>
<pre class="raw">{{ rawOutput }}</pre>
</n-collapse-item>
</n-collapse>
</n-flex>