@exercode/problem-utils 2.4.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/checkAllProblems.cjs +1 -1
- package/dist/cli/checkAllProblems.cjs.map +1 -1
- package/dist/cli/checkAllProblems.js +1 -1
- package/dist/cli/checkAllProblems.js.map +1 -1
- package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
- package/dist/helpers/checkProblemDirIsolation.js +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
- package/dist/helpers/cleanWorkingDirectory.d.ts +2 -6
- package/dist/helpers/cleanWorkingDirectory.js +1 -1
- package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.js +1 -1
- package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.js +1 -1
- package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
- package/dist/helpers/readOutputFiles.cjs +1 -1
- package/dist/helpers/readOutputFiles.cjs.map +1 -1
- package/dist/helpers/readOutputFiles.d.ts +0 -9
- package/dist/helpers/readOutputFiles.js +1 -1
- package/dist/helpers/readOutputFiles.js.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.d.ts +0 -3
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
- package/dist/helpers/spawnSyncWithTimeout.cjs +1 -1
- package/dist/helpers/spawnSyncWithTimeout.cjs.map +1 -1
- package/dist/helpers/spawnSyncWithTimeout.d.ts +0 -7
- package/dist/helpers/spawnSyncWithTimeout.js +1 -1
- package/dist/helpers/spawnSyncWithTimeout.js.map +1 -1
- package/dist/helpers/startHttpServer.cjs +1 -1
- package/dist/helpers/startHttpServer.cjs.map +1 -1
- package/dist/helpers/startHttpServer.js +1 -1
- package/dist/helpers/startHttpServer.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -1
- package/dist/presets/command.cjs +1 -1
- package/dist/presets/command.cjs.map +1 -1
- package/dist/presets/command.d.ts +0 -16
- package/dist/presets/command.js +1 -1
- package/dist/presets/command.js.map +1 -1
- package/dist/presets/evaluation.cjs +4 -4
- package/dist/presets/evaluation.cjs.map +1 -1
- package/dist/presets/evaluation.js +4 -4
- package/dist/presets/evaluation.js.map +1 -1
- package/dist/presets/guiCommand.cjs +3 -3
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.d.ts +0 -24
- package/dist/presets/guiCommand.js +3 -3
- package/dist/presets/guiCommand.js.map +1 -1
- package/dist/presets/stdio.cjs +1 -1
- package/dist/presets/stdio.cjs.map +1 -1
- package/dist/presets/stdio.js +1 -1
- package/dist/presets/stdio.js.map +1 -1
- package/dist/types/language.cjs +2 -2
- package/dist/types/language.cjs.map +1 -1
- package/dist/types/language.js +2 -2
- package/dist/types/language.js.map +1 -1
- package/package.json +4 -4
- package/dist/helpers/runCustomRunner.cjs +0 -2
- package/dist/helpers/runCustomRunner.cjs.map +0 -1
- package/dist/helpers/runCustomRunner.d.ts +0 -8
- package/dist/helpers/runCustomRunner.js +0 -2
- package/dist/helpers/runCustomRunner.js.map +0 -1
- package/dist/helpers/safeFs.cjs +0 -2
- package/dist/helpers/safeFs.cjs.map +0 -1
- package/dist/helpers/safeFs.d.ts +0 -29
- package/dist/helpers/safeFs.js +0 -2
- package/dist/helpers/safeFs.js.map +0 -1
- package/dist/helpers/sandboxUser.cjs +0 -3
- package/dist/helpers/sandboxUser.cjs.map +0 -1
- package/dist/helpers/sandboxUser.d.ts +0 -146
- package/dist/helpers/sandboxUser.js +0 -3
- package/dist/helpers/sandboxUser.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../helpers/defaultStdioHarness.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../helpers/defaultStdioHarness.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../helpers/temporaryProblemDirCopy.cjs"),a=require("../types/decisionCode.cjs"),o=require("../helpers/runHarnessProcess.cjs"),s=require("../helpers/readProblemMarkdownFrontMatter.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("./runSingleHarness.cjs");let u=require("node:path");u=e.__toESM(u,1);let d=require("node:fs/promises");d=e.__toESM(d,1);const f=1e3,p=new Map(Object.entries(a.DecisionCode).map(([e,t])=>[t,e]));async function m(e){let t=_(e),i=u.default.resolve(t.rootDir),a=e=>(u.default.relative(i,e)||`.`).replaceAll(u.default.sep,`/`),o=await v(i),d=o.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(d.length===0)return console.error(o.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${o.length} problem directories under ${i} were excluded by --only/--skip.`),1;let f=[],p=[];for(let e of d){let t=await n.findDefaultStdioHarnessFiles(e);if(t.length>0){f.push(`${a(e)}: ${l.formatDefaultHarnessError(t)}`);continue}let i=await c.findModelAnswerDirs(e);if(i.length===0){f.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await b(u.default.join(e,`judge.ts`))&&(await r.readTestCases(u.default.join(e,`test_cases`))).length===0){let t;try{t=await s.readProblemMarkdownFrontMatter(e)}catch(t){f.push(`${a(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!s.judgesWithoutTestCases(t)){f.push(`${a(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let o=await c.findFailingModelAnswerDirs(e);p.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...o.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of f)console.error(`✗ ${e}`);let m=u.default.resolve(process.argv[1]??``),g=0,y=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t.concurrency,p.length))},async()=>{for(;y<p.length;){let e=p[y++];if(!e)return;let t=`${a(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await h(e,m);n===void 0?(g++,console.info(`✓ ${t}`)):(f.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${g} passed, ${f.length} failed (${p.length} runs, ${d.length} problems)`),f.length===0?0:1}async function h(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return x(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let a,s;try{a=g(e,await o.runHarnessProcess([`run`,t,`judge`,u.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){a=x(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{s=await i.forciblyRemoveDirectory(n)}if(s)return a;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?c:x(`${a}; ${c}`)}function g(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return x(n.failureReason);if(n.exitCode!==0)return x(`harness exited with ${n.exitCode??`a signal`}${i.trim()?`: ${i.trim()}`:``}`);let o=r.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of o){let n;try{n=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)))}catch{n=void 0}if(!n?.success)return x(`malformed test case result line: ${e}`);s.push(n.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==a.DecisionCode.ACCEPTED);return e?x(`${p.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===a.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function _(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function v(e){let t=[];return await y(e,t),t.toSorted()}async function y(e,t){let n=await d.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await y(u.default.join(e,r.name),t)}async function b(e){try{return await d.default.stat(e),!0}catch{return!1}}function x(e){return e.length<=f?e:`${e.slice(0,f)}...`}exports.checkAllProblems=m;
|
|
2
2
|
//# sourceMappingURL=checkAllProblems.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","fs"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n for (const problemDir of problemDirs) {\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n failures.push(`${toRelative(problemDir)}: ${formatDefaultHarnessError(defaultHarnessFileNames)}`);\n continue;\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n failures.push(`${toRelative(problemDir)}: no model answers found under model_answers/`);\n continue;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n let frontMatter;\n try {\n frontMatter = await readProblemMarkdownFrontMatter(problemDir);\n } catch (error) {\n failures.push(\n `${toRelative(problemDir)}: failed to read the problem markdown front matter: ${error instanceof Error ? error.message : String(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n }\n })\n );\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: createHarnessEnv(),\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\n// The all-problem check runs harnesses concurrently and has no sandbox-delegation contract, while the sandbox\n// helpers assume one harness at a time (their pkill targets every process of the sandbox user), so\n// never forward the sandbox user to judged runs.\nfunction createHarnessEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.EXERCODE_SANDBOX_USER;\n return env;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","fs"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n for (const problemDir of problemDirs) {\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n failures.push(`${toRelative(problemDir)}: ${formatDefaultHarnessError(defaultHarnessFileNames)}`);\n continue;\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n failures.push(`${toRelative(problemDir)}: no model answers found under model_answers/`);\n continue;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n let frontMatter;\n try {\n frontMatter = await readProblemMarkdownFrontMatter(problemDir);\n } catch (error) {\n failures.push(\n `${toRelative(problemDir)}: failed to read the problem markdown front matter: ${error instanceof Error ? error.message : String(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n }\n })\n );\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{findDefaultStdioHarnessFiles as n}from"../helpers/defaultStdioHarness.js";import{readTestCases as r}from"../helpers/readTestCases.js";import{
|
|
1
|
+
import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{findDefaultStdioHarnessFiles as n}from"../helpers/defaultStdioHarness.js";import{readTestCases as r}from"../helpers/readTestCases.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"../helpers/temporaryProblemDirCopy.js";import{DecisionCode as o}from"../types/decisionCode.js";import{runHarnessProcess as s}from"../helpers/runHarnessProcess.js";import{judgesWithoutTestCases as c,readProblemMarkdownFrontMatter as l}from"../helpers/readProblemMarkdownFrontMatter.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:path";import h from"node:fs/promises";const g=1e3,_=new Map(Object.entries(o).map(([e,t])=>[t,e]));async function v(e){let t=x(e),i=m.resolve(t.rootDir),a=e=>(m.relative(i,e)||`.`).replaceAll(m.sep,`/`),o=await S(i),s=o.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(s.length===0)return console.error(o.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${o.length} problem directories under ${i} were excluded by --only/--skip.`),1;let h=[],g=[];for(let e of s){let t=await n(e);if(t.length>0){h.push(`${a(e)}: ${p(t)}`);continue}let i=await d(e);if(i.length===0){h.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await w(m.join(e,`judge.ts`))&&(await r(m.join(e,`test_cases`))).length===0){let t;try{t=await l(e)}catch(t){h.push(`${a(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!c(t)){h.push(`${a(e)}: ${f}`);continue}}let o=await u(e);g.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...o.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of h)console.error(`✗ ${e}`);let _=m.resolve(process.argv[1]??``),v=0,b=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t.concurrency,g.length))},async()=>{for(;b<g.length;){let e=g[b++];if(!e)return;let t=`${a(e.problemDir)} ${m.relative(e.problemDir,e.answerDir).replaceAll(m.sep,`/`)}`,n=await y(e,_);n===void 0?(v++,console.info(`✓ ${t}`)):(h.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${v} passed, ${h.length} failed (${g.length} runs, ${s.length} problems)`),h.length===0?0:1}async function y(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return T(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let o,c;try{o=b(e,await s([`run`,t,`judge`,m.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=T(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{c=await a(n)}if(c)return o;let l=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return o===void 0?l:T(`${o}; ${l}`)}function b(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return T(r.failureReason);if(r.exitCode!==0)return T(`harness exited with ${r.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let s=i.split(/\r?\n/).filter(t=>t.startsWith(e)),c=[];for(let n of s){let r;try{r=t.safeParse(JSON.parse(n.slice(e.length)))}catch{r=void 0}if(!r?.success)return T(`malformed test case result line: ${n}`);c.push(r.data)}if(c.length===0)return`no test case results were printed`;if(n.expectation===`accepted`){let e=c.find(e=>e.decisionCode!==o.ACCEPTED);return e?T(`${_.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return c.every(e=>e.decisionCode===o.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function x(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function S(e){let t=[];return await C(e,t),t.toSorted()}async function C(e,t){let n=await h.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await C(m.join(e,r.name),t)}async function w(e){try{return await h.stat(e),!0}catch{return!1}}function T(e){return e.length<=g?e:`${e.slice(0,g)}...`}export{v as checkAllProblems};
|
|
2
2
|
//# sourceMappingURL=checkAllProblems.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkAllProblems.js","names":[],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n for (const problemDir of problemDirs) {\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n failures.push(`${toRelative(problemDir)}: ${formatDefaultHarnessError(defaultHarnessFileNames)}`);\n continue;\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n failures.push(`${toRelative(problemDir)}: no model answers found under model_answers/`);\n continue;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n let frontMatter;\n try {\n frontMatter = await readProblemMarkdownFrontMatter(problemDir);\n } catch (error) {\n failures.push(\n `${toRelative(problemDir)}: failed to read the problem markdown front matter: ${error instanceof Error ? error.message : String(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n }\n })\n );\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: createHarnessEnv(),\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\n// The all-problem check runs harnesses concurrently and has no sandbox-delegation contract, while the sandbox\n// helpers assume one harness at a time (their pkill targets every process of the sandbox user), so\n// never forward the sandbox user to judged runs.\nfunction createHarnessEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.EXERCODE_SANDBOX_USER;\n return env;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"checkAllProblems.js","names":[],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n for (const problemDir of problemDirs) {\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n failures.push(`${toRelative(problemDir)}: ${formatDefaultHarnessError(defaultHarnessFileNames)}`);\n continue;\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n failures.push(`${toRelative(problemDir)}: no model answers found under model_answers/`);\n continue;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n let frontMatter;\n try {\n frontMatter = await readProblemMarkdownFrontMatter(problemDir);\n } catch (error) {\n failures.push(\n `${toRelative(problemDir)}: failed to read the problem markdown front matter: ${error instanceof Error ? error.message : String(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n }\n })\n );\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("./temporaryProblemDirCopy.cjs"),r=require("../types/decisionCode.cjs"),i=require("./printDebugBanner.cjs"),a=require("./runHarnessProcess.cjs");let o=require("node:path");o=e.__toESM(o,1);const s=3e4,c=1e4;async function l(e,t,r,l={}){let h=Math.max(s,(l.expectedMaxDurationMs??0)+c),g;try{let s=await n.copyProblemDirToTemporaryRoot(e);g=s.tempRoot;let{copiedProblemDir:c}=s,_=o.default.resolve(e),v=o.default.relative(_,o.default.resolve(t.cwd)),y=o.default.join(c,v),b=p(_);if(b.startsWith(`..`)||o.default.isAbsolute(b))return i.printDebugBanner([`[DEBUG MODE] isolated problem directory check skipped`,``,`The invoked judge script is located outside the problem directory.`,`Script path: ${b}`]),{passed:!0};let x=process.execArgv.filter(d),S=JSON.stringify(f(r)?r:{}),C=await a.runHarnessProcess([...x,b,y,S],{cwd:c,env:process.env,timeoutMs:h,maxOutputBytes:67108864,tempRoot:g});return C.exitCode===0&&m(C.stdout)?(i.printDebugBanner([`[DEBUG MODE] isolated problem directory check passed`,``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`]),{passed:!0}):(i.printDebugBanner([`[DEBUG MODE] isolated problem directory check failed`,``,...u(C,h,l),``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`,`Exit status : ${C.exitCode??C.signal??`unknown`}`,`Failure reason : ${C.failureReason??`<none>`}`,``,`stdout:`,C.stdout.trimEnd()||`<empty>`,``,`stderr:`,C.stderr.trimEnd()||`<empty>`]),{passed:!1})}catch(e){return i.printDebugBanner([`[DEBUG MODE] isolated problem directory check failed due to an unexpected error`,``,e instanceof Error?e.message:String(e)]),{passed:!1}}finally{g&&await n.forciblyRemoveDirectory(g)}}function u(e,t,n){return e.timedOut?[`The judge did not finish within ${t/1e3} seconds after copying only the problem directory to a temporary location.`,...n.expectedMaxDurationMs===void 0?[`This judge declares no run time, so the check uses its fixed ${s/1e3}-second budget.`]:[`The budget is the larger of ${s/1e3} seconds and the build timeout plus the time limit of every test case plus a ${c/1e3}-second overhead.`,`Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.`]]:e.failureReason===void 0?e.exitCode===void 0?[`The judge was terminated by ${e.signal??`an unknown signal`} (e.g. an out-of-memory kill or a cancelled job).`]:[`The judge did not complete successfully after copying only the problem directory to a temporary location.`,`Make sure judge.ts imports only files included in the problem directory.`]:[`The judge was stopped before it finished; see the failure reason below.`]}function d(e){return!e.startsWith(`--inspect`)&&!e.startsWith(`--watch`)&&!e.startsWith(`--hot`)}function f(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function p(e){let t=process.argv[1];if(!t)return`.${o.default.sep}judge.ts`;let n=o.default.relative(e,o.default.resolve(t));return o.default.isAbsolute(n)||n.startsWith(`.`)?n:`.${o.default.sep}${n}`}function m(e){let n=e.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX));return n.length!==0&&n.every(e=>{try{let n=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)));return n.success&&n.data.decisionCode===r.DecisionCode.ACCEPTED}catch{return!1}})}exports.checkProblemDirIsolation=l;
|
|
2
2
|
//# sourceMappingURL=checkProblemDirIsolation.cjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{
|
|
1
|
+
import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{copyProblemDirToTemporaryRoot as n,forciblyRemoveDirectory as r}from"./temporaryProblemDirCopy.js";import{DecisionCode as i}from"../types/decisionCode.js";import{printDebugBanner as a}from"./printDebugBanner.js";import{runHarnessProcess as o}from"./runHarnessProcess.js";import s from"node:path";const c=3e4,l=1e4;async function u(e,t,i,u={}){let g=Math.max(c,(u.expectedMaxDurationMs??0)+l),_;try{let r=await n(e);_=r.tempRoot;let{copiedProblemDir:c}=r,l=s.resolve(e),v=s.relative(l,s.resolve(t.cwd)),y=s.join(c,v),b=m(l);if(b.startsWith(`..`)||s.isAbsolute(b))return a([`[DEBUG MODE] isolated problem directory check skipped`,``,`The invoked judge script is located outside the problem directory.`,`Script path: ${b}`]),{passed:!0};let x=process.execArgv.filter(f),S=JSON.stringify(p(i)?i:{}),C=await o([...x,b,y,S],{cwd:c,env:process.env,timeoutMs:g,maxOutputBytes:67108864,tempRoot:_});return C.exitCode===0&&h(C.stdout)?(a([`[DEBUG MODE] isolated problem directory check passed`,``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`]),{passed:!0}):(a([`[DEBUG MODE] isolated problem directory check failed`,``,...d(C,g,u),``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`,`Exit status : ${C.exitCode??C.signal??`unknown`}`,`Failure reason : ${C.failureReason??`<none>`}`,``,`stdout:`,C.stdout.trimEnd()||`<empty>`,``,`stderr:`,C.stderr.trimEnd()||`<empty>`]),{passed:!1})}catch(e){return a([`[DEBUG MODE] isolated problem directory check failed due to an unexpected error`,``,e instanceof Error?e.message:String(e)]),{passed:!1}}finally{_&&await r(_)}}function d(e,t,n){return e.timedOut?[`The judge did not finish within ${t/1e3} seconds after copying only the problem directory to a temporary location.`,...n.expectedMaxDurationMs===void 0?[`This judge declares no run time, so the check uses its fixed ${c/1e3}-second budget.`]:[`The budget is the larger of ${c/1e3} seconds and the build timeout plus the time limit of every test case plus a ${l/1e3}-second overhead.`,`Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.`]]:e.failureReason===void 0?e.exitCode===void 0?[`The judge was terminated by ${e.signal??`an unknown signal`} (e.g. an out-of-memory kill or a cancelled job).`]:[`The judge did not complete successfully after copying only the problem directory to a temporary location.`,`Make sure judge.ts imports only files included in the problem directory.`]:[`The judge was stopped before it finished; see the failure reason below.`]}function f(e){return!e.startsWith(`--inspect`)&&!e.startsWith(`--watch`)&&!e.startsWith(`--hot`)}function p(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function m(e){let t=process.argv[1];if(!t)return`.${s.sep}judge.ts`;let n=s.relative(e,s.resolve(t));return s.isAbsolute(n)||n.startsWith(`.`)?n:`.${s.sep}${n}`}function h(n){let r=n.split(/\r?\n/).filter(t=>t.startsWith(e));return r.length!==0&&r.every(n=>{try{let r=t.safeParse(JSON.parse(n.slice(e.length)));return r.success&&r.data.decisionCode===i.ACCEPTED}catch{return!1}})}export{u as checkProblemDirIsolation};
|
|
2
2
|
//# sourceMappingURL=checkProblemDirIsolation.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs")
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:fs/promises");n=e.__toESM(n,1);async function r(e){let t=await n.default.readdir(e,{recursive:!0});return new Set(t)}async function i(e,t){await a(e,``,t)}async function a(e,r,i){for(let o of await n.default.readdir(e,{withFileTypes:!0})){let s=t.default.join(r,o.name);i.has(s)?o.isDirectory()&&await a(t.default.join(e,o.name),s,i):await n.default.rm(t.default.join(e,o.name),{force:!0,recursive:!0})}}exports.cleanWorkingDirectory=i,exports.snapshotWorkingDirectory=r;
|
|
2
2
|
//# sourceMappingURL=cleanWorkingDirectory.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanWorkingDirectory.cjs","names":["
|
|
1
|
+
{"version":3,"file":"cleanWorkingDirectory.cjs","names":["fs","path"],"sources":["../../src/helpers/cleanWorkingDirectory.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\n// Currently, it does not support changing file contents and deleting files.\nexport async function snapshotWorkingDirectory(cwd: string): Promise<ReadonlySet<string>> {\n const paths = await fs.readdir(cwd, { recursive: true });\n return new Set(paths);\n}\n\nexport async function cleanWorkingDirectory(cwd: string, snapshot: ReadonlySet<string>): Promise<void> {\n await cleanDirectory(cwd, '', snapshot);\n}\n\nasync function cleanDirectory(dir: string, relativePrefix: string, snapshot: ReadonlySet<string>): Promise<void> {\n for (const entry of await fs.readdir(dir, { withFileTypes: true })) {\n const relativePath = path.join(relativePrefix, entry.name);\n if (!snapshot.has(relativePath)) {\n // An unsnapshotted directory is removed whole, so its contents are never visited.\n await fs.rm(path.join(dir, entry.name), { force: true, recursive: true });\n } else if (entry.isDirectory()) {\n await cleanDirectory(path.join(dir, entry.name), relativePath, snapshot);\n }\n }\n}\n"],"mappings":"iKAIA,eAAsB,EAAyB,EAA2C,CACxF,IAAM,EAAQ,MAAMA,EAAAA,QAAG,QAAQ,EAAK,CAAE,UAAW,EAAK,CAAC,EACvD,OAAO,IAAI,IAAI,CAAK,CACtB,CAEA,eAAsB,EAAsB,EAAa,EAA8C,CACrG,MAAM,EAAe,EAAK,GAAI,CAAQ,CACxC,CAEA,eAAe,EAAe,EAAa,EAAwB,EAA8C,CAC/G,IAAK,IAAM,KAAS,MAAMA,EAAAA,QAAG,QAAQ,EAAK,CAAE,cAAe,EAAK,CAAC,EAAG,CAClE,IAAM,EAAeC,EAAAA,QAAK,KAAK,EAAgB,EAAM,IAAI,EACpD,EAAS,IAAI,CAAY,EAGnB,EAAM,YAAY,GAC3B,MAAM,EAAeA,EAAAA,QAAK,KAAK,EAAK,EAAM,IAAI,EAAG,EAAc,CAAQ,EAFvE,MAAMD,EAAAA,QAAG,GAAGC,EAAAA,QAAK,KAAK,EAAK,EAAM,IAAI,EAAG,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,CAI5E,CACF"}
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
type WorkingDirectoryEntryKind = 'directory' | 'file' | 'symlink' | 'other';
|
|
4
|
-
export declare function snapshotWorkingDirectory(cwd: string): Promise<WorkingDirectorySnapshot>;
|
|
5
|
-
export declare function cleanWorkingDirectory(cwd: string, snapshot: WorkingDirectorySnapshot): Promise<void>;
|
|
6
|
-
export {};
|
|
1
|
+
export declare function snapshotWorkingDirectory(cwd: string): Promise<ReadonlySet<string>>;
|
|
2
|
+
export declare function cleanWorkingDirectory(cwd: string, snapshot: ReadonlySet<string>): Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"node:path";import t from"node:fs/promises";async function n(e){let n=await t.readdir(e,{recursive:!0});return new Set(n)}async function r(e,t){await i(e,``,t)}async function i(n,r,a){for(let o of await t.readdir(n,{withFileTypes:!0})){let s=e.join(r,o.name);a.has(s)?o.isDirectory()&&await i(e.join(n,o.name),s,a):await t.rm(e.join(n,o.name),{force:!0,recursive:!0})}}export{r as cleanWorkingDirectory,n as snapshotWorkingDirectory};
|
|
2
2
|
//# sourceMappingURL=cleanWorkingDirectory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanWorkingDirectory.js","names":[],"sources":["../../src/helpers/cleanWorkingDirectory.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\
|
|
1
|
+
{"version":3,"file":"cleanWorkingDirectory.js","names":[],"sources":["../../src/helpers/cleanWorkingDirectory.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\n// Currently, it does not support changing file contents and deleting files.\nexport async function snapshotWorkingDirectory(cwd: string): Promise<ReadonlySet<string>> {\n const paths = await fs.readdir(cwd, { recursive: true });\n return new Set(paths);\n}\n\nexport async function cleanWorkingDirectory(cwd: string, snapshot: ReadonlySet<string>): Promise<void> {\n await cleanDirectory(cwd, '', snapshot);\n}\n\nasync function cleanDirectory(dir: string, relativePrefix: string, snapshot: ReadonlySet<string>): Promise<void> {\n for (const entry of await fs.readdir(dir, { withFileTypes: true })) {\n const relativePath = path.join(relativePrefix, entry.name);\n if (!snapshot.has(relativePath)) {\n // An unsnapshotted directory is removed whole, so its contents are never visited.\n await fs.rm(path.join(dir, entry.name), { force: true, recursive: true });\n } else if (entry.isDirectory()) {\n await cleanDirectory(path.join(dir, entry.name), relativePath, snapshot);\n }\n }\n}\n"],"mappings":"yDAIA,eAAsB,EAAyB,EAA2C,CACxF,IAAM,EAAQ,MAAM,EAAG,QAAQ,EAAK,CAAE,UAAW,EAAK,CAAC,EACvD,OAAO,IAAI,IAAI,CAAK,CACtB,CAEA,eAAsB,EAAsB,EAAa,EAA8C,CACrG,MAAM,EAAe,EAAK,GAAI,CAAQ,CACxC,CAEA,eAAe,EAAe,EAAa,EAAwB,EAA8C,CAC/G,IAAK,IAAM,KAAS,MAAM,EAAG,QAAQ,EAAK,CAAE,cAAe,EAAK,CAAC,EAAG,CAClE,IAAM,EAAe,EAAK,KAAK,EAAgB,EAAM,IAAI,EACpD,EAAS,IAAI,CAAY,EAGnB,EAAM,YAAY,GAC3B,MAAM,EAAe,EAAK,KAAK,EAAK,EAAM,IAAI,EAAG,EAAc,CAAQ,EAFvE,MAAM,EAAG,GAAG,EAAK,KAAK,EAAK,EAAM,IAAI,EAAG,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,CAI5E,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./compareStdoutAsSpaceSeparatedTokens.cjs"),n=require("./printTestCaseResult.cjs")
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./compareStdoutAsSpaceSeparatedTokens.cjs"),n=require("./printTestCaseResult.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);const a=8388608;async function o(e,t){let o=[],s=[],c=(await r.default.promises.readdir(t,{withFileTypes:!0,recursive:!0})).filter(e=>e.isFile()).map(e=>({absolutePath:i.default.join(e.parentPath,e.name),relativePath:i.default.posix.join(...i.default.relative(t,e.parentPath).split(i.default.sep),e.name)})).toSorted((e,t)=>e.relativePath<t.relativePath?-1:+(e.relativePath>t.relativePath));for(let{absolutePath:t,relativePath:f}of c){if((await r.default.promises.stat(t)).size>8388608)throw Error(`expected output file ${f} exceeds ${a} bytes`);let c=await r.default.promises.readFile(t),p=await u(i.default.join(e,f));p&&d(c,p)||(o.push(f),c.length<=1048576&&s.push(n.encodeFileForTestCaseResult(l(f,`expected`),c)),p&&p.length<=1048576&&s.push(n.encodeFileForTestCaseResult(l(f,`received`),p)))}return{matches:o.length===0,mismatchedPaths:o,outputFiles:s}}async function s(e){let{stdout:n,expectedStdout:r,fileOutputPath:i,cwd:a}=e,s=[...e.outputFiles],l=r===void 0||t.compareStdoutAsSpaceSeparatedTokens(n,r);if(i!==void 0){let e=await o(a,i);e.matches||(l=!1,s=c(s,e))}return{matches:l,outputFiles:s}}function c(e,t){let n=new Set(t.outputFiles.map(e=>e.path)),r=new Set(t.mismatchedPaths.filter(e=>n.has(l(e,`received`))));return[...e.filter(e=>!r.has(e.path)&&!n.has(e.path)),...t.outputFiles]}function l(e,t){let{dir:n,name:r,ext:a}=i.default.posix.parse(e);return i.default.posix.join(n,`${r}_${t}${a}`)}async function u(e){try{let t=await r.default.promises.stat(e);return!t.isFile()||t.size>8388608?void 0:await r.default.promises.readFile(e)}catch{return}}function d(e,n){return p(e)?p(n)?t.compareStdoutAsSpaceSeparatedTokens(n.toString(`utf8`),e.toString(`utf8`)):!1:n.equals(e)}const f=new TextDecoder(`utf-8`,{fatal:!0});function p(e){if(e.includes(0))return!1;try{return f.decode(e),!0}catch{return!1}}exports.MAX_COMPARED_FILE_BYTES=a,exports.MAX_REPORTED_FILE_BYTES=1048576,exports.compareExpectedOutputFiles=o,exports.judgeAgainstExpectations=s,exports.mergeComparisonOutputFiles=c;
|
|
2
2
|
//# sourceMappingURL=compareExpectedOutputFiles.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compareExpectedOutputFiles.cjs","names":["fs","path","encodeFileForTestCaseResult","compareStdoutAsSpaceSeparatedTokens","isSafeSubmissionOutputPath"],"sources":["../../src/helpers/compareExpectedOutputFiles.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { compareStdoutAsSpaceSeparatedTokens } from './compareStdoutAsSpaceSeparatedTokens.js';\nimport { encodeFileForTestCaseResult } from './printTestCaseResult.js';\nimport { isSafeSubmissionOutputPath } from './readOutputFiles.js';\n\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\n\n// A submission controls the received files, so bound what the trusted harness reads and reports.\nexport const MAX_COMPARED_FILE_BYTES = 8 * 1024 * 1024;\nexport const MAX_REPORTED_FILE_BYTES = 1024 * 1024;\n\nexport interface ExpectedOutputFilesComparison {\n /** Whether every file under the expected directory has a matching file in the working directory. */\n matches: boolean;\n /** Relative paths (from the working directory) of the files that are missing or different. */\n mismatchedPaths: string[];\n /**\n * `<name>_expected.<ext>` / `<name>_received.<ext>` pairs for each mismatched file, in the\n * format Exercode renders side by side. A received file that is missing, unreadable, not a\n * regular file or larger than `MAX_COMPARED_FILE_BYTES` yields only the expected entry, and a\n * file larger than `MAX_REPORTED_FILE_BYTES` is left out of the pair. Exercode decides per test case whether a learner may see these files.\n */\n outputFiles: OutputFile[];\n}\n\n/**\n * Compare the files under a test case's `.fout` directory with the files the submission wrote to\n * its working directory. Text files are compared like stdout (space-separated tokens with a\n * numeric tolerance); binary files must match byte for byte.\n */\nexport async function compareExpectedOutputFiles(\n cwd: string,\n fileOutputPath: string\n): Promise<ExpectedOutputFilesComparison> {\n const mismatchedPaths: string[] = [];\n const outputFiles: OutputFile[] = [];\n const dirents = await fs.promises.readdir(fileOutputPath, { withFileTypes: true, recursive: true });\n const expectedFiles = dirents\n .filter((dirent) => dirent.isFile())\n .map((dirent) => ({\n absolutePath: path.join(dirent.parentPath, dirent.name),\n // POSIX separators: `requiredOutputFilePaths` and the result format use them on every platform.\n relativePath: path.posix.join(...path.relative(fileOutputPath, dirent.parentPath).split(path.sep), dirent.name),\n }))\n .toSorted((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));\n for (const { absolutePath, relativePath } of expectedFiles) {\n // A larger expectation could never be matched by a received file, so it is an authoring error.\n const expectedStats = await fs.promises.stat(absolutePath);\n if (expectedStats.size > MAX_COMPARED_FILE_BYTES) {\n throw new Error(`expected output file ${relativePath} exceeds ${MAX_COMPARED_FILE_BYTES} bytes`);\n }\n const expected = await fs.promises.readFile(absolutePath);\n const received = await readReceivedFile(cwd, path.join(cwd, relativePath));\n if (received && fileContentsMatch(expected, received)) continue;\n\n mismatchedPaths.push(relativePath);\n // A file too large to ship in a result line is left out of the pair rather than truncated.\n if (expected.length <= MAX_REPORTED_FILE_BYTES) {\n outputFiles.push(encodeFileForTestCaseResult(toComparisonPath(relativePath, 'expected'), expected));\n }\n if (received && received.length <= MAX_REPORTED_FILE_BYTES) {\n outputFiles.push(encodeFileForTestCaseResult(toComparisonPath(relativePath, 'received'), received));\n }\n }\n return { matches: mismatchedPaths.length === 0, mismatchedPaths, outputFiles };\n}\n\n/**\n * The default verdict of the presets: `.out` decides stdout and `.fout/` decides output files, both\n * checked so a result carries every mismatch; either expectation may be absent. Returns whether the\n * run matched and the output files to report (the plain copies of mismatched required output files\n * replaced by their comparison pairs).\n */\nexport async function judgeAgainstExpectations(context: {\n stdout: string;\n expectedStdout: string | undefined;\n fileOutputPath: string | undefined;\n cwd: string;\n outputFiles: readonly OutputFile[];\n}): Promise<{ matches: boolean; outputFiles: OutputFile[] }> {\n const { stdout, expectedStdout, fileOutputPath, cwd } = context;\n let outputFiles = [...context.outputFiles];\n let matches = expectedStdout === undefined || compareStdoutAsSpaceSeparatedTokens(stdout, expectedStdout);\n if (fileOutputPath !== undefined) {\n const comparison = await compareExpectedOutputFiles(cwd, fileOutputPath);\n if (!comparison.matches) {\n matches = false;\n outputFiles = mergeComparisonOutputFiles(outputFiles, comparison);\n }\n }\n return { matches, outputFiles };\n}\n\n/**\n * Replace the plain copies of mismatched required output files with the comparison pairs, so a\n * result shows `<name>_expected.<ext>` / `<name>_received.<ext>` instead of a lone `<name>.<ext>`.\n */\nexport function mergeComparisonOutputFiles(\n outputFiles: readonly OutputFile[],\n comparison: ExpectedOutputFilesComparison\n): OutputFile[] {\n const comparisonPaths = new Set(comparison.outputFiles.map((file) => file.path));\n // The plain copy gives way to its `_received` entry; a mismatch too large for a pair keeps the\n // plain copy, and a kept file literally named like a pair entry would make the path ambiguous.\n const replacedPaths = new Set(\n comparison.mismatchedPaths.filter((filePath) => comparisonPaths.has(toComparisonPath(filePath, 'received')))\n );\n return [\n ...outputFiles.filter((file) => !replacedPaths.has(file.path) && !comparisonPaths.has(file.path)),\n ...comparison.outputFiles,\n ];\n}\n\n/** `c.txt` → `c_expected.txt`; `out/c.txt` → `out/c_expected.txt`. */\nfunction toComparisonPath(filePath: string, role: 'expected' | 'received'): string {\n const { dir, name, ext } = path.posix.parse(filePath);\n return path.posix.join(dir, `${name}_${role}${ext}`);\n}\n\n// A submission controls this path, so anything that is not a plain, readable file within the size\n// limit (a directory, a FIFO that would block, an unreadable or huge file) counts as \"not produced\".\nasync function readReceivedFile(cwd: string, receivedPath: string): Promise<Buffer | undefined> {\n // A submission-planted symlink to the expected file itself would otherwise compare equal.\n if (!(await isSafeSubmissionOutputPath(cwd, receivedPath))) return undefined;\n try {\n const stats = await fs.promises.stat(receivedPath);\n if (!stats.isFile() || stats.size > MAX_COMPARED_FILE_BYTES) return undefined;\n return await fs.promises.readFile(receivedPath);\n } catch {\n return undefined;\n }\n}\n\nfunction fileContentsMatch(expected: Buffer, received: Buffer): boolean {\n if (!isTextFile(expected)) return received.equals(expected);\n if (!isTextFile(received)) return false;\n return compareStdoutAsSpaceSeparatedTokens(received.toString('utf8'), expected.toString('utf8'));\n}\n\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\n// Valid UTF-8 without NUL bytes: a binary format that happens to decode (e.g. a PNG chunk) almost\n// always contains NUL, while text files containing U+FFFD are still treated as text.\nfunction isTextFile(data: Buffer): boolean {\n if (data.includes(0)) return false;\n try {\n utf8Decoder.decode(data);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compareExpectedOutputFiles.cjs","names":["fs","path","encodeFileForTestCaseResult","compareStdoutAsSpaceSeparatedTokens"],"sources":["../../src/helpers/compareExpectedOutputFiles.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { compareStdoutAsSpaceSeparatedTokens } from './compareStdoutAsSpaceSeparatedTokens.js';\nimport { encodeFileForTestCaseResult } from './printTestCaseResult.js';\n\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\n\n// Bound what the harness reads and reports.\nexport const MAX_COMPARED_FILE_BYTES = 8 * 1024 * 1024;\nexport const MAX_REPORTED_FILE_BYTES = 1024 * 1024;\n\nexport interface ExpectedOutputFilesComparison {\n /** Whether every file under the expected directory has a matching file in the working directory. */\n matches: boolean;\n /** Relative paths (from the working directory) of the files that are missing or different. */\n mismatchedPaths: string[];\n /**\n * `<name>_expected.<ext>` / `<name>_received.<ext>` pairs for each mismatched file, in the\n * format Exercode renders side by side. A received file that is missing, unreadable, not a\n * regular file or larger than `MAX_COMPARED_FILE_BYTES` yields only the expected entry, and a\n * file larger than `MAX_REPORTED_FILE_BYTES` is left out of the pair. Exercode decides per test case whether a learner may see these files.\n */\n outputFiles: OutputFile[];\n}\n\n/**\n * Compare the files under a test case's `.fout` directory with the files the submission wrote to\n * its working directory. Text files are compared like stdout (space-separated tokens with a\n * numeric tolerance); binary files must match byte for byte.\n */\nexport async function compareExpectedOutputFiles(\n cwd: string,\n fileOutputPath: string\n): Promise<ExpectedOutputFilesComparison> {\n const mismatchedPaths: string[] = [];\n const outputFiles: OutputFile[] = [];\n const dirents = await fs.promises.readdir(fileOutputPath, { withFileTypes: true, recursive: true });\n const expectedFiles = dirents\n .filter((dirent) => dirent.isFile())\n .map((dirent) => ({\n absolutePath: path.join(dirent.parentPath, dirent.name),\n // POSIX separators: `requiredOutputFilePaths` and the result format use them on every platform.\n relativePath: path.posix.join(...path.relative(fileOutputPath, dirent.parentPath).split(path.sep), dirent.name),\n }))\n .toSorted((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));\n for (const { absolutePath, relativePath } of expectedFiles) {\n // A larger expectation could never be matched by a received file, so it is an authoring error.\n const expectedStats = await fs.promises.stat(absolutePath);\n if (expectedStats.size > MAX_COMPARED_FILE_BYTES) {\n throw new Error(`expected output file ${relativePath} exceeds ${MAX_COMPARED_FILE_BYTES} bytes`);\n }\n const expected = await fs.promises.readFile(absolutePath);\n const received = await readReceivedFile(path.join(cwd, relativePath));\n if (received && fileContentsMatch(expected, received)) continue;\n\n mismatchedPaths.push(relativePath);\n // A file too large to ship in a result line is left out of the pair rather than truncated.\n if (expected.length <= MAX_REPORTED_FILE_BYTES) {\n outputFiles.push(encodeFileForTestCaseResult(toComparisonPath(relativePath, 'expected'), expected));\n }\n if (received && received.length <= MAX_REPORTED_FILE_BYTES) {\n outputFiles.push(encodeFileForTestCaseResult(toComparisonPath(relativePath, 'received'), received));\n }\n }\n return { matches: mismatchedPaths.length === 0, mismatchedPaths, outputFiles };\n}\n\n/**\n * The default verdict of the presets: `.out` decides stdout and `.fout/` decides output files, both\n * checked so a result carries every mismatch; either expectation may be absent. Returns whether the\n * run matched and the output files to report (the plain copies of mismatched required output files\n * replaced by their comparison pairs).\n */\nexport async function judgeAgainstExpectations(context: {\n stdout: string;\n expectedStdout: string | undefined;\n fileOutputPath: string | undefined;\n cwd: string;\n outputFiles: readonly OutputFile[];\n}): Promise<{ matches: boolean; outputFiles: OutputFile[] }> {\n const { stdout, expectedStdout, fileOutputPath, cwd } = context;\n let outputFiles = [...context.outputFiles];\n let matches = expectedStdout === undefined || compareStdoutAsSpaceSeparatedTokens(stdout, expectedStdout);\n if (fileOutputPath !== undefined) {\n const comparison = await compareExpectedOutputFiles(cwd, fileOutputPath);\n if (!comparison.matches) {\n matches = false;\n outputFiles = mergeComparisonOutputFiles(outputFiles, comparison);\n }\n }\n return { matches, outputFiles };\n}\n\n/**\n * Replace the plain copies of mismatched required output files with the comparison pairs, so a\n * result shows `<name>_expected.<ext>` / `<name>_received.<ext>` instead of a lone `<name>.<ext>`.\n */\nexport function mergeComparisonOutputFiles(\n outputFiles: readonly OutputFile[],\n comparison: ExpectedOutputFilesComparison\n): OutputFile[] {\n const comparisonPaths = new Set(comparison.outputFiles.map((file) => file.path));\n // The plain copy gives way to its `_received` entry; a mismatch too large for a pair keeps the\n // plain copy, and a kept file literally named like a pair entry would make the path ambiguous.\n const replacedPaths = new Set(\n comparison.mismatchedPaths.filter((filePath) => comparisonPaths.has(toComparisonPath(filePath, 'received')))\n );\n return [\n ...outputFiles.filter((file) => !replacedPaths.has(file.path) && !comparisonPaths.has(file.path)),\n ...comparison.outputFiles,\n ];\n}\n\n/** `c.txt` → `c_expected.txt`; `out/c.txt` → `out/c_expected.txt`. */\nfunction toComparisonPath(filePath: string, role: 'expected' | 'received'): string {\n const { dir, name, ext } = path.posix.parse(filePath);\n return path.posix.join(dir, `${name}_${role}${ext}`);\n}\n\n// Anything that is not a plain, readable file within the size limit (a directory, a FIFO that would\n// block, an unreadable or huge file) counts as \"not produced\".\nasync function readReceivedFile(receivedPath: string): Promise<Buffer | undefined> {\n try {\n const stats = await fs.promises.stat(receivedPath);\n if (!stats.isFile() || stats.size > MAX_COMPARED_FILE_BYTES) return undefined;\n return await fs.promises.readFile(receivedPath);\n } catch {\n return undefined;\n }\n}\n\nfunction fileContentsMatch(expected: Buffer, received: Buffer): boolean {\n if (!isTextFile(expected)) return received.equals(expected);\n if (!isTextFile(received)) return false;\n return compareStdoutAsSpaceSeparatedTokens(received.toString('utf8'), expected.toString('utf8'));\n}\n\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\n// Valid UTF-8 without NUL bytes: a binary format that happens to decode (e.g. a PNG chunk) almost\n// always contains NUL, while text files containing U+FFFD are still treated as text.\nfunction isTextFile(data: Buffer): boolean {\n if (data.includes(0)) return false;\n try {\n utf8Decoder.decode(data);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{compareStdoutAsSpaceSeparatedTokens as e}from"./compareStdoutAsSpaceSeparatedTokens.js";import{encodeFileForTestCaseResult as t}from"./printTestCaseResult.js";import
|
|
1
|
+
import{compareStdoutAsSpaceSeparatedTokens as e}from"./compareStdoutAsSpaceSeparatedTokens.js";import{encodeFileForTestCaseResult as t}from"./printTestCaseResult.js";import n from"node:fs";import r from"node:path";const i=8388608,a=1048576;async function o(e,a){let o=[],s=[],c=(await n.promises.readdir(a,{withFileTypes:!0,recursive:!0})).filter(e=>e.isFile()).map(e=>({absolutePath:r.join(e.parentPath,e.name),relativePath:r.posix.join(...r.relative(a,e.parentPath).split(r.sep),e.name)})).toSorted((e,t)=>e.relativePath<t.relativePath?-1:+(e.relativePath>t.relativePath));for(let{absolutePath:a,relativePath:f}of c){if((await n.promises.stat(a)).size>8388608)throw Error(`expected output file ${f} exceeds ${i} bytes`);let c=await n.promises.readFile(a),p=await u(r.join(e,f));p&&d(c,p)||(o.push(f),c.length<=1048576&&s.push(t(l(f,`expected`),c)),p&&p.length<=1048576&&s.push(t(l(f,`received`),p)))}return{matches:o.length===0,mismatchedPaths:o,outputFiles:s}}async function s(t){let{stdout:n,expectedStdout:r,fileOutputPath:i,cwd:a}=t,s=[...t.outputFiles],l=r===void 0||e(n,r);if(i!==void 0){let e=await o(a,i);e.matches||(l=!1,s=c(s,e))}return{matches:l,outputFiles:s}}function c(e,t){let n=new Set(t.outputFiles.map(e=>e.path)),r=new Set(t.mismatchedPaths.filter(e=>n.has(l(e,`received`))));return[...e.filter(e=>!r.has(e.path)&&!n.has(e.path)),...t.outputFiles]}function l(e,t){let{dir:n,name:i,ext:a}=r.posix.parse(e);return r.posix.join(n,`${i}_${t}${a}`)}async function u(e){try{let t=await n.promises.stat(e);return!t.isFile()||t.size>8388608?void 0:await n.promises.readFile(e)}catch{return}}function d(t,n){return p(t)?p(n)?e(n.toString(`utf8`),t.toString(`utf8`)):!1:n.equals(t)}const f=new TextDecoder(`utf-8`,{fatal:!0});function p(e){if(e.includes(0))return!1;try{return f.decode(e),!0}catch{return!1}}export{i as MAX_COMPARED_FILE_BYTES,a as MAX_REPORTED_FILE_BYTES,o as compareExpectedOutputFiles,s as judgeAgainstExpectations,c as mergeComparisonOutputFiles};
|
|
2
2
|
//# sourceMappingURL=compareExpectedOutputFiles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compareExpectedOutputFiles.js","names":[],"sources":["../../src/helpers/compareExpectedOutputFiles.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { compareStdoutAsSpaceSeparatedTokens } from './compareStdoutAsSpaceSeparatedTokens.js';\nimport { encodeFileForTestCaseResult } from './printTestCaseResult.js';\
|
|
1
|
+
{"version":3,"file":"compareExpectedOutputFiles.js","names":[],"sources":["../../src/helpers/compareExpectedOutputFiles.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { compareStdoutAsSpaceSeparatedTokens } from './compareStdoutAsSpaceSeparatedTokens.js';\nimport { encodeFileForTestCaseResult } from './printTestCaseResult.js';\n\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\n\n// Bound what the harness reads and reports.\nexport const MAX_COMPARED_FILE_BYTES = 8 * 1024 * 1024;\nexport const MAX_REPORTED_FILE_BYTES = 1024 * 1024;\n\nexport interface ExpectedOutputFilesComparison {\n /** Whether every file under the expected directory has a matching file in the working directory. */\n matches: boolean;\n /** Relative paths (from the working directory) of the files that are missing or different. */\n mismatchedPaths: string[];\n /**\n * `<name>_expected.<ext>` / `<name>_received.<ext>` pairs for each mismatched file, in the\n * format Exercode renders side by side. A received file that is missing, unreadable, not a\n * regular file or larger than `MAX_COMPARED_FILE_BYTES` yields only the expected entry, and a\n * file larger than `MAX_REPORTED_FILE_BYTES` is left out of the pair. Exercode decides per test case whether a learner may see these files.\n */\n outputFiles: OutputFile[];\n}\n\n/**\n * Compare the files under a test case's `.fout` directory with the files the submission wrote to\n * its working directory. Text files are compared like stdout (space-separated tokens with a\n * numeric tolerance); binary files must match byte for byte.\n */\nexport async function compareExpectedOutputFiles(\n cwd: string,\n fileOutputPath: string\n): Promise<ExpectedOutputFilesComparison> {\n const mismatchedPaths: string[] = [];\n const outputFiles: OutputFile[] = [];\n const dirents = await fs.promises.readdir(fileOutputPath, { withFileTypes: true, recursive: true });\n const expectedFiles = dirents\n .filter((dirent) => dirent.isFile())\n .map((dirent) => ({\n absolutePath: path.join(dirent.parentPath, dirent.name),\n // POSIX separators: `requiredOutputFilePaths` and the result format use them on every platform.\n relativePath: path.posix.join(...path.relative(fileOutputPath, dirent.parentPath).split(path.sep), dirent.name),\n }))\n .toSorted((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));\n for (const { absolutePath, relativePath } of expectedFiles) {\n // A larger expectation could never be matched by a received file, so it is an authoring error.\n const expectedStats = await fs.promises.stat(absolutePath);\n if (expectedStats.size > MAX_COMPARED_FILE_BYTES) {\n throw new Error(`expected output file ${relativePath} exceeds ${MAX_COMPARED_FILE_BYTES} bytes`);\n }\n const expected = await fs.promises.readFile(absolutePath);\n const received = await readReceivedFile(path.join(cwd, relativePath));\n if (received && fileContentsMatch(expected, received)) continue;\n\n mismatchedPaths.push(relativePath);\n // A file too large to ship in a result line is left out of the pair rather than truncated.\n if (expected.length <= MAX_REPORTED_FILE_BYTES) {\n outputFiles.push(encodeFileForTestCaseResult(toComparisonPath(relativePath, 'expected'), expected));\n }\n if (received && received.length <= MAX_REPORTED_FILE_BYTES) {\n outputFiles.push(encodeFileForTestCaseResult(toComparisonPath(relativePath, 'received'), received));\n }\n }\n return { matches: mismatchedPaths.length === 0, mismatchedPaths, outputFiles };\n}\n\n/**\n * The default verdict of the presets: `.out` decides stdout and `.fout/` decides output files, both\n * checked so a result carries every mismatch; either expectation may be absent. Returns whether the\n * run matched and the output files to report (the plain copies of mismatched required output files\n * replaced by their comparison pairs).\n */\nexport async function judgeAgainstExpectations(context: {\n stdout: string;\n expectedStdout: string | undefined;\n fileOutputPath: string | undefined;\n cwd: string;\n outputFiles: readonly OutputFile[];\n}): Promise<{ matches: boolean; outputFiles: OutputFile[] }> {\n const { stdout, expectedStdout, fileOutputPath, cwd } = context;\n let outputFiles = [...context.outputFiles];\n let matches = expectedStdout === undefined || compareStdoutAsSpaceSeparatedTokens(stdout, expectedStdout);\n if (fileOutputPath !== undefined) {\n const comparison = await compareExpectedOutputFiles(cwd, fileOutputPath);\n if (!comparison.matches) {\n matches = false;\n outputFiles = mergeComparisonOutputFiles(outputFiles, comparison);\n }\n }\n return { matches, outputFiles };\n}\n\n/**\n * Replace the plain copies of mismatched required output files with the comparison pairs, so a\n * result shows `<name>_expected.<ext>` / `<name>_received.<ext>` instead of a lone `<name>.<ext>`.\n */\nexport function mergeComparisonOutputFiles(\n outputFiles: readonly OutputFile[],\n comparison: ExpectedOutputFilesComparison\n): OutputFile[] {\n const comparisonPaths = new Set(comparison.outputFiles.map((file) => file.path));\n // The plain copy gives way to its `_received` entry; a mismatch too large for a pair keeps the\n // plain copy, and a kept file literally named like a pair entry would make the path ambiguous.\n const replacedPaths = new Set(\n comparison.mismatchedPaths.filter((filePath) => comparisonPaths.has(toComparisonPath(filePath, 'received')))\n );\n return [\n ...outputFiles.filter((file) => !replacedPaths.has(file.path) && !comparisonPaths.has(file.path)),\n ...comparison.outputFiles,\n ];\n}\n\n/** `c.txt` → `c_expected.txt`; `out/c.txt` → `out/c_expected.txt`. */\nfunction toComparisonPath(filePath: string, role: 'expected' | 'received'): string {\n const { dir, name, ext } = path.posix.parse(filePath);\n return path.posix.join(dir, `${name}_${role}${ext}`);\n}\n\n// Anything that is not a plain, readable file within the size limit (a directory, a FIFO that would\n// block, an unreadable or huge file) counts as \"not produced\".\nasync function readReceivedFile(receivedPath: string): Promise<Buffer | undefined> {\n try {\n const stats = await fs.promises.stat(receivedPath);\n if (!stats.isFile() || stats.size > MAX_COMPARED_FILE_BYTES) return undefined;\n return await fs.promises.readFile(receivedPath);\n } catch {\n return undefined;\n }\n}\n\nfunction fileContentsMatch(expected: Buffer, received: Buffer): boolean {\n if (!isTextFile(expected)) return received.equals(expected);\n if (!isTextFile(received)) return false;\n return compareStdoutAsSpaceSeparatedTokens(received.toString('utf8'), expected.toString('utf8'));\n}\n\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\n// Valid UTF-8 without NUL bytes: a binary format that happens to decode (e.g. a PNG chunk) almost\n// always contains NUL, while text files containing U+FFFD are still treated as text.\nfunction isTextFile(data: Buffer): boolean {\n if (data.includes(0)) return false;\n try {\n utf8Decoder.decode(data);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs")
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:fs/promises");t=e.__toESM(t,1);async function n(e,n){await t.default.cp(e,n,{force:!0,recursive:!0})}exports.copyTestCaseFileInput=n;
|
|
2
2
|
//# sourceMappingURL=copyTestCaseFileInput.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copyTestCaseFileInput.cjs","names":["fs"
|
|
1
|
+
{"version":3,"file":"copyTestCaseFileInput.cjs","names":["fs"],"sources":["../../src/helpers/copyTestCaseFileInput.ts"],"sourcesContent":["import fs from 'node:fs/promises';\n\nexport async function copyTestCaseFileInput(fileInputPath: string, cwd: string): Promise<void> {\n await fs.cp(fileInputPath, cwd, { force: true, recursive: true });\n}\n"],"mappings":"qHAEA,eAAsB,EAAsB,EAAuB,EAA4B,CAC7F,MAAMA,EAAAA,QAAG,GAAG,EAAe,EAAK,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,CAClE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"node:fs/promises";async function t(t,n){await e.cp(t,n,{force:!0,recursive:!0})}export{t as copyTestCaseFileInput};
|
|
2
2
|
//# sourceMappingURL=copyTestCaseFileInput.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copyTestCaseFileInput.js","names":[],"sources":["../../src/helpers/copyTestCaseFileInput.ts"],"sourcesContent":["import fs from 'node:fs';\n\
|
|
1
|
+
{"version":3,"file":"copyTestCaseFileInput.js","names":[],"sources":["../../src/helpers/copyTestCaseFileInput.ts"],"sourcesContent":["import fs from 'node:fs/promises';\n\nexport async function copyTestCaseFileInput(fileInputPath: string, cwd: string): Promise<void> {\n await fs.cp(fileInputPath, cwd, { force: true, recursive: true });\n}\n"],"mappings":"gCAEA,eAAsB,EAAsB,EAAuB,EAA4B,CAC7F,MAAM,EAAG,GAAG,EAAe,EAAK,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,CAClE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./printTestCaseResult.cjs")
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./printTestCaseResult.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);async function i(e,i){let a=[];for(let o of i)try{let i=await n.default.promises.readFile(r.default.join(e,o));a.push(t.encodeFileForTestCaseResult(o,i))}catch{}return a}exports.readOutputFiles=i;
|
|
2
2
|
//# sourceMappingURL=readOutputFiles.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readOutputFiles.cjs","names":["
|
|
1
|
+
{"version":3,"file":"readOutputFiles.cjs","names":["fs","path","encodeFileForTestCaseResult"],"sources":["../../src/helpers/readOutputFiles.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { encodeFileForTestCaseResult } from './printTestCaseResult.js';\n\nexport async function readOutputFiles(\n cwd: string,\n outputFilePaths: readonly string[]\n): Promise<NonNullable<TestCaseResult['outputFiles']>> {\n const outputFiles: NonNullable<TestCaseResult['outputFiles']> = [];\n for (const filePath of outputFilePaths) {\n try {\n // Read as-is on purpose: the submission runs as the same OS user as this harness, so a symlink\n // only reaches what it can already read, and a planted FIFO only stalls its own run, which the\n // judge server's request timeout ends.\n const buffer = await fs.promises.readFile(path.join(cwd, filePath));\n outputFiles.push(encodeFileForTestCaseResult(filePath, buffer));\n } catch {\n // file not found\n }\n }\n return outputFiles;\n}\n"],"mappings":"+LAOA,eAAsB,EACpB,EACA,EACqD,CACrD,IAAM,EAA0D,CAAC,EACjE,IAAK,IAAM,KAAY,EACrB,GAAI,CAIF,IAAM,EAAS,MAAMA,EAAAA,QAAG,SAAS,SAASC,EAAAA,QAAK,KAAK,EAAK,CAAQ,CAAC,EAClE,EAAY,KAAKC,EAAAA,4BAA4B,EAAU,CAAM,CAAC,CAChE,MAAQ,CAER,CAEF,OAAO,CACT"}
|
|
@@ -1,11 +1,2 @@
|
|
|
1
1
|
import type { TestCaseResult } from '../types/testCaseResult.js';
|
|
2
2
|
export declare function readOutputFiles(cwd: string, outputFilePaths: readonly string[]): Promise<NonNullable<TestCaseResult['outputFiles']>>;
|
|
3
|
-
/**
|
|
4
|
-
* Whether the harness may read the given submission-created path. When submissions run as the
|
|
5
|
-
* sandbox user while the harness is trusted, a submission could plant a symlink (or a symlinked
|
|
6
|
-
* parent directory) escaping its working directory — e.g. to the 0700 problem directory — and the
|
|
7
|
-
* privileged read would exfiltrate protected bytes, so only paths that resolve inside the working
|
|
8
|
-
* directory are allowed. Without user separation the submission can read those targets itself, so
|
|
9
|
-
* the check is skipped to keep authoring behavior unchanged.
|
|
10
|
-
*/
|
|
11
|
-
export declare function isSafeSubmissionOutputPath(cwd: string, filePath: string): Promise<boolean>;
|