diff --git a/.env.example b/.env.example index ac2cc1a40..7aae600f8 100644 --- a/.env.example +++ b/.env.example @@ -73,16 +73,8 @@ SANDBOX_PIDS_LIMIT=64 # Worker PORT=8080 WORKER_CONCURRENCY=4 -# WORKER_MODE: "all" (default), "judge" (sandbox only), "platform" (lifecycle/plagiarism only), -# "test" (checker and interactive Test on the test-judge queue; needs both WASM-OJ dirs below) +# WORKER_MODE: "all" (default), "judge" (sandbox only), "platform" (lifecycle/plagiarism only) WORKER_MODE=all -# Checker and interactive Test through the WASM-OJ test judge. "all" serves the -# test-judge queue only when both WASM-OJ dirs are set (docs/runbooks/getting-started.md). -TEST_JUDGE_ENABLED=false -# WASM_OJ_RUNTIME_DIR=/home/you/src/wasm-oj-forge/crates/runtime-core/target/release -# WASM_OJ_TOOLCHAIN_DIR=/home/you/.cache/nojv-wasm-oj-toolchains -# WASM_OJ_CACHE_DIR=/tmp/wasm-oj -# TEST_JUDGE_SLOTS=2 # Minutes a submission may stay pending/running before the platform sweeper marks # it system_error and terminates its judge. Range 10–1440; defaults to 10. SUBMISSION_PENDING_TIMEOUT_MINUTES=10 diff --git a/.github/renovate.json b/.github/renovate.json index aba640832..16381cd87 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -95,12 +95,6 @@ "matchDepTypes": ["packageManager", "engines"], "enabled": false }, - { - "description": "WASM-OJ packages are pinned to the forge release in the worker image and checked by the pin test; they move by hand with a forge upgrade", - "matchManagers": ["npm"], - "matchPackageNames": ["/^@wasm-oj\\//"], - "enabled": false - }, { "matchManagers": ["github-actions"], "schedule": ["* 9-11 1-7 * 1"], @@ -122,12 +116,6 @@ "matchPackageNames": ["node"], "enabled": false }, - { - "description": "The rust image only builds the pinned WASM-OJ runtime; each bump replaces an 82 MB worker layer, so it moves by hand with a forge upgrade", - "matchDatasources": ["docker"], - "matchPackageNames": ["rust"], - "enabled": false - }, { "description": "alpine/k8s tracks the cluster's kubectl minor", "matchDatasources": ["docker"], diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index a0b52a9e4..cf531edf8 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -270,13 +270,8 @@ "admin_announcementsUnpin": "Unpin", "admin_announcementsUnpublish": "Unpublish", "admin_cancel": "Cancel", - "admin_checkerHelpBody": "Your validator follows the DOMjudge output-validator interface and runs in its own isolated container — the student's program never sees the answer. It is invoked as `validator ` with the student output on stdin. It renders an accept/reject verdict only (no partial scoring). Python: `judge_input`, `judge_answer`, `team_output` are pre-bound as strings, and helpers `accept(team_msg=\"\")`, `wrong(team_msg=\"\")`, `judge_log(msg)` write the right feedback files and exit. C++: read `argv[1]` / `argv[2]` and stdin yourself, write `teammessage.txt` into the `argv[3]` feedback dir, and exit 42 (accept) or 43 (wrong) — no judge header required. Feedback is shown to the student. Validator timeout is 30 seconds; a crash or hang becomes a system error and does not count against the student.", + "admin_checkerHelpBody": "Your validator follows the DOMjudge output-validator interface. On Submit it runs in the judge sandbox, apart from the student's program, which never sees the answer; on Test it runs in the student's browser and checks the samples. It is invoked as `validator ` with the student output on stdin. It renders an accept/reject verdict only (no partial scoring). Python: `judge_input`, `judge_answer`, `team_output` are pre-bound as strings, and `accept(team_msg=\"\")` and `wrong(team_msg=\"\")` write the feedback and exit, and `judge_log(msg)` writes the staff-only judge message. C++: read `argv[1]` / `argv[2]` and stdin yourself, write `teammessage.txt` into the `argv[3]` feedback dir, and exit 42 (accept) or 43 (wrong) — no judge header required. Feedback is shown to the student. The validator gets the larger of 30 seconds and the time limit; a crash or hang becomes a system error and does not count against the student.", "admin_checkerHelpTitle": "How to write a checker", - "admin_checkSamples": "Check samples with the checker", - "admin_checkSamplesRejected": "The checker must accept each sample's output, because students' Test uses it as the answer. Fix the checker or the sample.", - "admin_checkSamplesSaveFirst": "Save your changes first to check samples against them.", - "admin_checkSamplesUnavailable": "The test judge is unavailable right now. Try again later.", - "admin_checkingSamples": "Checking samples…", "admin_compareCaseSensitive": "Case-sensitive comparison", "admin_compareFloatTolerance": "Float tolerance (ε)", "admin_compareFloatToleranceHint": "Leave empty for exact matching. When set, numeric tokens match within absolute OR relative error ε. For anything more, write a Checker.", @@ -306,7 +301,6 @@ "admin_envRemove": "Remove env var", "admin_envVars": "Environment variables", "admin_fileEditable": "Editable", - "admin_fileHidden": "Hidden", "admin_filePath": "Path", "admin_filePathPlaceholder": "main.c or include/header.h", "admin_fileReadonly": "Read-only", @@ -317,16 +311,12 @@ "admin_inputFormatTooltip": "Describe the input format. Supports Markdown.", "admin_interactionFormat": "Interaction notes", "admin_interactionFormatTooltip": "Describe what the interactor reads from its input file and how it talks to the student's program. Shown to students. Supports Markdown.", - "admin_interactorHelpBody": "Your interactor follows the DOMjudge interactive interface and runs in its own isolated container, wired to the student program by a byte proxy: student stdout flows to interactor stdin, interactor stdout flows to student stdin. It is invoked as `interactor `. Python: `judge_input` and `judge_answer` are pre-bound strings; `read()` reads one line from the student (auto-rejecting if the stream closes) and `write(msg)` sends one line back (already flushed). Helpers `accept(team_msg=\"\")`, `wrong(team_msg=\"\")`, `set_score(x)`, `judge_log(msg)` write the feedback files and exit 42/43. C++: read `argv[1]`/`argv[2]`, talk to the solution over `cin`/`cout` (flush before reading), write `score.txt` / `teammessage.txt` into `argv[3]`, and exit 42 (accept) or 43 (wrong) — no judge header required. The student and interactor share the problem's time limit.", + "admin_interactorHelpBody": "Your interactor follows the DOMjudge interactive interface. On Submit it runs in the judge sandbox, and on Test in the student's browser; either way student stdout flows to interactor stdin, and interactor stdout flows to student stdin. It is invoked as `interactor `; Test passes an empty `judge_answer`, so read the secret from `judge_input`. Python: `judge_input` and `judge_answer` are pre-bound strings; `read()` reads one line from the student (auto-rejecting if the stream closes) and `write(msg)` sends one line back (already flushed). `accept(team_msg=\"\")` and `wrong(team_msg=\"\")` write the feedback and exit 42/43, and `judge_log(msg)` writes the staff-only judge message. C++: read `argv[1]`/`argv[2]`, talk to the solution over `cin`/`cout` (flush before reading), write `teammessage.txt` into `argv[3]`, and exit 42 (accept) or 43 (wrong) — no judge header required. The student and interactor share the problem's time limit.", "admin_interactorHelpTitle": "How to write an interactor", "admin_interactorLanguage": "Interactor language", "admin_judgeChecker": "Checker script", "admin_judgeInteractive": "Interactive", - "admin_judgeProgramDiagnostics": "Build output", - "admin_judgeProgramTestFailed": "This judge program can't run in Test. Submissions are still judged normally.", - "admin_judgeProgramTestPending": "Preparing this judge program for Test…", - "admin_judgeProgramTestReady": "Test can run this judge program.", - "admin_judgeProgramTestUnavailable": "Couldn't check this judge program right now.", + "admin_judgeProgramReadableNote": "Students can read this program when they press Test.", "admin_judgeStandard": "Standard (stdin/stdout diff)", "admin_judgeType": "Judge type", "admin_judgeTypeHint": "How each testcase is evaluated.", @@ -403,8 +393,6 @@ "admin_tabBasicInfo": "Basic Info", "admin_tabReports": "Reports", "admin_tabJudge": "Judge Settings", - "admin_testJudgeDisabled": "Test judging isn't enabled on this server.", - "admin_testPythonInteractorUnsupported": "Test doesn't support Python interactors yet.", "admin_tabLocked": "Please complete and save basic info first", "admin_tabLockedFields": "Fill in and save these in Basic Info first: {fields}", "admin_tabOverview": "Overview", @@ -472,7 +460,7 @@ "admin_workspaceEmptyFullSource": "No starter template for {language}. Leaving this blank is fine for single-file problems — students will see an empty editor.", "admin_workspaceEmptyMultiFile": "No files for {language} yet. Multi-file problems need at least a main file named {filename}.", "admin_workspaceFilesHintFullSource": "Optionally provide starter code per language. Students see this template when they open the editor. Leave blank for an empty editor.", - "admin_workspaceFilesHintMultiFile": "Provide one editable main file per allowed language, plus any helper files (readonly or hidden).", + "admin_workspaceFilesHintMultiFile": "Provide one editable main file per allowed language, plus any readonly helper files.", "admin_workspaceFilesTitleFullSource": "Starter templates (optional)", "admin_workspaceFilesTitleMultiFile": "Workspace files", "admin_workspaceLanguageMissing": "Missing template", @@ -1145,23 +1133,23 @@ "editor_invalidSource": "Source code must be nonblank and fit the submission limits: 50,000 characters for a single file, or 500,000 per editable workspace file. Check file paths as well.", "editor_invalidRunCases": "Provide 1–10 testcases, with at most 200,000 characters in each input or expected output.", "editor_requestTooLarge": "The combined source and testcase request exceeds 2 MiB. Reduce its size and try again.", - "admin_workspaceHiddenTestNote": "Browser Test cannot access hidden files. Public code must compile and run without them. If a dependency is safe to share, mark it read-only; otherwise redesign the public test workspace. Hidden files remain server-only.", "editor_clientTestCustomImage": "This problem requires a custom container that cannot run in the browser. Ask the author to provide a browser-compatible Test workspace.", "editor_clientTestLanguage": "The browser runtime for this language is unavailable. Test runs entirely on your device.", "editor_testInteractiveLanguage": "Test can't run interactive problems in JavaScript or TypeScript yet. Switch to another language, or use Submit to have your code judged.", - "editor_testNoInteractiveSamples": "This problem has no sample with an interactor input, so there is nothing to test. Use Submit to have your code judged.", - "editor_testJudgeBusy": "Test is busy right now. Try again in a moment.", - "editor_testJudgeProgramBuildFailed": "This problem's checker or interactor can't run in Test. Submit still judges your code normally.", - "editor_testUnavailableForProblem": "Test isn't available for this problem right now. Submit still judges your code normally.", "editor_testUnsupportedProblemType": "This problem type doesn't support Test. Use Submit to have your code judged.", - "editor_testTooLarge": "This Test request is too large to send to the server. Reduce your program's size or output, or use Submit to have your code judged.", - "editor_judgingOnServer": "Judging on the server…", + "editor_checkerPreparing": "Preparing checker...", + "editor_checkerBuildFailed": "This problem's checker failed to build.", + "editor_checkerLoadFailed": "Couldn't load this problem's checker. Reload the page to try again.", + "editor_checkerUnavailable": "Couldn't load this problem's checker.", + "editor_interactorPreparing": "Preparing interactor...", + "editor_interactorBuildFailed": "This problem's interactor failed to build.", + "editor_interactorLoadFailed": "Couldn't load this problem's interactor. Reload the page to try again.", + "editor_interactorUnavailable": "Couldn't load this problem's interactor.", "editor_judgeSystemError": "The judge failed on this case. This is a problem on the judge's side, not with your program.", - "editor_interactiveTestNote": "Test runs your program against this problem's interactor on each sample's interactor input; the interactor judges your replies live. You can't add your own cases to an interactive problem.", + "editor_interactiveTestNote": "Test runs your program against this problem's interactor on each case's interactor input; the interactor judges your replies live. Cases you add are judged the same way.", "editor_checkerCasesNote": "This problem's checker judges the samples only. Cases you add or change just run without a verdict, so check their output yourself.", "editor_executed": "Executed", "editor_executedNote": "This case ran without being judged. Check its output yourself.", - "editor_judgedOnServer": "Judged on the server", "editor_judgeFeedback": "Feedback", "editor_transcript": "Transcript", "editor_transcriptFromInteractor": "From the interactor", @@ -1596,7 +1584,7 @@ "problemDetail_loadingSource": "Loading source code...", "problemDetail_memoryLimit": "Memory limit", "problemDetail_multiFileBadge": "Multi-file", - "problemDetail_multiFileHelp": "The problem ships multiple files — one or more you edit plus read-only helpers (and sometimes hidden files). You edit the designated files in the browser and submit them together.", + "problemDetail_multiFileHelp": "The problem ships multiple files — one or more you edit plus read-only helpers. You edit the designated files in the browser and submit them together.", "problemDetail_noSubmissions": "No submissions yet.", "problemDetail_output": "Output", "problemDetail_outputFormat": "Output Description", @@ -1907,12 +1895,9 @@ "virtualContest_youLabel": "You", "workspace_description": "Description", "workspace_descriptionPlaceholder": "Describe what this file is for…", - "workspace_fileHidden": "This file is hidden", - "workspace_fileHiddenNoDescription": "(no description provided)", "workspace_filesHeading": "Files", "workspace_mustHaveMainFile": "Each enabled language must have an editable file named {filename}", "workspace_visibilityEditable": "edit", - "workspace_visibilityHidden": "hidden", "workspace_visibilityReadonly": "read", "contestScoreboard_notStartedHint": "The scoreboard opens once the contest begins.", "contestDetail_settingsDeleteConfirmBody": "This will permanently remove the contest draft.", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 8b17b5745..4e04191cb 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -270,13 +270,8 @@ "admin_announcementsUnpin": "取消置頂", "admin_announcementsUnpublish": "取消發佈", "admin_cancel": "取消", - "admin_checkerHelpBody": "你的 validator 採用 DOMjudge 輸出驗證器介面,在獨立隔離容器中執行——學生程式永遠看不到答案。呼叫方式為 `validator `,學生輸出由 stdin 傳入。只輸出接受/錯誤判決(不做部分計分)。Python:`judge_input`、`judge_answer`、`team_output` 已預先注入為字串,輔助函式 `accept(team_msg=\"\")`、`wrong(team_msg=\"\")`、`judge_log(msg)` 會寫入對應回饋檔並結束。C++:自行讀取 `argv[1]` / `argv[2]` 與 stdin,把 `teammessage.txt` 寫進 `argv[3]` 的 feedback 目錄,並以 exit 42(接受)或 43(錯誤)結束——不需任何評測標頭檔。回饋訊息會顯示給學生。Validator 超時上限 30 秒;掛掉或逾時記為系統錯誤,不影響學生判決。", + "admin_checkerHelpBody": "你的 validator 採用 DOMjudge 輸出驗證器介面。提交時在判題沙盒中執行,與學生程式分開,學生程式看不到答案;按「測試」時則在學生的瀏覽器中執行,檢查範例。呼叫方式為 `validator `,學生輸出由 stdin 傳入。只輸出接受/錯誤判決(不做部分計分)。Python:`judge_input`、`judge_answer`、`team_output` 已預先注入為字串,`accept(team_msg=\"\")`、`wrong(team_msg=\"\")` 會寫入回饋並結束,`judge_log(msg)` 則寫入只給助教看的判題訊息。C++:自行讀取 `argv[1]` / `argv[2]` 與 stdin,把 `teammessage.txt` 寫進 `argv[3]` 的 feedback 目錄,並以 exit 42(接受)或 43(錯誤)結束——不需任何評測標頭檔。回饋訊息會顯示給學生。Validator 的時限為 30 秒與題目時限取較大者;掛掉或逾時記為系統錯誤,不影響學生判決。", "admin_checkerHelpTitle": "如何撰寫 Checker", - "admin_checkSamples": "用 checker 檢查範例", - "admin_checkSamplesRejected": "checker 必須接受每個範例的輸出,因為學生的測試會拿它當作答案。請修正 checker 或範例。", - "admin_checkSamplesSaveFirst": "請先儲存變更,才能用新的設定檢查範例。", - "admin_checkSamplesUnavailable": "測試評測目前無法使用,請稍後再試。", - "admin_checkingSamples": "正在檢查範例…", "admin_compareCaseSensitive": "區分大小寫比對", "admin_compareFloatTolerance": "浮點容差(ε)", "admin_compareFloatToleranceHint": "留空為精確比對。設定後,數值 token 在絕對或相對誤差 ε 內即視為相符。需要更複雜的判定請改寫 Checker。", @@ -306,7 +301,6 @@ "admin_envRemove": "移除環境變數", "admin_envVars": "環境變數", "admin_fileEditable": "可編輯", - "admin_fileHidden": "隱藏", "admin_filePath": "路徑", "admin_filePathPlaceholder": "main.c 或 include/header.h", "admin_fileReadonly": "唯讀", @@ -317,16 +311,12 @@ "admin_inputFormatTooltip": "描述輸入資料的格式,支援 Markdown。", "admin_interactionFormat": "互動說明", "admin_interactionFormatTooltip": "說明 interactor 會從輸入檔讀到什麼,以及它如何與學生程式互動。會顯示給學生,支援 Markdown。", - "admin_interactorHelpBody": "你的 interactor 採用 DOMjudge 互動介面,在獨立隔離容器中執行,並由 worker 的位元組代理與學生程式相接:學生 stdout → interactor stdin,interactor stdout → 學生 stdin。呼叫方式為 `interactor `。Python:`judge_input`、`judge_answer` 已預先注入為字串;`read()` 讀學生送來的一行(自動去掉換行符號,若學生提早關閉串流會自動 wrong);`write(msg)` 寫一行給學生(已自動 flush)。輔助函式 `accept(team_msg=\"\")`、`wrong(team_msg=\"\")`、`set_score(x)`、`judge_log(msg)` 會寫入回饋檔並以 exit 42/43 結束。C++:讀取 `argv[1]` / `argv[2]`,透過 `cin` / `cout` 與學生互動(讀取前先 flush),把 `score.txt` / `teammessage.txt` 寫進 `argv[3]`,並以 exit 42(接受)或 43(錯誤)結束——不需任何評測標頭檔。Interactor 與學生共享題目的時間限制。", + "admin_interactorHelpBody": "你的 interactor 採用 DOMjudge 互動介面。提交時在判題沙盒中執行,按「測試」時則在學生的瀏覽器中執行;兩種情況都一樣:學生 stdout → interactor stdin,interactor stdout → 學生 stdin。呼叫方式為 `interactor `;按「測試」時 `judge_answer` 是空的,請改從 `judge_input` 讀取祕密資料。Python:`judge_input`、`judge_answer` 已預先注入為字串;`read()` 讀學生送來的一行(自動去掉換行符號,若學生提早關閉串流會自動 wrong);`write(msg)` 寫一行給學生(已自動 flush)。`accept(team_msg=\"\")`、`wrong(team_msg=\"\")` 會寫入回饋並以 exit 42/43 結束,`judge_log(msg)` 則寫入只給助教看的判題訊息。C++:讀取 `argv[1]` / `argv[2]`,透過 `cin` / `cout` 與學生互動(讀取前先 flush),把 `teammessage.txt` 寫進 `argv[3]`,並以 exit 42(接受)或 43(錯誤)結束——不需任何評測標頭檔。Interactor 與學生共享題目的時間限制。", "admin_interactorHelpTitle": "如何撰寫 Interactor", "admin_interactorLanguage": "Interactor 語言", "admin_judgeChecker": "Checker 腳本", "admin_judgeInteractive": "互動題", - "admin_judgeProgramDiagnostics": "編譯輸出", - "admin_judgeProgramTestFailed": "這個評測程式無法在測試中執行,提交仍會照常評測。", - "admin_judgeProgramTestPending": "正在準備讓測試使用這個評測程式…", - "admin_judgeProgramTestReady": "測試可以使用這個評測程式。", - "admin_judgeProgramTestUnavailable": "目前無法檢查這個評測程式。", + "admin_judgeProgramReadableNote": "學生按下「測試」時可以讀到這支程式。", "admin_judgeStandard": "標準(stdin/stdout 比對)", "admin_judgeType": "評測類型", "admin_judgeTypeHint": "每筆測資的評測方式。", @@ -403,8 +393,6 @@ "admin_tabBasicInfo": "題目資訊", "admin_tabReports": "內容檢舉", "admin_tabJudge": "判題設定", - "admin_testJudgeDisabled": "此伺服器未啟用測試評測。", - "admin_testPythonInteractorUnsupported": "測試尚不支援 Python interactor。", "admin_tabLocked": "請先完成並儲存基本資訊", "admin_tabLockedFields": "請先在「基本資訊」填寫並儲存:{fields}", "admin_tabOverview": "總覽", @@ -472,7 +460,7 @@ "admin_workspaceEmptyFullSource": "{language} 還沒有範本檔案。單一檔案模式可以留空,學生會看到空白編輯器。", "admin_workspaceEmptyMultiFile": "{language} 還沒有任何檔案,至少需要一個名為 {filename} 的 main 檔案。", "admin_workspaceFilesHintFullSource": "可為指定語言提供起始程式碼,學生打開編輯器時會看到。留空就是空白編輯器。", - "admin_workspaceFilesHintMultiFile": "為每個允許的語言提供一個 editable 的 main 檔案,加上任何輔助檔(可設為唯讀或隱藏)。", + "admin_workspaceFilesHintMultiFile": "為每個允許的語言提供一個 editable 的 main 檔案,加上任何唯讀輔助檔。", "admin_workspaceFilesTitleFullSource": "範本程式碼(選用)", "admin_workspaceFilesTitleMultiFile": "工作區檔案", "admin_workspaceLanguageMissing": "缺少範本", @@ -1145,23 +1133,23 @@ "editor_invalidSource": "程式碼不可為空白,單一檔案上限為 50,000 字元,多檔工作區每個可編輯檔案上限為 500,000 字元;也請確認檔名有效。", "editor_invalidRunCases": "請提供 1–10 組測試資料,每組輸入或預期輸出最多 200,000 個字元。", "editor_requestTooLarge": "程式碼與測試資料合計超過 2 MiB,請縮減後重試。", - "admin_workspaceHiddenTestNote": "瀏覽器 Test 無法取得隱藏檔案,公開程式必須能在缺少它們時獨立編譯及執行。若相依檔案可公開,請設為唯讀;否則請調整公開測試工作區的設計。隱藏檔案仍只在伺服器使用。", "editor_clientTestCustomImage": "此題需要自訂容器,無法直接在瀏覽器執行。請出題者提供可在瀏覽器執行的 Test 工作區。", "editor_clientTestLanguage": "目前無法使用此語言的瀏覽器執行環境。Test 全程在你的裝置執行。", "editor_testInteractiveLanguage": "互動題的測試目前還不支援 JavaScript 與 TypeScript。請改用其他語言,或直接提交評測。", - "editor_testNoInteractiveSamples": "此題沒有附 Interactor 輸入的範例,無法測試。請直接提交評測。", - "editor_testJudgeBusy": "測試目前忙碌中,請稍後再試。", - "editor_testJudgeProgramBuildFailed": "此題的 checker 或 interactor 無法在測試中執行,提交仍會照常評測。", - "editor_testUnavailableForProblem": "此題目前無法使用測試,提交仍會照常評測。", "editor_testUnsupportedProblemType": "此題型不支援測試,請直接提交評測。", - "editor_testTooLarge": "這次測試的資料太大,無法送到伺服器。請縮小程式或輸出,或直接提交評測。", - "editor_judgingOnServer": "伺服器評測中…", + "editor_checkerPreparing": "準備 checker 中...", + "editor_checkerBuildFailed": "此題的 checker 編譯失敗。", + "editor_checkerLoadFailed": "無法載入此題的 checker,請重新載入頁面後再試。", + "editor_checkerUnavailable": "無法載入此題的 checker。", + "editor_interactorPreparing": "準備 interactor 中...", + "editor_interactorBuildFailed": "此題的 interactor 編譯失敗。", + "editor_interactorLoadFailed": "無法載入此題的 interactor,請重新載入頁面後再試。", + "editor_interactorUnavailable": "無法載入此題的 interactor。", "editor_judgeSystemError": "評測程式在這組測資出錯,這是評測端的問題,不是你的程式造成的。", - "editor_interactiveTestNote": "測試會用每組範例的 Interactor 輸入,讓你的程式和本題的 interactor 對話,由 interactor 即時判斷你的回應。互動題無法自行新增測資。", + "editor_interactiveTestNote": "測試會用每組測資的 Interactor 輸入,讓你的程式和本題的 interactor 對話,由 interactor 即時判斷你的回應。你自行新增的測資也會這樣評測。", "editor_checkerCasesNote": "本題的 checker 只評測範例測資。你新增或修改的測資只會執行、不會判定對錯,請自行檢查輸出。", "editor_executed": "已執行", "editor_executedNote": "這組測資只有執行、沒有評測,請自行檢查輸出。", - "editor_judgedOnServer": "由伺服器評測", "editor_judgeFeedback": "評測回饋", "editor_transcript": "互動紀錄", "editor_transcriptFromInteractor": "Interactor 送出", @@ -1580,7 +1568,7 @@ "problemDetail_loadingSource": "載入原始碼中...", "problemDetail_memoryLimit": "記憶體限制", "problemDetail_multiFileBadge": "多檔案專案", - "problemDetail_multiFileHelp": "題目提供多個檔案——你要編輯的主程式、唯讀的輔助檔(有時還有隱藏檔)。你在瀏覽器中直接編輯指定的檔案並一起繳交。", + "problemDetail_multiFileHelp": "題目提供多個檔案——你要編輯的主程式和唯讀的輔助檔。你在瀏覽器中直接編輯指定的檔案並一起繳交。", "problemDetail_noSubmissions": "尚無提交紀錄。", "problemDetail_output": "輸出", "problemDetail_outputFormat": "輸出說明", @@ -1891,12 +1879,9 @@ "virtualContest_youLabel": "你", "workspace_description": "說明", "workspace_descriptionPlaceholder": "說明這個檔案的用途…", - "workspace_fileHidden": "此檔案已隱藏", - "workspace_fileHiddenNoDescription": "(未提供說明)", "workspace_filesHeading": "檔案", "workspace_mustHaveMainFile": "每個啟用的語言必須有一個名為 {filename} 的可編輯檔案", "workspace_visibilityEditable": "可編輯", - "workspace_visibilityHidden": "隱藏", "workspace_visibilityReadonly": "唯讀", "contestScoreboard_notStartedHint": "排行榜會在比賽開始後開放。", "contestDetail_settingsDeleteConfirmBody": "此動作將永久刪除競賽草稿。", diff --git a/apps/web/package.json b/apps/web/package.json index 83d9f6cdd..90c787a29 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,7 +36,7 @@ "@opentelemetry/exporter-metrics-otlp-http": "^0.222.0", "@opentelemetry/resources": "^2.11.0", "@opentelemetry/sdk-node": "^0.222.0", - "@wasm-oj/browser": "0.2.3", + "@wasm-oj/browser": "0.2.4", "@wasm-oj/toolchain-clang": "0.2.0", "@wasm-oj/toolchain-go": "0.2.0", "@wasm-oj/toolchain-java": "0.2.0", diff --git a/apps/web/src/lib/components/features/problem/editors/Editor.svelte b/apps/web/src/lib/components/features/problem/editors/Editor.svelte index efc3b5991..481e5e5e0 100644 --- a/apps/web/src/lib/components/features/problem/editors/Editor.svelte +++ b/apps/web/src/lib/components/features/problem/editors/Editor.svelte @@ -37,6 +37,11 @@ prewarmBrowserLocalEngine, supportsBrowserLocalRun, } from "$lib/services/browser-local-run"; + import { + prepareJudgeProgram, + type JudgeProgramProgress, + type PreparedJudgeProgram, + } from "$lib/services/judge-program"; interface Props { allowedLanguages?: Language[] | undefined; @@ -83,6 +88,43 @@ let drafts = $state({ ...initialProblem.starterByLanguage }); let isFullscreen = $state(false); let toolchainPercent = $state(null); + let judgeProgramProgress = $state(null); + let judgeProgramFailure = $state | null>(null); + let judgeProgramPreparation: Promise | null = null; + const judgeProgramAbort = new AbortController(); + onDestroy(() => judgeProgramAbort.abort()); + const judgeProgramMessages = + initialProblem.judgeType === "interactive" + ? { + preparing: m.editor_interactorPreparing, + build_failed: m.editor_interactorBuildFailed, + load_failed: m.editor_interactorLoadFailed, + unavailable: m.editor_interactorUnavailable, + } + : { + preparing: m.editor_checkerPreparing, + build_failed: m.editor_checkerBuildFailed, + load_failed: m.editor_checkerLoadFailed, + unavailable: m.editor_checkerUnavailable, + }; + + function prepareProblemJudgeProgram(): Promise { + judgeProgramPreparation ??= prepareJudgeProgram( + { + problemId: initialProblem.id, + context: untrack(() => context), + signal: judgeProgramAbort.signal, + }, + (progress) => (judgeProgramProgress = progress), + ).then((prepared) => { + judgeProgramProgress = null; + if (!prepared.ok) judgeProgramFailure = prepared; + if (!prepared.ok && prepared.reason === "build_failed") + runController.setBottomTab("result"); + return prepared; + }); + return judgeProgramPreparation; + } let isWorkspaceMode = $derived(isWorkspaceProblem(problem.type)); let isSpecialEnv = $derived(isSpecialEnvProblem(problem.type)); @@ -103,6 +145,7 @@ preloadBrowserToolchain(selected, (progress) => { if (active) toolchainPercent = browserToolchainPercent(progress); }).then(finish, finish); + if (initialProblem.judgeType !== "standard") void prepareProblemJudgeProgram(); }, 0); return () => { @@ -215,6 +258,7 @@ workspaceDrafts: () => workspaceFiles.drafts, workspaceFiles: () => workspaceFilesForLanguage, context: () => context, + judgeProgram: prepareProblemJudgeProgram, onSubmissionDispatched: (id, lang) => onSubmissionDispatched?.(id, lang), onSubmissionComplete: (id, result, lang, src) => onSubmissionComplete?.(id, result, lang, src), @@ -223,20 +267,23 @@ $effect(() => () => runController.markDestroyed()); let testDisabledReason = $derived.by(() => { - const capability = problem.testCapability; - if (!capability.available) { - return capability.reason === "special_env" - ? m.editor_testUnsupportedProblemType() - : m.editor_testUnavailableForProblem(); - } - if (problem.judgeType === "interactive") { - if (!interactiveContestantSupported(language)) return m.editor_testInteractiveLanguage(); - if (!problem.samples.some((sample) => sample.interactorInput?.trim())) - return m.editor_testNoInteractiveSamples(); - } - return runController.testDisabledReason; + if (isSpecialEnv) return m.editor_testUnsupportedProblemType(); + if (problem.judgeType === "interactive" && !interactiveContestantSupported(language)) + return m.editor_testInteractiveLanguage(); + if (judgeProgramFailure) return judgeProgramMessages[judgeProgramFailure.reason](); + return null; }); + let testToolchainPercent = $derived( + toolchainPercent ?? + (judgeProgramProgress?.phase === "toolchain" ? judgeProgramProgress.percent : null), + ); + let testPreparingLabel = $derived( + judgeProgramProgress && judgeProgramProgress.phase !== "toolchain" + ? judgeProgramMessages.preparing() + : null, + ); + function handleShortcut(event: KeyboardEvent) { if (!(event.ctrlKey || event.metaKey) || event.shiftKey || event.altKey) return; if (event.key.toLowerCase() === "s" && !isWorkspaceMode) { @@ -313,7 +360,8 @@
- {#if testDisabledReason !== null} -

- {testDisabledReason} -

- {/if} +

+ {testDisabledReason ?? ""} +

{/if} - {#if interactive && runCases.length === 0} -

- {m.editor_testNoInteractiveSamples()} -

- {:else} -
- {#each runCases as _, index (`tab-${index}`)} -
+ {#each runCases as _, index (`tab-${index}`)} +
+ + {#if runCases.length > 1} - {#if customCasesAllowed && runCases.length > 1} - - {/if} -
- {/each} - {#if customCasesAllowed} - - {/if} -
+ {/if} +
+ {/each} + + + +
+

{caseInputLabel}

+ +
+ {#if interactive} +

+ {m.editor_interactiveTestNote()} +

+ {:else if judgeType === "checker"} +

+ {m.editor_checkerCasesNote()} +

+ {:else}
-

{caseInputLabel}

- {#if customCasesAllowed} - - {:else} -
{runCases[
-                    selectedCase
-                  ]?.input ?? ""}
- {/if} + /> + {m.editor_compareOutput()} + +
- - {#if interactive} -

- {m.editor_interactiveTestNote()} -

- {:else if judgeType === "checker"} -

- {m.editor_checkerCasesNote()} -

- {:else} -
- - -
- {/if} {/if} {/if} @@ -270,9 +252,6 @@ {executedOnly ? m.editor_executed() : formatVerdictLabel(runResult.verdict)} {m.editor_samplesOnly()} - {#if judgedOnServer} - {m.editor_judgedOnServer()} - {/if} {#if runResult.runtimeMs > 0} {runSource === "local" @@ -282,14 +261,6 @@ {/if} - {#if runResult.serverNotice} -

- {runResult.serverNotice} -

- {/if} - {#if runResult.caseResults && runResult.caseResults.length > 0}
{#each runResult.caseResults as cr, index (`rc-${index}`)} @@ -318,7 +289,7 @@
{#if selectedCaseResult?.executionOnly}

{m.editor_executedNote()}

- {:else if selectedCaseResult?.serverJudged && selectedCaseResult.verdict === "SE"} + {:else if selectedCaseResult?.judged && selectedCaseResult.verdict === "SE"}

{m.editor_judgeSystemError()}

@@ -435,6 +406,12 @@

{testDisabledReason ?? m.editor_runFirst()}

+ {#if judgeProgramDiagnostics} +
{formatJudgeOutput(
+                judgeProgramDiagnostics,
+              )}
+ {/if} {/if}
{/if} diff --git a/apps/web/src/lib/components/features/problem/editors/StudentProblemView.svelte b/apps/web/src/lib/components/features/problem/editors/StudentProblemView.svelte index f1ba59f0e..8b7db7460 100644 --- a/apps/web/src/lib/components/features/problem/editors/StudentProblemView.svelte +++ b/apps/web/src/lib/components/features/problem/editors/StudentProblemView.svelte @@ -70,23 +70,16 @@ : 'text-muted-foreground'}" onclick={() => onselect(index)} > - - {#if file.visibility === "hidden"}🔒 - {/if}{file.path} - + {file.path} {file.visibility === "editable" ? m.workspace_visibilityEditable() - : file.visibility === "hidden" - ? m.workspace_visibilityHidden() - : m.workspace_visibilityReadonly()} + : m.workspace_visibilityReadonly()} @@ -117,41 +110,22 @@
{#if selectedFile} {@const file = selectedFile} - {#if file.visibility !== "hidden" && file.description !== ""} + {#if file.description !== ""}

{file.description}

{/if}
- {#if file.visibility === "hidden"} -
-

- {m.workspace_fileHidden()} -

- {#if file.description !== ""} -
- {file.description} -
- {:else} -

- {m.workspace_fileHiddenNoDescription()} -

- {/if} -
- {:else} - {#key `${file.language}::${file.path}`} - - {/key} - {/if} + {#key `${file.language}::${file.path}`} + + {/key}
{/if}
diff --git a/apps/web/src/lib/components/features/problem/editors/editor-bindings.ts b/apps/web/src/lib/components/features/problem/editors/editor-bindings.ts index c53b57721..1e9438bd6 100644 --- a/apps/web/src/lib/components/features/problem/editors/editor-bindings.ts +++ b/apps/web/src/lib/components/features/problem/editors/editor-bindings.ts @@ -244,12 +244,10 @@ export function projectBrowserSubmission( request: SubmissionRequest, workspaceFiles: WorkspaceFile[], ): SubmissionRequest { - const publicFiles = workspaceFiles.filter( - (file) => file.language === request.language && file.visibility !== "hidden", - ); - if (publicFiles.length === 0) return request; + const languageFiles = workspaceFiles.filter((file) => file.language === request.language); + if (languageFiles.length === 0) return request; const sources = request.sourceFiles?.length ? request.sourceFiles : [{ path: entryFileNameFor(request.language), content: request.sourceCode }]; - return { ...request, sourceFiles: mergeWorkspaceSources(sources, publicFiles) }; + return { ...request, sourceFiles: mergeWorkspaceSources(sources, languageFiles) }; } diff --git a/apps/web/src/lib/components/features/problem/editors/use-editor-run.svelte.ts b/apps/web/src/lib/components/features/problem/editors/use-editor-run.svelte.ts index eb4c43b32..db0644b38 100644 --- a/apps/web/src/lib/components/features/problem/editors/use-editor-run.svelte.ts +++ b/apps/web/src/lib/components/features/problem/editors/use-editor-run.svelte.ts @@ -1,18 +1,14 @@ -import { - MAX_CASE_STDOUT_BYTES, - interactiveContestantSupported, - serialiseBuildArtifact, - type JudgeConfig, - type JudgeType, - type Language, - type SubmissionContext, - type SubmissionResult, - type SubmissionRunCase, - type TestJudgeCaseResult, +import type { + JudgeConfig, + JudgeType, + Language, + SubmissionContext, + SubmissionResult, + SubmissionRunCase, } from "@nojv/core"; +import type { BuildArtifact } from "@wasm-oj/browser"; import { m } from "$lib/paraglide/messages.js"; import { - requestTestJudge, submissionRequestValidationError, SubmissionRequestError, type SubmissionRequest, @@ -27,11 +23,12 @@ import { compileBrowserLocally, preloadBrowserToolchain, runBrowserCases, + runBrowserChecker, + runBrowserInteraction, runBrowserLocally, supportsBrowserLocalRun, - type BrowserCaseRun, - type BrowserCompileOutcome, } from "$lib/services/browser-local-run"; +import type { PreparedJudgeProgram } from "$lib/services/judge-program"; import type { ProblemDetail, TestCaseView, TestRunResult } from "$lib/types"; import { buildSubmissionRequest, @@ -55,6 +52,7 @@ interface EditorRunArgs { workspaceDrafts: () => Record; workspaceFiles: () => WorkspaceFile[]; context: () => SubmissionContext; + judgeProgram: () => Promise; onSubmissionDispatched?: ((submissionId: string, language: string) => void) | undefined; onSubmissionComplete?: | (( @@ -74,8 +72,6 @@ export interface EditorRunController { readonly runSource: "local" | null; readonly runStatus: string | null; readonly runError: string | null; - readonly testDisabledReason: string | null; - readonly customCasesAllowed: boolean; readonly cooldownUntil: number | null; panelRunCases: SubmissionRunCase[]; setBottomTab: (tab: "testcase" | "result") => void; @@ -90,21 +86,6 @@ function messageForSubmitError(code: string | null): string { return m.editor_clientTestCustomImage(); case "client_test_language": return m.editor_clientTestLanguage(); - case "client_test_interactive_language": - return m.editor_testInteractiveLanguage(); - case "client_test_no_interactive_samples": - return m.editor_testNoInteractiveSamples(); - case "test_judge_busy": - return m.editor_testJudgeBusy(); - case "test_request_too_large": - return m.editor_testTooLarge(); - case "judge_program_build_failed": - return m.editor_testJudgeProgramBuildFailed(); - case "test_judge_unavailable": - case "judge_program_unsupported": - return m.editor_testUnavailableForProblem(); - case "test_rejected": - return m.editor_runFailed(); case "browser_toolchain_unavailable": return m.editor_toolchainUnavailable(); case "invalid_source": @@ -128,17 +109,6 @@ function messageForSubmitError(code: string | null): string { } } -const TEST_DISABLING_CODES = new Set([ - "judge_program_build_failed", - "judge_program_unsupported", -]); - -const UNJUDGED_SAMPLE_CODES = new Set(["test_judge_busy", "test_judge_unavailable"]); - -function interactiveSampleIndices(samples: ProblemDetail["samples"]): number[] { - return samples.flatMap((sample, index) => (sample.interactorInput?.trim() ? [index] : [])); -} - function initialRunCases( samples: ProblemDetail["samples"], judgeType: JudgeType, @@ -159,7 +129,6 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl let runSource = $state<"local" | null>(null); let runStatus = $state(null); let runError = $state(null); - let testDisabledReason = $state(null); let cooldownUntil = $state(null); let panelRunCases = $state( initialRunCases(args.initialSamples, args.judgeType()), @@ -168,122 +137,84 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl let destroyed = false; let abortController: AbortController | null = null; + function runLimits(language: Language) { + return { + language, + timeLimitMs: args.timeLimitMs, + memoryLimitMb: args.memoryLimitMb, + env: args.judgeConfig().runtime?.env ?? {}, + }; + } + async function runCheckerTest( request: SubmissionRequest, runCases: SubmissionRunCase[], + checker: BuildArtifact, signal: AbortSignal, ): Promise { - let runs: BrowserCaseRun[]; try { const build = await compileBrowserLocally(request, args.problemId, signal); if (!build.ok) return build.result; - runs = await runBrowserCases( + const runs = await runBrowserCases( build.artifact, runCases, - { - language: request.language, - timeLimitMs: args.timeLimitMs, - memoryLimitMb: args.memoryLimitMb, - env: args.judgeConfig().runtime?.env ?? {}, - }, + runLimits(request.language), signal, ); - } catch (error) { - return signal.aborted ? null : browserLocalErrorResult(error); - } - const judgedCases = new Map(); - for (const [index, runCase] of runCases.entries()) { - const sampleIndex = args.initialSamples.findIndex( - (sample) => sample.input === runCase.input, - ); - if (sampleIndex >= 0 && runs[index]?.verdict === "AC" && !judgedCases.has(sampleIndex)) { - judgedCases.set(sampleIndex, index); - } - } - const judgements = new Map(); - let serverNotice: string | undefined; - if (judgedCases.size > 0) { - runStatus = m.editor_judgingOnServer(); - try { - const response = await requestTestJudge( - args.problemId, + const caseResults: TestCaseView[] = []; + for (const [index, run] of runs.entries()) { + const view = browserCaseResult(run, undefined, undefined, index); + const sample = + run.verdict === "AC" + ? args.initialSamples.find( + (candidate) => candidate.input === runCases[index]?.input, + ) + : undefined; + if (!sample) { + caseResults.push(view.verdict === "AC" ? { ...view, executionOnly: true } : view); + continue; + } + const judgement = await runBrowserChecker( + checker, { - kind: "checker", - context: args.context(), - cases: [...judgedCases].map(([sampleIndex, index]) => ({ - sampleIndex, - output: (runs[index]?.stdout ?? "").slice(0, MAX_CASE_STDOUT_BYTES), - })), + input: sample.input, + answer: sample.output, + output: run.stdout, + timeLimitMs: args.timeLimitMs, }, signal, ); - if (!response) return null; - for (const [position, index] of [...judgedCases.values()].entries()) { - const judgement = response.cases[position]; - if (judgement) judgements.set(index, judgement); - } - } catch (error) { - if ( - !(error instanceof SubmissionRequestError) || - !UNJUDGED_SAMPLE_CODES.has(error.code ?? "") - ) - throw error; - serverNotice = messageForSubmitError(error.code); + caseResults.push({ ...view, ...judgement, judged: true }); } + return { ...browserLocalSubmissionResult(caseResults), caseResults }; + } catch (error) { + return signal.aborted ? null : browserLocalErrorResult(error); } - const caseResults = runs.map((run, index): TestCaseView => { - const view = browserCaseResult(run, undefined, undefined, index); - const judgement = judgements.get(index); - if (!judgement) return view.verdict === "AC" ? { ...view, executionOnly: true } : view; - return { - ...view, - verdict: judgement.verdict, - serverJudged: true, - ...(judgement.teamMessage ? { teamMessage: judgement.teamMessage } : {}), - }; - }); - return { - ...browserLocalSubmissionResult(caseResults), - caseResults, - ...(serverNotice ? { serverNotice } : {}), - }; } async function runInteractiveTest( request: SubmissionRequest, - sampleIndices: number[], + runCases: SubmissionRunCase[], + interactor: BuildArtifact, signal: AbortSignal, ): Promise { - let build: BrowserCompileOutcome; try { - build = await compileBrowserLocally(request, args.problemId, signal); + const build = await compileBrowserLocally(request, args.problemId, signal); + if (!build.ok) return build.result; + const caseResults: TestCaseView[] = []; + for (const [index, testCase] of runCases.entries()) { + const interaction = await runBrowserInteraction( + build.artifact, + interactor, + { interactorInput: testCase.input, limits: runLimits(request.language) }, + signal, + ); + caseResults.push({ index, ...interaction, judged: true }); + } + return { ...browserLocalSubmissionResult(caseResults), caseResults }; } catch (error) { return signal.aborted ? null : browserLocalErrorResult(error); } - if (!build.ok) return build.result; - runStatus = m.editor_judgingOnServer(); - const response = await requestTestJudge( - args.problemId, - { - kind: "interactive", - context: args.context(), - language: request.language, - artifact: serialiseBuildArtifact(build.artifact), - cases: sampleIndices.map((sampleIndex) => ({ sampleIndex })), - }, - signal, - ); - if (!response) return null; - const caseResults = response.cases.map((judgement, index): TestCaseView => ({ - index, - verdict: judgement.verdict, - serverJudged: true, - timeMs: judgement.timeMs ?? 0, - ...(judgement.contestantStderr ? { stderr: judgement.contestantStderr } : {}), - ...(judgement.teamMessage ? { teamMessage: judgement.teamMessage } : {}), - ...(judgement.transcript ? { transcript: judgement.transcript } : {}), - })); - return { ...browserLocalSubmissionResult(caseResults), caseResults }; } async function runSubmission(): Promise { @@ -293,28 +224,13 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl "client_test_custom_image", null, ); - const judgeType = args.judgeType(); const language = args.language(); - const interactive = judgeType === "interactive"; - if (interactive && !interactiveContestantSupported(language)) - throw new SubmissionRequestError( - "Interactive Test does not support this language.", - "client_test_interactive_language", - null, - ); - const sampleIndices = interactive ? interactiveSampleIndices(args.initialSamples) : []; - if (interactive && sampleIndices.length === 0) - throw new SubmissionRequestError( - "No sample has an interactor input.", - "client_test_no_interactive_samples", - null, - ); abortController = new AbortController(); const { signal } = abortController; - const runCases = interactive ? [] : projectRunCasesForRequest(panelRunCases); - if (!interactive && runCases.length === 0) + const runCases = projectRunCasesForRequest(panelRunCases); + if (runCases.length === 0) throw new SubmissionRequestError("No testcases provided.", "invalid_run_cases", null); const request = buildSubmissionRequest({ @@ -326,7 +242,7 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl sampleOnly: true, workspaceDrafts: args.workspaceDrafts(), workspaceFiles: args.workspaceFiles(), - ...(interactive ? {} : { runCases }), + runCases, }); const validationError = submissionRequestValidationError(request); @@ -356,32 +272,39 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl ); } if (signal.aborted) return null; - runStatus = m.editor_running(); const browserRequest = projectBrowserSubmission(request, args.workspaceFiles()); - let result: TestRunResult | null; - if (judgeType === "checker") { - result = await runCheckerTest(browserRequest, runCases, signal); - } else if (interactive) { - result = await runInteractiveTest(browserRequest, sampleIndices, signal); - } else { - const local = await runBrowserLocally({ - request: browserRequest, - cases: runCases, - judgeConfig: args.judgeConfig(), - problemId: args.problemId, - timeLimitMs: args.timeLimitMs, - memoryLimitMb: args.memoryLimitMb, - signal, - }); - result = local && { - ...local, - caseResults: local.caseResults?.map((view, index): TestCaseView => - view.verdict === "AC" && runCases[index]?.expectedOutput === undefined - ? { ...view, executionOnly: true } - : view, - ), - }; + if (args.judgeType() !== "standard") { + runStatus = + args.judgeType() === "interactive" + ? m.editor_interactorPreparing() + : m.editor_checkerPreparing(); + const judgeProgram = await args.judgeProgram(); + if (!judgeProgram.ok) return null; + runStatus = m.editor_running(); + const judged = + args.judgeType() === "interactive" + ? await runInteractiveTest(browserRequest, runCases, judgeProgram.artifact, signal) + : await runCheckerTest(browserRequest, runCases, judgeProgram.artifact, signal); + return destroyed ? null : judged; } + runStatus = m.editor_running(); + const local = await runBrowserLocally({ + request: browserRequest, + cases: runCases, + judgeConfig: args.judgeConfig(), + problemId: args.problemId, + timeLimitMs: args.timeLimitMs, + memoryLimitMb: args.memoryLimitMb, + signal, + }); + const result = local && { + ...local, + caseResults: local.caseResults?.map((view, index): TestCaseView => + view.verdict === "AC" && runCases[index]?.expectedOutput === undefined + ? { ...view, executionOnly: true } + : view, + ), + }; return destroyed ? null : result; } @@ -402,9 +325,6 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl err instanceof SubmissionRequestError ? messageForSubmitError(err.code) : m.editor_runFailed(); - if (err instanceof SubmissionRequestError && TEST_DISABLING_CODES.has(err.code ?? "")) { - testDisabledReason = message; - } runError = message; toasts.error(message); runStatus = null; @@ -496,12 +416,6 @@ export function createEditorRunController(args: EditorRunArgs): EditorRunControl get runError() { return runError; }, - get testDisabledReason() { - return testDisabledReason; - }, - get customCasesAllowed() { - return args.judgeType() !== "interactive"; - }, get cooldownUntil() { return cooldownUntil; }, diff --git a/apps/web/src/lib/components/features/problem/reference/ReferenceSolutionSection.svelte b/apps/web/src/lib/components/features/problem/reference/ReferenceSolutionSection.svelte index 89bd2e437..69d884a29 100644 --- a/apps/web/src/lib/components/features/problem/reference/ReferenceSolutionSection.svelte +++ b/apps/web/src/lib/components/features/problem/reference/ReferenceSolutionSection.svelte @@ -152,9 +152,7 @@ const workspaceLanguages = $derived([ ...new Set(workspaceFiles.map((file) => file.language)), ]); - const visibleFiles = $derived( - workspaceFiles.filter((file) => file.language === language && file.visibility !== "hidden"), - ); + const visibleFiles = $derived(workspaceFiles.filter((file) => file.language === language)); const selectedFile = $derived(visibleFiles[selectedIndex]); const failedSubtasks = $derived( (lastResult?.subtaskResults ?? []) diff --git a/apps/web/src/lib/components/features/problem/tabs/JudgeTab.svelte b/apps/web/src/lib/components/features/problem/tabs/JudgeTab.svelte index e5cdf8778..d66f584c9 100644 --- a/apps/web/src/lib/components/features/problem/tabs/JudgeTab.svelte +++ b/apps/web/src/lib/components/features/problem/tabs/JudgeTab.svelte @@ -2,13 +2,11 @@ import { untrack } from "svelte"; import { invalidateAll } from "$app/navigation"; import type { ProblemDetail } from "$lib/types"; - import type { testJudgeDomain } from "@nojv/application"; import type { JudgeScriptLanguage, JudgeType } from "@nojv/core"; import { inputClassName } from "$lib/utils/css"; import { m } from "$lib/paraglide/messages.js"; import MonacoScriptEditor from "$lib/components/primitives/ui/MonacoScriptEditor.svelte"; import UploadDropZone from "$lib/components/features/problem/admin/UploadDropZone.svelte"; - import JudgeProgramTestStatus from "./judge/JudgeProgramTestStatus.svelte"; import { toasts } from "$lib/stores/toast"; import { submitFormAction } from "$lib/utils/actions"; import { @@ -21,11 +19,10 @@ interface Props { problem: ProblemDetail; validatorScripts: { checkerScript: string; interactorScript: string }; - judgeProgramStatus: testJudgeDomain.JudgeProgramStatus | null; ondirtychange?: (dirty: boolean) => void; } - let { problem, validatorScripts, judgeProgramStatus, ondirtychange }: Props = $props(); + let { problem, validatorScripts, ondirtychange }: Props = $props(); const cfg = untrack(() => problem.judgeConfig ?? {}); @@ -69,10 +66,9 @@ let initialConfig = $state(dirtySnapshot()); let saving = $state(false); let saveMessage = $state(""); - let dirty = $derived(dirtySnapshot() !== initialConfig); $effect(() => { - ondirtychange?.(dirty); + ondirtychange?.(dirtySnapshot() !== initialConfig); }); export function save() { @@ -235,6 +231,8 @@ +

{m.admin_judgeProgramReadableNote()}

+
{m.admin_checkerHelpTitle()} @@ -278,6 +276,8 @@ +

{m.admin_judgeProgramReadableNote()}

+
{m.admin_interactorHelpTitle()} @@ -306,20 +306,6 @@ />
{/if} - - {#if judgeProgramStatus && judgeType !== "standard" && judgeType === problem.judgeType} -
- -
- {/if} {#if saving || saveMessage} diff --git a/apps/web/src/lib/components/features/problem/tabs/judge/JudgeProgramTestStatus.svelte b/apps/web/src/lib/components/features/problem/tabs/judge/JudgeProgramTestStatus.svelte deleted file mode 100644 index 0dd95f127..000000000 --- a/apps/web/src/lib/components/features/problem/tabs/judge/JudgeProgramTestStatus.svelte +++ /dev/null @@ -1,169 +0,0 @@ - - -{#if status.status === "not_applicable"} - {#if testJudgeDisabled} -

{m.admin_testJudgeDisabled()}

- {:else if pythonInteractor} -

- {m.admin_testPythonInteractorUnsupported()} -

- {/if} -{:else if status.status === "unavailable"} -

- {m.admin_judgeProgramTestUnavailable()} -

-{:else} -
-
- {status.status === "ok" - ? m.admin_judgeProgramTestReady() - : status.status === "failed" - ? m.admin_judgeProgramTestFailed() - : m.admin_judgeProgramTestPending()} -
- - {#if status.status === "failed"} -
- - {m.admin_judgeProgramDiagnostics()} - -
{status.diagnostics}
-
- {:else if checksSamples} -
- - {#if hasUnsavedChanges} - {m.admin_checkSamplesSaveFirst()} - {/if} -
- - {#if checkError} - - {/if} - - {#if results} -
    - {#each results as result (result.sampleIndex)} - {@const accepted = result.verdict === "AC"} -
  • -
    - - {m.admin_sampleNumber({ number: result.sampleIndex + 1 })} - -
    - {#if !accepted && result.teamMessage} -
    {result.teamMessage}
    - {/if} -
  • - {/each} -
- {#if anyRejected} -

{m.admin_checkSamplesRejected()}

- {/if} - {/if} - {/if} -
-{/if} diff --git a/apps/web/src/lib/components/features/problem/tabs/judge/script-examples.ts b/apps/web/src/lib/components/features/problem/tabs/judge/script-examples.ts index 43be2123d..de18a457f 100644 --- a/apps/web/src/lib/components/features/problem/tabs/judge/script-examples.ts +++ b/apps/web/src/lib/components/features/problem/tabs/judge/script-examples.ts @@ -1,5 +1,5 @@ export const PYTHON_CHECKER_EXAMPLE = `# Bound: judge_input, judge_answer, team_output (strings) -# Helpers: accept(team_msg=""), wrong(team_msg=""), set_score(x), judge_log(msg) +# Helpers: accept(team_msg=""), wrong(team_msg=""), judge_log(msg) u = team_output.split() e = judge_answer.split() @@ -12,13 +12,12 @@ accept() `; export const PYTHON_INTERACTOR_EXAMPLE = `# Bound: judge_input, judge_answer (strings); read(), write(msg) -# Helpers: accept(team_msg=""), wrong(team_msg=""), set_score(x), judge_log(msg) +# Helpers: accept(team_msg=""), wrong(team_msg=""), judge_log(msg) secret = int(judge_input.strip()) for i in range(1, 21): g = int(read()) if g == secret: - set_score(max(0, 100 - (i - 1) * 5)) accept(f"correct in {i} guesses") write("higher" if secret > g else "lower") wrong("exceeded 20 guesses") @@ -62,8 +61,7 @@ int main(int argc, char* argv[]) { std::ifstream in(argv[1]); std::string feedback_dir = argv[3]; - auto finish = [&](int code, int score, const std::string& msg) { - std::ofstream(feedback_dir + "/score.txt") << score; + auto finish = [&](int code, const std::string& msg) { std::ofstream(feedback_dir + "/teammessage.txt") << msg; std::exit(code); }; @@ -72,12 +70,11 @@ int main(int argc, char* argv[]) { in >> secret; for (int guess_count = 1; guess_count <= 20; guess_count++) { int g; - if (!(std::cin >> g)) finish(43, 0, "solution closed its output early"); + if (!(std::cin >> g)) finish(43, "solution closed its output early"); if (g == secret) - finish(42, std::max(0, 100 - (guess_count - 1) * 5), - "correct in " + std::to_string(guess_count) + " guesses"); + finish(42, "correct in " + std::to_string(guess_count) + " guesses"); std::cout << (secret > g ? "higher" : "lower") << std::endl; } - finish(43, 0, "exceeded 20 guesses"); + finish(43, "exceeded 20 guesses"); } `; diff --git a/apps/web/src/lib/components/features/problem/workspace/WorkspaceFileEditor.svelte b/apps/web/src/lib/components/features/problem/workspace/WorkspaceFileEditor.svelte index 5cd4680b0..eb9ae5bb0 100644 --- a/apps/web/src/lib/components/features/problem/workspace/WorkspaceFileEditor.svelte +++ b/apps/web/src/lib/components/features/problem/workspace/WorkspaceFileEditor.svelte @@ -9,7 +9,7 @@ path: string; content: string; description: string; - visibility: "editable" | "readonly" | "hidden"; + visibility: "editable" | "readonly"; orderIndex: number; } @@ -51,7 +51,6 @@ > -