@exercode/problem-utils 4.3.2 → 4.3.4
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/cli/exercodeProblem.cjs +1 -1
- package/dist/cli/exercodeProblem.cjs.map +1 -1
- package/dist/cli/exercodeProblem.js +1 -1
- package/dist/cli/exercodeProblem.js.map +1 -1
- package/dist/helpers/guiRecording.cjs +1 -1
- package/dist/helpers/guiRecording.cjs.map +1 -1
- package/dist/helpers/guiRecording.js +1 -1
- package/dist/helpers/guiRecording.js.map +1 -1
- package/dist/helpers/javaJudge.cjs +1 -1
- package/dist/helpers/javaJudge.cjs.map +1 -1
- package/dist/helpers/javaJudge.js +1 -1
- package/dist/helpers/javaJudge.js.map +1 -1
- package/dist/helpers/parseCsv.cjs +2 -2
- package/dist/helpers/parseCsv.cjs.map +1 -1
- package/dist/helpers/parseCsv.js +2 -2
- package/dist/helpers/parseCsv.js.map +1 -1
- package/dist/helpers/removeCommentsInSourceCode.cjs +4 -4
- package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
- package/dist/helpers/removeCommentsInSourceCode.js +4 -4
- package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
- package/dist/helpers/runHarnessProcess.cjs +1 -1
- package/dist/helpers/runHarnessProcess.cjs.map +1 -1
- package/dist/helpers/runHarnessProcess.js +1 -1
- package/dist/helpers/runHarnessProcess.js.map +1 -1
- package/dist/helpers/spawnWithLimits.cjs +1 -1
- package/dist/helpers/spawnWithLimits.cjs.map +1 -1
- package/dist/helpers/spawnWithLimits.js +1 -1
- package/dist/helpers/spawnWithLimits.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/learningMaterial/validateCourse.cjs +1 -1
- package/dist/learningMaterial/validateCourse.cjs.map +1 -1
- package/dist/learningMaterial/validateCourse.js +1 -1
- package/dist/learningMaterial/validateCourse.js.map +1 -1
- package/dist/learningMaterial/validateProblem.cjs +1 -1
- package/dist/learningMaterial/validateProblem.cjs.map +1 -1
- package/dist/learningMaterial/validateProblem.js +1 -1
- package/dist/learningMaterial/validateProblem.js.map +1 -1
- package/dist/presets/guiCommand.cjs +1 -1
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.js +1 -1
- package/dist/presets/guiCommand.js.map +1 -1
- package/package.json +7 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateProblem.js","names":["ALLOWED_PARSING_HELPERS","PARSING_CODE_SNIPPETS","SHARED_FILE_INPUT_NAME","IGNORED_TEST_CASE_ENTRY_NAMES","validateProblemDirectory","errors","warnings","result","absoluteDirectoryPath","resolve","problemDirectoryPath","isRegularDirectory","isDirectory","problemId","basename","LEARNING_MATERIAL_ID_REGEX","linkPath","listSymbolicLinks","relative","problemDirents","readdir","dirent","d1","d2","problemFilePath","join","isRegularFile","isFile","frontmatter","parseProblemFrontmatter","harness","readHarnessFiles","requiresExpectedOutput","judgesTestCasesWithoutExpectations","validateTestCasePresence","readFileTestCases","isScoredAutomatically","validateHarnessFiles","modelAnswers","readModelAnswers","usableModelAnswers","isRunnable","answer","findLanguageDefinitionByPath","file","validateCodePatterns","validateRequiredSubmissionFiles","validateTemplates","hasJudgeTs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","hasParsedFrontmatter","attributes","body","parseFrontmatter","readFile","reportExcessChatMarkers","error","getErrorMessage","parsed","problemFrontmatterSchema","formatZodIssues","testCasesDirectoryPath","dirents","idToTestCase","getTestCase","testCase","testCaseId","listFilesRecursively","filePaths","filePath","hasCustomJudgeTs","stat","MAX_COMPARED_FILE_BYTES","fileNames","f1","f2","fileName","content","testCases","t1","t2","hasAnyExpectedOutput","hasExpectedOutput","maxStdoutLength","MAX_STDOUT_LENGTH","stdinToFirstId","firstId","linkPaths","directoryPath","entryPath","p1","p2","fileTestCases","reportTestCaseCompositionIssues","EXAMPLE_TEST_CASE_ID_PATTERN","modelAnswersDirectoryPath","availableLanguageIds","files","readSourceFilesRecursively","requiredRegExps","compilePatterns","forbiddenRegExps","forbiddenTexts","normalizeCodeRule","rule","pattern","regExp","snippet","text","modelAnswer","strippedFiles","readSourceCodeWithoutComments","matched","forbiddenText","filePathSet","requiredPath","languageDefinition","grammar","removeCommentsInSourceCode","compiled","rules","fieldName","templatesDirectoryPath","entries","hasFiles","hasDirectories","directoryNames","templateDirectoryNames","templateDirectoryName","templateFiles","templateContentByPath","judgedFiles"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { MAX_COMPARED_FILE_BYTES } from '../helpers/compareExpectedOutputFiles.js';\nimport { findDefaultStdioHarnessFiles, type HarnessFileName } from '../helpers/defaultStdioHarness.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { judgesTestCasesWithoutExpectations } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { removeCommentsInSourceCode } from '../helpers/removeCommentsInSourceCode.js';\nimport { EXAMPLE_TEST_CASE_ID_PATTERN, MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { type CodeRule, normalizeCodeRule } from '../types/problem.js';\n\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n readSourceFilesRecursively,\n type SourceFile,\n} from './fsHelpers.js';\nimport {\n availableLanguageIds,\n LEARNING_MATERIAL_ID_REGEX,\n problemFrontmatterSchema,\n type ProblemFrontmatter,\n} from './schemas.js';\nimport { reportExcessChatMarkers } from './validateMaterial.js';\nimport { formatZodIssues, type ValidationResult } from './validationResult.js';\n\nconst RECOMMENDED_MIN_TEST_CASE_COUNT = 4;\n// Forbidden patterns must not restrict classes/functions commonly needed just to parse stdin (from gen-em).\n// Instead of a substring check on the pattern source (\"print\" contains \"int\"), each forbidden\n// pattern is applied to representative parsing code; a hit means the pattern would forbid parsing.\nconst ALLOWED_PARSING_HELPERS = ['Scanner', 'map', 'int', 'split'] as const;\nconst PARSING_CODE_SNIPPETS = [\n 'a, b = map(int, input().split())',\n 'Scanner scanner = new Scanner(System.in);',\n \"const [a, b] = line.split(' ').map(Number);\",\n] as const;\n\ninterface FileTestCase {\n id: string;\n stdin?: string;\n stdout?: string;\n /** Whether `<id>.fin/` (input files copied into the working directory) exists and holds at least one file. */\n hasFileInput: boolean;\n /** Whether `<id>.fout/` (expected output files) exists and holds at least one file. */\n hasFileOutput: boolean;\n /** Whether `<id>.json` (configuration read by a custom `judge.ts`) exists. */\n hasJudgeConfig: boolean;\n}\n\n// The importer rejects a longer expected stdout whichever judge the problem has.\nconst IMPORTER_MAX_STDOUT_LENGTH = 100_000;\n// `_shared.fin/` holds files copied for every test case; it is not a test case itself.\nconst SHARED_FILE_INPUT_NAME = '_shared';\n// The importer leaves generated artifacts out of the packaged problem, so a directory holding only\n// these is as good as empty. Any other file counts, even a `.gitkeep` placeholder.\nconst IGNORED_TEST_CASE_ENTRY_NAMES: ReadonlySet<string> = new Set(['.DS_Store', '__pycache__']);\n\ninterface ModelAnswer {\n id: string;\n files: SourceFile[];\n}\n\n/**\n * Validates a judge v2 problem directory (problem.md, test_cases, model_answers, templates, and —\n * for custom judging or custom debugging only — judge.ts/debug.ts).\n */\nexport async function validateProblemDirectory(problemDirectoryPath: string): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const absoluteDirectoryPath = resolve(problemDirectoryPath);\n // Problem discovery does not traverse a symbolic link, so a linked problem directory is never imported.\n if (!(await isRegularDirectory(absoluteDirectoryPath))) {\n errors.push(\n (await isDirectory(absoluteDirectoryPath))\n ? `problem directory is a symbolic link, which the judge does not traverse: ${problemDirectoryPath}`\n : `problem directory not found: ${problemDirectoryPath}`\n );\n return result;\n }\n\n const problemId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(`problem ID \"${problemId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n // The judge's front-matter reader takes the first `problem.md` or `*.problem.md` it finds, so a\n // leftover v1 file can be judged instead of problem.md.\n // The judge readers enumerate regular files and directories only, so a symbolic link anywhere in\n // the problem is ignored at import time and would silently drop or change what is judged.\n for (const linkPath of await listSymbolicLinks(absoluteDirectoryPath)) {\n errors.push(\n `${relative(absoluteDirectoryPath, linkPath)} is a symbolic link, which the judge ignores; commit a real file or directory`\n );\n }\n const problemDirents = await readdir(absoluteDirectoryPath, { withFileTypes: true });\n for (const dirent of problemDirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (dirent.isFile() && dirent.name.endsWith('.problem.md')) {\n errors.push(`found v1 problem file ${dirent.name}; use the v2 layout with a file named exactly problem.md`);\n }\n }\n\n const problemFilePath = join(absoluteDirectoryPath, 'problem.md');\n // The judge discovers problems by their problem.md and skips symbolic links (reported above).\n if (!(await isRegularFile(problemFilePath))) {\n if (!(await isFile(problemFilePath))) {\n errors.push('problem.md not found; a v2 problem directory must contain problem.md');\n }\n return result;\n }\n\n const frontmatter = await parseProblemFrontmatter(problemFilePath, errors);\n const harness = await readHarnessFiles(absoluteDirectoryPath);\n const requiresExpectedOutput = frontmatter === undefined || !judgesTestCasesWithoutExpectations(frontmatter);\n const fileTestCases = await readFileTestCases(\n absoluteDirectoryPath,\n { hasCustomJudgeTs: harness.hasCustomJudgeTs, requiresExpectedOutput },\n errors,\n warnings\n );\n // A default judge.ts is about to be deleted, so treat only a genuinely custom judge.ts as a\n // special judge; otherwise the case-less error would surface only after the deletion.\n validateTestCasePresence(frontmatter, fileTestCases, harness.hasCustomJudgeTs, errors, warnings);\n\n // A problem whose frontmatter failed to parse already has a primary error; deriving secondary\n // judge.ts/model-answer errors from it would send the fix loop after the wrong problems.\n const isScoredAutomatically =\n frontmatter !== undefined && frontmatter.isManualScoringRequired !== true && frontmatter.type !== 'prompt_study';\n validateHarnessFiles(harness, frontmatter !== undefined, errors, warnings);\n\n const modelAnswers = await readModelAnswers(absoluteDirectoryPath, errors, warnings);\n // Every model answer directory is judged by the all-problem check, so each one must hold what\n // its judge runs: a custom judge defines that itself (e.g. a CSV submission), while the stdio\n // judge needs a non-empty file of a supported language as its entry point.\n const usableModelAnswers = modelAnswers.filter((answer) => {\n // A custom judge may accept an empty data file, so only the stdio judge needs content.\n if (harness.hasCustomJudgeTs) return true;\n const isRunnable = answer.files.some(\n (file) => findLanguageDefinitionByPath(file.path) !== undefined && file.data.trim().length > 0\n );\n if (!isRunnable) {\n errors.push(\n `model answer directory \"${answer.id}\" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`\n );\n }\n return isRunnable;\n });\n if (isScoredAutomatically && modelAnswers.length === 0) {\n errors.push('no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files');\n }\n if (frontmatter) {\n validateCodePatterns(frontmatter, usableModelAnswers, errors, warnings);\n validateRequiredSubmissionFiles(frontmatter, usableModelAnswers, errors);\n }\n await validateTemplates(absoluteDirectoryPath, usableModelAnswers, harness.hasCustomJudgeTs, errors, warnings);\n\n return result;\n}\n\ninterface HarnessFiles {\n hasDebugTs: boolean;\n rejectedDefaultHarnessFileNames: readonly HarnessFileName[];\n hasCustomJudgeTs: boolean;\n}\n\nasync function readHarnessFiles(absoluteDirectoryPath: string): Promise<HarnessFiles> {\n const hasJudgeTs = await isFile(join(absoluteDirectoryPath, 'judge.ts'));\n // The same policy the `judge` subcommand and the all-problem check apply: a default judge.ts is\n // always rejected, and a default debug.ts is rejected unless it accompanies a custom judge.ts.\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(absoluteDirectoryPath);\n return {\n hasDebugTs: await isFile(join(absoluteDirectoryPath, 'debug.ts')),\n rejectedDefaultHarnessFileNames: defaultHarnessFileNames,\n hasCustomJudgeTs: hasJudgeTs && !defaultHarnessFileNames.includes('judge.ts'),\n };\n}\n\n/**\n * The judge treats a problem without judge.ts as a standard stdin/stdout problem and auto-generates\n * both the stdio judge and the stdio debug runner. Committed copies of those default harnesses are\n * rejected: they add nothing and can drift from the server's defaults, and a default judge.ts also\n * hides the standard-problem marker that enables the default debug runner.\n */\nfunction validateHarnessFiles(\n harness: HarnessFiles,\n hasParsedFrontmatter: boolean,\n errors: string[],\n warnings: string[]\n): void {\n if (harness.rejectedDefaultHarnessFileNames.includes('judge.ts')) {\n errors.push(\n 'judge.ts only calls stdioJudgePreset; delete it — the judge auto-generates it, and only a problem without judge.ts is treated as a standard stdio problem (which enables the default debug runner)'\n );\n }\n if (harness.rejectedDefaultHarnessFileNames.includes('debug.ts')) {\n errors.push(\n 'debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts'\n );\n }\n // A custom debug.ts without judge.ts stays accepted: the judge runs debug.ts whenever it is\n // present (judge.ts only selects the judging side), so it is a supported way to customize\n // debugging for a problem judged by the default stdio judge.\n // The debug feature itself is independent of the scoring mode, so warn for manual-scoring and\n // prompt_study problems too.\n if (hasParsedFrontmatter && harness.hasCustomJudgeTs && !harness.hasDebugTs) {\n warnings.push(\n \"custom judge.ts without debug.ts; the debug feature is unavailable for this problem — add debug.ts (e.g. calling stdioDebugPreset from '@exercode/problem-utils/presets/stdio') or a custom debug harness\"\n );\n }\n}\n\nasync function parseProblemFrontmatter(\n problemFilePath: string,\n errors: string[]\n): Promise<ProblemFrontmatter | undefined> {\n let attributes: unknown;\n try {\n let body: string;\n ({ attributes, body } = parseFrontmatter(await readFile(problemFilePath, 'utf8')));\n // exercode rejects problem bodies with more than one standalone chat delimiter, like materials.\n reportExcessChatMarkers(body, errors);\n } catch (error) {\n errors.push(`problem.md: invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return undefined;\n }\n const parsed = problemFrontmatterSchema.safeParse(attributes);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'problem.md frontmatter'));\n return undefined;\n }\n return parsed.data;\n}\n\nasync function readFileTestCases(\n problemDirectoryPath: string,\n { hasCustomJudgeTs, requiresExpectedOutput }: { hasCustomJudgeTs: boolean; requiresExpectedOutput: boolean },\n errors: string[],\n warnings: string[]\n): Promise<FileTestCase[]> {\n const testCasesDirectoryPath = join(problemDirectoryPath, 'test_cases');\n if (!(await isDirectory(testCasesDirectoryPath))) return [];\n\n const dirents = await readdir(testCasesDirectoryPath, { withFileTypes: true });\n const idToTestCase = new Map<string, FileTestCase>();\n const getTestCase = (testCaseId: string): FileTestCase => {\n let testCase = idToTestCase.get(testCaseId);\n if (!testCase) {\n testCase = {\n id: testCaseId,\n hasFileInput: false,\n hasFileOutput: false,\n hasJudgeConfig: false,\n };\n idToTestCase.set(testCaseId, testCase);\n }\n return testCase;\n };\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n // Symbolic links were already reported for the whole problem directory.\n if (dirent.isSymbolicLink()) continue;\n // `_shared` is reserved: only `_shared.fin/` (a directory of inputs copied for every case) exists.\n if (dirent.name === SHARED_FILE_INPUT_NAME || dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)) {\n if (dirent.name !== `${SHARED_FILE_INPUT_NAME}.fin`) {\n errors.push(`test_cases/${dirent.name} is not supported; only _shared.fin/ is shared across test cases`);\n } else if (!dirent.isDirectory()) {\n errors.push(`test_cases/${dirent.name} must be a directory`);\n } else {\n const sharedFilePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (sharedFilePaths.length === 0) {\n errors.push(`test_cases/${dirent.name}/ is empty; put at least one shared input file in it`);\n }\n }\n continue;\n }\n const testCaseId = dirent.name.replace(/\\.(fin|fout)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${dirent.name} must have a test case ID before its extension`);\n continue;\n }\n if (testCaseId === dirent.name) {\n // A directory named like a file entry is a wrong-typed entry, not an ignorable stranger.\n if (dirent.isDirectory() && /\\.(in|out|json)$/.test(dirent.name)) {\n errors.push(`test_cases/${dirent.name} must be a file, not a directory`);\n }\n continue;\n }\n if (!dirent.isDirectory()) {\n errors.push(`test case ${testCaseId}: test_cases/${dirent.name} must be a directory`);\n continue;\n }\n // git does not track an empty directory, so an empty `.fin/` or `.fout/` would silently vanish on\n // commit, and the importer counts only real files that are not generated artifacts (an empty\n // subdirectory does not count); such a directory is an error and does not make the id a test case.\n const filePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (filePaths.length === 0) {\n errors.push(`test case ${testCaseId}: ${dirent.name}/ is empty; put at least one file in it or delete it`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n if (dirent.name.endsWith('.fin')) {\n testCase.hasFileInput = true;\n continue;\n }\n testCase.hasFileOutput = true;\n // The importer keeps a placeholder like any file, so the stdio judge would require the program to write it.\n for (const filePath of filePaths) {\n if (basename(filePath) === '.gitkeep') {\n warnings.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is compared as an expected output file; delete the placeholder`\n );\n }\n }\n // The stdio judge compares each expected file and refuses one above its limit (the importer rejects\n // the problem); a custom judge.ts reads the directory under its own contract.\n if (!hasCustomJudgeTs) {\n for (const filePath of filePaths) {\n const fileStats = await stat(filePath);\n if (fileStats.size > MAX_COMPARED_FILE_BYTES) {\n errors.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is larger than ${MAX_COMPARED_FILE_BYTES} bytes, which the stdio judge cannot compare`\n );\n }\n }\n }\n }\n // `_shared.*` files were already reported by the entry loop above.\n const fileNames = dirents\n .filter(\n (dirent) =>\n dirent.isFile() && /\\.(in|out|json)$/.test(dirent.name) && !dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)\n )\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const testCaseId = fileName.replace(/\\.(in|out|json)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${fileName} must have a test case ID before its extension`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n const content = await readFile(join(testCasesDirectoryPath, fileName), 'utf8');\n if (fileName.endsWith('.in')) {\n testCase.stdin = content.trimEnd();\n } else if (fileName.endsWith('.out')) {\n testCase.stdout = content.trimEnd();\n } else {\n // The presets ignore `.json`; only a custom judge.ts reads it, with a shape of its own.\n testCase.hasJudgeConfig = true;\n if (!hasCustomJudgeTs) {\n errors.push(\n `test case ${testCaseId}: ${fileName} is configuration for a custom judge.ts, which this problem does not have`\n );\n }\n try {\n JSON.parse(content);\n } catch {\n errors.push(`test case ${testCaseId}: ${fileName} is not valid JSON`);\n }\n }\n }\n\n // Directories and files are read in separate passes, so sort for deterministic per-case diagnostics below\n // (the entry-level diagnostics above follow the entry-name order of their own passes).\n const testCases = [...idToTestCase.values()].toSorted((t1, t2) => t1.id.localeCompare(t2.id));\n // A custom judge defines its own contract (e.g. it renders an output file from the input and\n // ships `.in` only), but it usually treats every case alike, so a partial set of expectations\n // still hints at a forgotten file. Input is optional in every form, so nothing guesses at it.\n // `.json` is configuration, not an output expectation, so it neither counts nor is warned about.\n const hasAnyExpectedOutput = testCases.some(hasExpectedOutput);\n for (const testCase of testCases) {\n // An empty `.fout/` was already reported above and does not count as an expectation, so a case that\n // exists through another entry is also reported here. A `.json` case without judge.ts was already\n // rejected as unsupported configuration.\n if (!hasExpectedOutput(testCase) && !testCase.hasJudgeConfig) {\n // The default stdio harness rejects such a case, so it is an error without judge.ts.\n if (!hasCustomJudgeTs) {\n if (!requiresExpectedOutput) continue;\n errors.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`\n );\n } else if (hasAnyExpectedOutput) {\n warnings.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing while other cases have one`\n );\n }\n }\n // The stdio judge rejects a run whose raw stdout exceeds the limit, so a longer expectation can\n // never match (a program may still print exactly the limit without a trailing newline).\n // A custom judge.ts reads `.out` under its own contract, so only the importer's limit applies to it.\n // The importer measures the trimmed expectation. The stdio judge measures the raw output a run\n // prints; a run matching a trimmed expectation prints at least that many characters (the token\n // comparison ignores trailing whitespace), so only a longer trimmed expectation is impossible.\n if (testCase.stdout !== undefined) {\n const maxStdoutLength = hasCustomJudgeTs ? IMPORTER_MAX_STDOUT_LENGTH : MAX_STDOUT_LENGTH;\n if (testCase.stdout.length > maxStdoutLength) {\n errors.push(\n hasCustomJudgeTs\n ? `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length} > ${maxStdoutLength})`\n : `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length}); the stdio judge rejects a run that prints more than ${maxStdoutLength} characters`\n );\n }\n }\n // The judge accepts an empty `.out` (the program must print nothing or an empty line), but it is\n // usually a stray file of a case whose stdout is not compared.\n if (testCase.stdout !== undefined && testCase.stdout.length === 0) {\n warnings.push(\n `test case ${testCase.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`\n );\n }\n }\n\n // Only stdin is compared: a case with input files may legitimately share its stdin with others,\n // and file contents are not compared for duplicates.\n const stdinToFirstId = new Map<string, string>();\n for (const testCase of testCases) {\n // An empty `.in` means \"no input\" (like an absent one) and is not a duplicate of another empty one.\n if (!testCase.stdin || testCase.hasFileInput) continue;\n const firstId = stdinToFirstId.get(testCase.stdin);\n if (firstId === undefined) {\n stdinToFirstId.set(testCase.stdin, testCase.id);\n } else {\n warnings.push(`test cases ${firstId} and ${testCase.id} have identical input`);\n }\n }\n return testCases;\n}\n\nasync function listSymbolicLinks(directoryPath: string): Promise<string[]> {\n const linkPaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isSymbolicLink()) {\n linkPaths.push(entryPath);\n } else if (dirent.isDirectory() && dirent.name !== 'node_modules') {\n linkPaths.push(...(await listSymbolicLinks(entryPath)));\n }\n }\n return linkPaths.toSorted((p1, p2) => p1.localeCompare(p2));\n}\n\n/** Lists the real files under a test case directory, leaving out generated artifacts like the importer. */\nasync function listFilesRecursively(directoryPath: string): Promise<string[]> {\n const filePaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n if (IGNORED_TEST_CASE_ENTRY_NAMES.has(dirent.name)) continue;\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isFile()) {\n filePaths.push(entryPath);\n } else if (dirent.isDirectory()) {\n filePaths.push(...(await listFilesRecursively(entryPath)));\n }\n }\n return filePaths;\n}\n\nfunction hasExpectedOutput(testCase: FileTestCase): boolean {\n return testCase.stdout !== undefined || testCase.hasFileOutput;\n}\n\nfunction validateTestCasePresence(\n frontmatter: ProblemFrontmatter | undefined,\n fileTestCases: FileTestCase[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): void {\n // Exercode's import requires test cases, a judge.ts or isManualScoringRequired and grants no\n // exemption for `type: prompt_study` or static-analysis rules (this package's `judge` subcommand\n // runs a rules-only problem, but it cannot be imported).\n if (fileTestCases.length === 0 && frontmatter?.isManualScoringRequired !== true) {\n // The judge only rejects a case-less problem when judge.ts is also missing; a special judge\n // may compute results itself, but the stdio presets need test_cases/, hence the warning.\n if (hasCustomJudgeTs) {\n warnings.push(\n 'no test cases found; fine for a special judge, but stdio-preset judges require an expected output (test_cases/<id>.out or <id>.fout/) for every case'\n );\n } else {\n errors.push(\n 'no test cases found; add test cases with an expected output (test_cases/<id>.out or <id>.fout/, plus <id>.in or <id>.fin/ when the program reads input; at least one example_* and one hidden test_*), or set isManualScoringRequired: true'\n );\n }\n }\n if (fileTestCases.length > 0) {\n reportTestCaseCompositionIssues(fileTestCases, errors, warnings);\n }\n}\n\nfunction reportTestCaseCompositionIssues(\n testCases: readonly { id: string }[],\n errors: string[],\n warnings: string[]\n): void {\n if (!testCases.some((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push('no example test case found; add at least one test case whose ID contains \"example\" (e.g. example_1)');\n }\n if (testCases.every((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push(\n 'no hidden test case found; add at least one test case whose ID does not contain \"example\" (e.g. test_1)'\n );\n }\n if (testCases.length < RECOMMENDED_MIN_TEST_CASE_COUNT) {\n warnings.push(\n `only ${testCases.length} test cases found; ${RECOMMENDED_MIN_TEST_CASE_COUNT} or more are recommended`\n );\n }\n}\n\nasync function readModelAnswers(\n problemDirectoryPath: string,\n errors: string[],\n warnings: string[]\n): Promise<ModelAnswer[]> {\n const modelAnswersDirectoryPath = join(problemDirectoryPath, 'model_answers');\n if (!(await isDirectory(modelAnswersDirectoryPath))) return [];\n\n const modelAnswers: ModelAnswer[] = [];\n const dirents = await readdir(modelAnswersDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (!dirent.isDirectory() || dirent.name === '__pycache__') continue;\n // The judge parses each directory name with learningMaterialIdSchema, so a regex-invalid\n // name (e.g. \"Python\") aborts import, while an unknown but valid ID (e.g. \"foo\") imports fine.\n if (!LEARNING_MATERIAL_ID_REGEX.test(dirent.name)) {\n errors.push(`model answer directory name \"${dirent.name}\" must match ${LEARNING_MATERIAL_ID_REGEX}`);\n continue;\n }\n if (!(availableLanguageIds as readonly string[]).includes(dirent.name)) {\n warnings.push(\n `model answer directory name \"${dirent.name}\" is not a known language ID (${availableLanguageIds.join(', ')})`\n );\n }\n const files = await readSourceFilesRecursively(join(modelAnswersDirectoryPath, dirent.name));\n // The all-problem check judges every model answer directory, so an empty one fails there.\n if (files.length === 0) {\n errors.push(`model answer directory \"${dirent.name}\" has no source files; add files or delete it`);\n continue;\n }\n modelAnswers.push({ id: dirent.name, files });\n }\n return modelAnswers;\n}\n\nfunction validateCodePatterns(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[],\n warnings: string[]\n): void {\n const requiredRegExps = compilePatterns(frontmatter.requiredRegExpsInCode, 'requiredRegExpsInCode', errors);\n const forbiddenRegExps = compilePatterns(frontmatter.forbiddenRegExpsInCode, 'forbiddenRegExpsInCode', errors);\n const forbiddenTexts = frontmatter.forbiddenTextsInCode.map((rule) => normalizeCodeRule(rule).pattern);\n\n for (const [pattern, regExp] of forbiddenRegExps) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => regExp.test(snippet))) {\n warnings.push(\n `forbiddenRegExpsInCode pattern \"${pattern}\" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n for (const text of forbiddenTexts) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => snippet.includes(text))) {\n warnings.push(\n `forbiddenTextsInCode text \"${text}\" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n\n for (const modelAnswer of modelAnswers) {\n const strippedFiles = readSourceCodeWithoutComments(modelAnswer.files);\n\n for (const [pattern, regExp] of requiredRegExps) {\n if (!strippedFiles.some((file) => regExp.test(file.data))) {\n errors.push(\n `required pattern \"${pattern}\" does not match any file of model answer \"${modelAnswer.id}\" (comments are ignored)`\n );\n }\n }\n for (const [pattern, regExp] of forbiddenRegExps) {\n for (const file of strippedFiles) {\n const matched = file.data.match(regExp);\n if (matched) {\n errors.push(\n `forbidden pattern \"${pattern}\" matches model answer file ${modelAnswer.id}/${file.path} (matched \"${matched[0]}\")`\n );\n }\n }\n }\n for (const forbiddenText of forbiddenTexts) {\n for (const file of strippedFiles) {\n if (file.data.includes(forbiddenText)) {\n errors.push(`forbidden text \"${forbiddenText}\" appears in model answer file ${modelAnswer.id}/${file.path}`);\n }\n }\n }\n }\n}\n\n/**\n * The judge rejects a submission missing a required file before running it, so every model answer\n * must ship them among the files the importer keeps (the same filter as `readSourceFilesRecursively`).\n */\nfunction validateRequiredSubmissionFiles(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[]\n): void {\n for (const modelAnswer of modelAnswers) {\n const filePathSet = new Set(modelAnswer.files.map((file) => file.path));\n for (const requiredPath of frontmatter.requiredSubmissionFilePaths) {\n if (!filePathSet.has(requiredPath)) {\n errors.push(\n `required submission file \"${requiredPath}\" is missing from model answer \"${modelAnswer.id}\" (requiredSubmissionFilePaths)`\n );\n }\n }\n }\n}\n\n/** Selects and strips the files exactly like the judge's static analysis of a submission does. */\nfunction readSourceCodeWithoutComments(files: SourceFile[]): SourceFile[] {\n return files.flatMap((file) => {\n const languageDefinition = findLanguageDefinitionByPath(file.path);\n if (!languageDefinition || file.data.includes('\\uFFFD')) return [];\n const grammar = languageDefinition.grammer;\n return [{ path: file.path, data: grammar ? removeCommentsInSourceCode(grammar, file.data) : file.data }];\n });\n}\n\nfunction compilePatterns(rules: CodeRule[], fieldName: string, errors: string[]): [string, RegExp][] {\n const compiled: [string, RegExp][] = [];\n for (const rule of rules) {\n const { pattern } = normalizeCodeRule(rule);\n try {\n compiled.push([pattern, new RegExp(pattern)]);\n } catch {\n errors.push(`invalid regular expression in ${fieldName}: ${pattern}`);\n }\n }\n return compiled;\n}\n\nasync function validateTemplates(\n problemDirectoryPath: string,\n modelAnswers: ModelAnswer[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const templatesDirectoryPath = join(problemDirectoryPath, 'templates');\n if (!(await isDirectory(templatesDirectoryPath))) return;\n\n const dirents = await readdir(templatesDirectoryPath, { withFileTypes: true });\n // The judge only skips __pycache__; a stray .DS_Store next to template directories fails\n // import as a mixed files-and-directories layout, so it must not be filtered out here.\n const entries = dirents.filter((dirent) => dirent.name !== '__pycache__');\n if (entries.some((dirent) => dirent.name === '.DS_Store')) {\n warnings.push('templates/.DS_Store found; delete it (the judge treats it as a template file)');\n }\n const hasFiles = entries.some((dirent) => dirent.isFile());\n const hasDirectories = entries.some((dirent) => dirent.isDirectory());\n if (hasFiles && hasDirectories) {\n errors.push(\n 'templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)'\n );\n return;\n }\n for (const dirent of entries) {\n if (\n dirent.isDirectory() &&\n dirent.name !== '_default' &&\n !(availableLanguageIds as readonly string[]).includes(dirent.name)\n ) {\n errors.push(\n `invalid template directory name \"${dirent.name}\"; expected a language ID (${availableLanguageIds.join(', ')}) or _default`\n );\n }\n }\n // A template must be an incomplete starting point: a template directory that carries every file\n // a model answer is judged by, unchanged, would pass the judge. The stdio judge runs the source\n // files of a supported language (notes next to the answer do not change what runs), while a\n // custom judge.ts may read any file (e.g. a CSV submission). Helper modules shared by both are fine.\n const directoryNames = entries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);\n const templateDirectoryNames = hasDirectories ? directoryNames : [''];\n for (const templateDirectoryName of templateDirectoryNames) {\n const templateFiles = await readSourceFilesRecursively(join(templatesDirectoryPath, templateDirectoryName));\n const templateContentByPath = new Map(templateFiles.map((file) => [file.path, file.data]));\n for (const modelAnswer of modelAnswers) {\n const judgedFiles = modelAnswer.files.filter(\n (file) =>\n hasCustomJudgeTs || (file.data.trim().length > 0 && findLanguageDefinitionByPath(file.path) !== undefined)\n );\n if (judgedFiles.length > 0 && judgedFiles.every((file) => templateContentByPath.get(file.path) === file.data)) {\n warnings.push(\n `templates/${templateDirectoryName} contains every judged file of model answer \"${modelAnswer.id}\" unchanged (identical to a model answer file); templates must not pass the judge`\n );\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validateProblem.js","names":["ALLOWED_PARSING_HELPERS","PARSING_CODE_SNIPPETS","SHARED_FILE_INPUT_NAME","IGNORED_TEST_CASE_ENTRY_NAMES","validateProblemDirectory","errors","warnings","result","absoluteDirectoryPath","resolve","problemDirectoryPath","isRegularDirectory","isDirectory","problemId","basename","LEARNING_MATERIAL_ID_REGEX","linkPath","listSymbolicLinks","relative","problemDirents","readdir","dirent","d1","d2","problemFilePath","join","isRegularFile","isFile","frontmatter","parseProblemFrontmatter","harness","readHarnessFiles","requiresExpectedOutput","judgesTestCasesWithoutExpectations","validateTestCasePresence","readFileTestCases","isScoredAutomatically","validateHarnessFiles","modelAnswers","readModelAnswers","usableModelAnswers","isRunnable","answer","findLanguageDefinitionByPath","file","validateCodePatterns","validateRequiredSubmissionFiles","validateTemplates","hasJudgeTs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","hasParsedFrontmatter","attributes","body","parseFrontmatter","readFile","reportExcessChatMarkers","error","getErrorMessage","parsed","problemFrontmatterSchema","formatZodIssues","testCasesDirectoryPath","dirents","idToTestCase","getTestCase","testCase","testCaseId","listFilesRecursively","filePaths","filePath","hasCustomJudgeTs","stat","MAX_COMPARED_FILE_BYTES","fileNames","f1","f2","fileName","content","testCases","t1","t2","hasAnyExpectedOutput","hasExpectedOutput","maxStdoutLength","MAX_STDOUT_LENGTH","stdinToFirstId","firstId","linkPaths","directoryPath","entryPath","p1","p2","fileTestCases","reportTestCaseCompositionIssues","EXAMPLE_TEST_CASE_ID_PATTERN","modelAnswersDirectoryPath","availableLanguageIds","files","readSourceFilesRecursively","requiredRegExps","compilePatterns","forbiddenRegExps","forbiddenTexts","normalizeCodeRule","rule","pattern","regExp","snippet","text","modelAnswer","strippedFiles","readSourceCodeWithoutComments","matched","forbiddenText","filePathSet","requiredPath","languageDefinition","grammar","removeCommentsInSourceCode","compiled","rules","fieldName","templatesDirectoryPath","entries","hasFiles","hasDirectories","directoryNames","templateDirectoryNames","templateDirectoryName","templateFiles","templateContentByPath","judgedFiles"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { MAX_COMPARED_FILE_BYTES } from '../helpers/compareExpectedOutputFiles.js';\nimport { findDefaultStdioHarnessFiles, type HarnessFileName } from '../helpers/defaultStdioHarness.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { judgesTestCasesWithoutExpectations } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { removeCommentsInSourceCode } from '../helpers/removeCommentsInSourceCode.js';\nimport { EXAMPLE_TEST_CASE_ID_PATTERN, MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { type CodeRule, normalizeCodeRule } from '../types/problem.js';\n\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n readSourceFilesRecursively,\n type SourceFile,\n} from './fsHelpers.js';\nimport {\n availableLanguageIds,\n LEARNING_MATERIAL_ID_REGEX,\n problemFrontmatterSchema,\n type ProblemFrontmatter,\n} from './schemas.js';\nimport { reportExcessChatMarkers } from './validateMaterial.js';\nimport { formatZodIssues, type ValidationResult } from './validationResult.js';\n\nconst RECOMMENDED_MIN_TEST_CASE_COUNT = 4;\n// Forbidden patterns must not restrict classes/functions commonly needed just to parse stdin (from gen-em).\n// Instead of a substring check on the pattern source (\"print\" contains \"int\"), each forbidden\n// pattern is applied to representative parsing code; a hit means the pattern would forbid parsing.\nconst ALLOWED_PARSING_HELPERS = ['Scanner', 'map', 'int', 'split'] as const;\nconst PARSING_CODE_SNIPPETS = [\n 'a, b = map(int, input().split())',\n 'Scanner scanner = new Scanner(System.in);',\n \"const [a, b] = line.split(' ').map(Number);\",\n] as const;\n\ninterface FileTestCase {\n id: string;\n stdin?: string;\n stdout?: string;\n /** Whether `<id>.fin/` (input files copied into the working directory) exists and holds at least one file. */\n hasFileInput: boolean;\n /** Whether `<id>.fout/` (expected output files) exists and holds at least one file. */\n hasFileOutput: boolean;\n /** Whether `<id>.json` (configuration read by a custom `judge.ts`) exists. */\n hasJudgeConfig: boolean;\n}\n\n// The importer rejects a longer expected stdout whichever judge the problem has.\nconst IMPORTER_MAX_STDOUT_LENGTH = 100_000;\n// `_shared.fin/` holds files copied for every test case; it is not a test case itself.\nconst SHARED_FILE_INPUT_NAME = '_shared';\n// The importer leaves generated artifacts out of the packaged problem, so a directory holding only\n// these is as good as empty. Any other file counts, even a `.gitkeep` placeholder.\nconst IGNORED_TEST_CASE_ENTRY_NAMES: ReadonlySet<string> = new Set(['.DS_Store', '__pycache__']);\n\ninterface ModelAnswer {\n id: string;\n files: SourceFile[];\n}\n\n/**\n * Validates a judge v2 problem directory (problem.md, test_cases, model_answers, templates, and —\n * for custom judging or custom debugging only — judge.ts/debug.ts).\n */\nexport async function validateProblemDirectory(problemDirectoryPath: string): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const absoluteDirectoryPath = resolve(problemDirectoryPath);\n // Problem discovery does not traverse a symbolic link, so a linked problem directory is never imported.\n if (!(await isRegularDirectory(absoluteDirectoryPath))) {\n errors.push(\n (await isDirectory(absoluteDirectoryPath))\n ? `problem directory is a symbolic link, which the judge does not traverse: ${problemDirectoryPath}`\n : `problem directory not found: ${problemDirectoryPath}`\n );\n return result;\n }\n\n const problemId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(`problem ID \"${problemId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n // The judge's front-matter reader takes the first `problem.md` or `*.problem.md` it finds, so a\n // leftover v1 file can be judged instead of problem.md.\n // The judge readers enumerate regular files and directories only, so a symbolic link anywhere in\n // the problem is ignored at import time and would silently drop or change what is judged.\n for (const linkPath of await listSymbolicLinks(absoluteDirectoryPath)) {\n errors.push(\n `${relative(absoluteDirectoryPath, linkPath)} is a symbolic link, which the judge ignores; commit a real file or directory`\n );\n }\n const problemDirents = await readdir(absoluteDirectoryPath, { withFileTypes: true });\n for (const dirent of problemDirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (dirent.isFile() && dirent.name.endsWith('.problem.md')) {\n errors.push(`found v1 problem file ${dirent.name}; use the v2 layout with a file named exactly problem.md`);\n }\n }\n\n const problemFilePath = join(absoluteDirectoryPath, 'problem.md');\n // The judge discovers problems by their problem.md and skips symbolic links (reported above).\n if (!(await isRegularFile(problemFilePath))) {\n if (!(await isFile(problemFilePath))) {\n errors.push('problem.md not found; a v2 problem directory must contain problem.md');\n }\n return result;\n }\n\n const frontmatter = await parseProblemFrontmatter(problemFilePath, errors);\n const harness = await readHarnessFiles(absoluteDirectoryPath);\n const requiresExpectedOutput = frontmatter === undefined || !judgesTestCasesWithoutExpectations(frontmatter);\n const fileTestCases = await readFileTestCases(\n absoluteDirectoryPath,\n { hasCustomJudgeTs: harness.hasCustomJudgeTs, requiresExpectedOutput },\n errors,\n warnings\n );\n // A default judge.ts is about to be deleted, so treat only a genuinely custom judge.ts as a\n // special judge; otherwise the case-less error would surface only after the deletion.\n validateTestCasePresence(frontmatter, fileTestCases, harness.hasCustomJudgeTs, errors, warnings);\n\n // A problem whose frontmatter failed to parse already has a primary error; deriving secondary\n // judge.ts/model-answer errors from it would send the fix loop after the wrong problems.\n const isScoredAutomatically =\n frontmatter !== undefined && frontmatter.isManualScoringRequired !== true && frontmatter.type !== 'prompt_study';\n validateHarnessFiles(harness, frontmatter !== undefined, errors, warnings);\n\n const modelAnswers = await readModelAnswers(absoluteDirectoryPath, errors, warnings);\n // Every model answer directory is judged by the all-problem check, so each one must hold what\n // its judge runs: a custom judge defines that itself (e.g. a CSV submission), while the stdio\n // judge needs a non-empty file of a supported language as its entry point.\n const usableModelAnswers = modelAnswers.filter((answer) => {\n // A custom judge may accept an empty data file, so only the stdio judge needs content.\n if (harness.hasCustomJudgeTs) return true;\n const isRunnable = answer.files.some(\n (file) => findLanguageDefinitionByPath(file.path) !== undefined && file.data.trim().length > 0\n );\n if (!isRunnable) {\n errors.push(\n `model answer directory \"${answer.id}\" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`\n );\n }\n return isRunnable;\n });\n if (isScoredAutomatically && modelAnswers.length === 0) {\n errors.push('no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files');\n }\n if (frontmatter) {\n validateCodePatterns(frontmatter, usableModelAnswers, errors, warnings);\n validateRequiredSubmissionFiles(frontmatter, usableModelAnswers, errors);\n }\n await validateTemplates(absoluteDirectoryPath, usableModelAnswers, harness.hasCustomJudgeTs, errors, warnings);\n\n return result;\n}\n\ninterface HarnessFiles {\n hasDebugTs: boolean;\n rejectedDefaultHarnessFileNames: readonly HarnessFileName[];\n hasCustomJudgeTs: boolean;\n}\n\nasync function readHarnessFiles(absoluteDirectoryPath: string): Promise<HarnessFiles> {\n const hasJudgeTs = await isFile(join(absoluteDirectoryPath, 'judge.ts'));\n // The same policy the `judge` subcommand and the all-problem check apply: a default judge.ts is\n // always rejected, and a default debug.ts is rejected unless it accompanies a custom judge.ts.\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(absoluteDirectoryPath);\n return {\n hasDebugTs: await isFile(join(absoluteDirectoryPath, 'debug.ts')),\n rejectedDefaultHarnessFileNames: defaultHarnessFileNames,\n hasCustomJudgeTs: hasJudgeTs && !defaultHarnessFileNames.includes('judge.ts'),\n };\n}\n\n/**\n * The judge treats a problem without judge.ts as a standard stdin/stdout problem and auto-generates\n * both the stdio judge and the stdio debug runner. Committed copies of those default harnesses are\n * rejected: they add nothing and can drift from the server's defaults, and a default judge.ts also\n * hides the standard-problem marker that enables the default debug runner.\n */\nfunction validateHarnessFiles(\n harness: HarnessFiles,\n hasParsedFrontmatter: boolean,\n errors: string[],\n warnings: string[]\n): void {\n if (harness.rejectedDefaultHarnessFileNames.includes('judge.ts')) {\n errors.push(\n 'judge.ts only calls stdioJudgePreset; delete it — the judge auto-generates it, and only a problem without judge.ts is treated as a standard stdio problem (which enables the default debug runner)'\n );\n }\n if (harness.rejectedDefaultHarnessFileNames.includes('debug.ts')) {\n errors.push(\n 'debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts'\n );\n }\n // A custom debug.ts without judge.ts stays accepted: the judge runs debug.ts whenever it is\n // present (judge.ts only selects the judging side), so it is a supported way to customize\n // debugging for a problem judged by the default stdio judge.\n // The debug feature itself is independent of the scoring mode, so warn for manual-scoring and\n // prompt_study problems too.\n if (hasParsedFrontmatter && harness.hasCustomJudgeTs && !harness.hasDebugTs) {\n warnings.push(\n \"custom judge.ts without debug.ts; the debug feature is unavailable for this problem — add debug.ts (e.g. calling stdioDebugPreset from '@exercode/problem-utils/presets/stdio') or a custom debug harness\"\n );\n }\n}\n\nasync function parseProblemFrontmatter(\n problemFilePath: string,\n errors: string[]\n): Promise<ProblemFrontmatter | undefined> {\n let attributes: unknown;\n try {\n let body: string;\n ({ attributes, body } = parseFrontmatter(await readFile(problemFilePath, 'utf8')));\n // exercode rejects problem bodies with more than one standalone chat delimiter, like materials.\n reportExcessChatMarkers(body, errors);\n } catch (error) {\n errors.push(`problem.md: invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return undefined;\n }\n const parsed = problemFrontmatterSchema.safeParse(attributes);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'problem.md frontmatter'));\n return undefined;\n }\n return parsed.data;\n}\n\nasync function readFileTestCases(\n problemDirectoryPath: string,\n { hasCustomJudgeTs, requiresExpectedOutput }: { hasCustomJudgeTs: boolean; requiresExpectedOutput: boolean },\n errors: string[],\n warnings: string[]\n): Promise<FileTestCase[]> {\n const testCasesDirectoryPath = join(problemDirectoryPath, 'test_cases');\n if (!(await isDirectory(testCasesDirectoryPath))) return [];\n\n const dirents = await readdir(testCasesDirectoryPath, { withFileTypes: true });\n const idToTestCase = new Map<string, FileTestCase>();\n const getTestCase = (testCaseId: string): FileTestCase => {\n let testCase = idToTestCase.get(testCaseId);\n if (!testCase) {\n testCase = {\n id: testCaseId,\n hasFileInput: false,\n hasFileOutput: false,\n hasJudgeConfig: false,\n };\n idToTestCase.set(testCaseId, testCase);\n }\n return testCase;\n };\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n // Symbolic links were already reported for the whole problem directory.\n if (dirent.isSymbolicLink()) continue;\n // `_shared` is reserved: only `_shared.fin/` (a directory of inputs copied for every case) exists.\n if (dirent.name === SHARED_FILE_INPUT_NAME || dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)) {\n if (dirent.name !== `${SHARED_FILE_INPUT_NAME}.fin`) {\n errors.push(`test_cases/${dirent.name} is not supported; only _shared.fin/ is shared across test cases`);\n } else if (!dirent.isDirectory()) {\n errors.push(`test_cases/${dirent.name} must be a directory`);\n } else {\n const sharedFilePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (sharedFilePaths.length === 0) {\n errors.push(`test_cases/${dirent.name}/ is empty; put at least one shared input file in it`);\n }\n }\n continue;\n }\n const testCaseId = dirent.name.replace(/\\.(fin|fout)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${dirent.name} must have a test case ID before its extension`);\n continue;\n }\n if (testCaseId === dirent.name) {\n // A directory named like a file entry is a wrong-typed entry, not an ignorable stranger.\n if (dirent.isDirectory() && /\\.(in|out|json)$/.test(dirent.name)) {\n errors.push(`test_cases/${dirent.name} must be a file, not a directory`);\n }\n continue;\n }\n if (!dirent.isDirectory()) {\n errors.push(`test case ${testCaseId}: test_cases/${dirent.name} must be a directory`);\n continue;\n }\n // git does not track an empty directory, so an empty `.fin/` or `.fout/` would silently vanish on\n // commit, and the importer counts only real files that are not generated artifacts (an empty\n // subdirectory does not count); such a directory is an error and does not make the id a test case.\n const filePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (filePaths.length === 0) {\n errors.push(`test case ${testCaseId}: ${dirent.name}/ is empty; put at least one file in it or delete it`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n if (dirent.name.endsWith('.fin')) {\n testCase.hasFileInput = true;\n continue;\n }\n testCase.hasFileOutput = true;\n // The importer keeps a placeholder like any file, so the stdio judge would require the program to write it.\n for (const filePath of filePaths) {\n if (basename(filePath) === '.gitkeep') {\n warnings.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is compared as an expected output file; delete the placeholder`\n );\n }\n }\n // The stdio judge compares each expected file and refuses one above its limit (the importer rejects\n // the problem); a custom judge.ts reads the directory under its own contract.\n if (!hasCustomJudgeTs) {\n for (const filePath of filePaths) {\n const fileStats = await stat(filePath);\n if (fileStats.size > MAX_COMPARED_FILE_BYTES) {\n errors.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is larger than ${MAX_COMPARED_FILE_BYTES} bytes, which the stdio judge cannot compare`\n );\n }\n }\n }\n }\n // `_shared.*` files were already reported by the entry loop above.\n const fileNames = dirents\n .filter(\n (dirent) =>\n dirent.isFile() && /\\.(in|out|json)$/.test(dirent.name) && !dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)\n )\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const testCaseId = fileName.replace(/\\.(in|out|json)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${fileName} must have a test case ID before its extension`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n const content = await readFile(join(testCasesDirectoryPath, fileName), 'utf8');\n if (fileName.endsWith('.in')) {\n testCase.stdin = content.trimEnd();\n } else if (fileName.endsWith('.out')) {\n testCase.stdout = content.trimEnd();\n } else {\n // The presets ignore `.json`; only a custom judge.ts reads it, with a shape of its own.\n testCase.hasJudgeConfig = true;\n if (!hasCustomJudgeTs) {\n errors.push(\n `test case ${testCaseId}: ${fileName} is configuration for a custom judge.ts, which this problem does not have`\n );\n }\n try {\n JSON.parse(content);\n } catch {\n errors.push(`test case ${testCaseId}: ${fileName} is not valid JSON`);\n }\n }\n }\n\n // Directories and files are read in separate passes, so sort for deterministic per-case diagnostics below\n // (the entry-level diagnostics above follow the entry-name order of their own passes).\n const testCases = [...idToTestCase.values()].toSorted((t1, t2) => t1.id.localeCompare(t2.id));\n // A custom judge defines its own contract (e.g. it renders an output file from the input and\n // ships `.in` only), but it usually treats every case alike, so a partial set of expectations\n // still hints at a forgotten file. Input is optional in every form, so nothing guesses at it.\n // `.json` is configuration, not an output expectation, so it neither counts nor is warned about.\n const hasAnyExpectedOutput = testCases.some(hasExpectedOutput);\n for (const testCase of testCases) {\n // An empty `.fout/` was already reported above and does not count as an expectation, so a case that\n // exists through another entry is also reported here. A `.json` case without judge.ts was already\n // rejected as unsupported configuration.\n if (!hasExpectedOutput(testCase) && !testCase.hasJudgeConfig) {\n // The default stdio harness rejects such a case, so it is an error without judge.ts.\n if (!hasCustomJudgeTs) {\n if (!requiresExpectedOutput) continue;\n errors.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`\n );\n } else if (hasAnyExpectedOutput) {\n warnings.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing while other cases have one`\n );\n }\n }\n // The stdio judge rejects a run whose raw stdout exceeds the limit, so a longer expectation can\n // never match (a program may still print exactly the limit without a trailing newline).\n // A custom judge.ts reads `.out` under its own contract, so only the importer's limit applies to it.\n // The importer measures the trimmed expectation. The stdio judge measures the raw output a run\n // prints; a run matching a trimmed expectation prints at least that many characters (the token\n // comparison ignores trailing whitespace), so only a longer trimmed expectation is impossible.\n if (testCase.stdout !== undefined) {\n const maxStdoutLength = hasCustomJudgeTs ? IMPORTER_MAX_STDOUT_LENGTH : MAX_STDOUT_LENGTH;\n if (testCase.stdout.length > maxStdoutLength) {\n errors.push(\n hasCustomJudgeTs\n ? `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length} > ${maxStdoutLength})`\n : `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length}); the stdio judge rejects a run that prints more than ${maxStdoutLength} characters`\n );\n }\n }\n // The judge accepts an empty `.out` (the program must print nothing or an empty line), but it is\n // usually a stray file of a case whose stdout is not compared.\n if (testCase.stdout !== undefined && testCase.stdout.length === 0) {\n warnings.push(\n `test case ${testCase.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`\n );\n }\n }\n\n // Only stdin is compared: a case with input files may legitimately share its stdin with others,\n // and file contents are not compared for duplicates.\n const stdinToFirstId = new Map<string, string>();\n for (const testCase of testCases) {\n // An empty `.in` means \"no input\" (like an absent one) and is not a duplicate of another empty one.\n if (!testCase.stdin || testCase.hasFileInput) continue;\n const firstId = stdinToFirstId.get(testCase.stdin);\n if (firstId === undefined) {\n stdinToFirstId.set(testCase.stdin, testCase.id);\n } else {\n warnings.push(`test cases ${firstId} and ${testCase.id} have identical input`);\n }\n }\n return testCases;\n}\n\nasync function listSymbolicLinks(directoryPath: string): Promise<string[]> {\n const linkPaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isSymbolicLink()) {\n linkPaths.push(entryPath);\n } else if (dirent.isDirectory() && dirent.name !== 'node_modules') {\n linkPaths.push(...(await listSymbolicLinks(entryPath)));\n }\n }\n return linkPaths.toSorted((p1, p2) => p1.localeCompare(p2));\n}\n\n/** Lists the real files under a test case directory, leaving out generated artifacts like the importer. */\nasync function listFilesRecursively(directoryPath: string): Promise<string[]> {\n const filePaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n if (IGNORED_TEST_CASE_ENTRY_NAMES.has(dirent.name)) continue;\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isFile()) {\n filePaths.push(entryPath);\n } else if (dirent.isDirectory()) {\n filePaths.push(...(await listFilesRecursively(entryPath)));\n }\n }\n return filePaths;\n}\n\nfunction hasExpectedOutput(testCase: FileTestCase): boolean {\n return testCase.stdout !== undefined || testCase.hasFileOutput;\n}\n\nfunction validateTestCasePresence(\n frontmatter: ProblemFrontmatter | undefined,\n fileTestCases: FileTestCase[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): void {\n // Exercode's import requires test cases, a judge.ts or isManualScoringRequired and grants no\n // exemption for `type: prompt_study` or static-analysis rules (this package's `judge` subcommand\n // runs a rules-only problem, but it cannot be imported).\n if (fileTestCases.length === 0 && frontmatter?.isManualScoringRequired !== true) {\n // The judge only rejects a case-less problem when judge.ts is also missing; a special judge\n // may compute results itself, but the stdio presets need test_cases/, hence the warning.\n if (hasCustomJudgeTs) {\n warnings.push(\n 'no test cases found; fine for a special judge, but stdio-preset judges require an expected output (test_cases/<id>.out or <id>.fout/) for every case'\n );\n } else {\n errors.push(\n 'no test cases found; add test cases with an expected output (test_cases/<id>.out or <id>.fout/, plus <id>.in or <id>.fin/ when the program reads input; at least one example_* and one hidden test_*), or set isManualScoringRequired: true'\n );\n }\n }\n if (fileTestCases.length > 0) {\n reportTestCaseCompositionIssues(fileTestCases, errors, warnings);\n }\n}\n\nfunction reportTestCaseCompositionIssues(\n testCases: readonly { id: string }[],\n errors: string[],\n warnings: string[]\n): void {\n if (!testCases.some((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push('no example test case found; add at least one test case whose ID contains \"example\" (e.g. example_1)');\n }\n if (testCases.every((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push(\n 'no hidden test case found; add at least one test case whose ID does not contain \"example\" (e.g. test_1)'\n );\n }\n if (testCases.length < RECOMMENDED_MIN_TEST_CASE_COUNT) {\n warnings.push(\n `only ${testCases.length} test cases found; ${RECOMMENDED_MIN_TEST_CASE_COUNT} or more are recommended`\n );\n }\n}\n\nasync function readModelAnswers(\n problemDirectoryPath: string,\n errors: string[],\n warnings: string[]\n): Promise<ModelAnswer[]> {\n const modelAnswersDirectoryPath = join(problemDirectoryPath, 'model_answers');\n if (!(await isDirectory(modelAnswersDirectoryPath))) return [];\n\n const modelAnswers: ModelAnswer[] = [];\n const dirents = await readdir(modelAnswersDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (!dirent.isDirectory() || dirent.name === '__pycache__') continue;\n // The judge parses each directory name with learningMaterialIdSchema, so a regex-invalid\n // name (e.g. \"Python\") aborts import, while an unknown but valid ID (e.g. \"foo\") imports fine.\n if (!LEARNING_MATERIAL_ID_REGEX.test(dirent.name)) {\n errors.push(`model answer directory name \"${dirent.name}\" must match ${LEARNING_MATERIAL_ID_REGEX}`);\n continue;\n }\n if (!(availableLanguageIds as readonly string[]).includes(dirent.name)) {\n warnings.push(\n `model answer directory name \"${dirent.name}\" is not a known language ID (${availableLanguageIds.join(', ')})`\n );\n }\n const files = await readSourceFilesRecursively(join(modelAnswersDirectoryPath, dirent.name));\n // The all-problem check judges every model answer directory, so an empty one fails there.\n if (files.length === 0) {\n errors.push(`model answer directory \"${dirent.name}\" has no source files; add files or delete it`);\n continue;\n }\n modelAnswers.push({ id: dirent.name, files });\n }\n return modelAnswers;\n}\n\nfunction validateCodePatterns(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[],\n warnings: string[]\n): void {\n const requiredRegExps = compilePatterns(frontmatter.requiredRegExpsInCode, 'requiredRegExpsInCode', errors);\n const forbiddenRegExps = compilePatterns(frontmatter.forbiddenRegExpsInCode, 'forbiddenRegExpsInCode', errors);\n const forbiddenTexts = frontmatter.forbiddenTextsInCode.map((rule) => normalizeCodeRule(rule).pattern);\n\n for (const [pattern, regExp] of forbiddenRegExps) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => regExp.test(snippet))) {\n warnings.push(\n `forbiddenRegExpsInCode pattern \"${pattern}\" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n for (const text of forbiddenTexts) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => snippet.includes(text))) {\n warnings.push(\n `forbiddenTextsInCode text \"${text}\" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n\n for (const modelAnswer of modelAnswers) {\n const strippedFiles = readSourceCodeWithoutComments(modelAnswer.files);\n\n for (const [pattern, regExp] of requiredRegExps) {\n if (!strippedFiles.some((file) => regExp.test(file.data))) {\n errors.push(\n `required pattern \"${pattern}\" does not match any file of model answer \"${modelAnswer.id}\" (comments are ignored)`\n );\n }\n }\n for (const [pattern, regExp] of forbiddenRegExps) {\n for (const file of strippedFiles) {\n const matched = file.data.match(regExp);\n if (matched) {\n errors.push(\n `forbidden pattern \"${pattern}\" matches model answer file ${modelAnswer.id}/${file.path} (matched \"${matched[0]}\")`\n );\n }\n }\n }\n for (const forbiddenText of forbiddenTexts) {\n for (const file of strippedFiles) {\n if (file.data.includes(forbiddenText)) {\n errors.push(`forbidden text \"${forbiddenText}\" appears in model answer file ${modelAnswer.id}/${file.path}`);\n }\n }\n }\n }\n}\n\n/**\n * The judge rejects a submission missing a required file before running it, so every model answer\n * must ship them among the files the importer keeps (the same filter as `readSourceFilesRecursively`).\n */\nfunction validateRequiredSubmissionFiles(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[]\n): void {\n for (const modelAnswer of modelAnswers) {\n const filePathSet = new Set(modelAnswer.files.map((file) => file.path));\n for (const requiredPath of frontmatter.requiredSubmissionFilePaths) {\n if (!filePathSet.has(requiredPath)) {\n errors.push(\n `required submission file \"${requiredPath}\" is missing from model answer \"${modelAnswer.id}\" (requiredSubmissionFilePaths)`\n );\n }\n }\n }\n}\n\n/** Selects and strips the files exactly like the judge's static analysis of a submission does. */\nfunction readSourceCodeWithoutComments(files: SourceFile[]): SourceFile[] {\n return files.flatMap((file) => {\n const languageDefinition = findLanguageDefinitionByPath(file.path);\n if (!languageDefinition || file.data.includes('\\uFFFD')) return [];\n const grammar = languageDefinition.grammer;\n return [{ path: file.path, data: grammar ? removeCommentsInSourceCode(grammar, file.data) : file.data }];\n });\n}\n\nfunction compilePatterns(rules: CodeRule[], fieldName: string, errors: string[]): [string, RegExp][] {\n const compiled: [string, RegExp][] = [];\n for (const rule of rules) {\n const { pattern } = normalizeCodeRule(rule);\n try {\n compiled.push([pattern, new RegExp(pattern)]);\n } catch {\n errors.push(`invalid regular expression in ${fieldName}: ${pattern}`);\n }\n }\n return compiled;\n}\n\nasync function validateTemplates(\n problemDirectoryPath: string,\n modelAnswers: ModelAnswer[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const templatesDirectoryPath = join(problemDirectoryPath, 'templates');\n if (!(await isDirectory(templatesDirectoryPath))) return;\n\n const dirents = await readdir(templatesDirectoryPath, { withFileTypes: true });\n // The judge only skips __pycache__; a stray .DS_Store next to template directories fails\n // import as a mixed files-and-directories layout, so it must not be filtered out here.\n const entries = dirents.filter((dirent) => dirent.name !== '__pycache__');\n if (entries.some((dirent) => dirent.name === '.DS_Store')) {\n warnings.push('templates/.DS_Store found; delete it (the judge treats it as a template file)');\n }\n const hasFiles = entries.some((dirent) => dirent.isFile());\n const hasDirectories = entries.some((dirent) => dirent.isDirectory());\n if (hasFiles && hasDirectories) {\n errors.push(\n 'templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)'\n );\n return;\n }\n for (const dirent of entries) {\n if (\n dirent.isDirectory() &&\n dirent.name !== '_default' &&\n !(availableLanguageIds as readonly string[]).includes(dirent.name)\n ) {\n errors.push(\n `invalid template directory name \"${dirent.name}\"; expected a language ID (${availableLanguageIds.join(', ')}) or _default`\n );\n }\n }\n // A template must be an incomplete starting point: a template directory that carries every file\n // a model answer is judged by, unchanged, would pass the judge. The stdio judge runs the source\n // files of a supported language (notes next to the answer do not change what runs), while a\n // custom judge.ts may read any file (e.g. a CSV submission). Helper modules shared by both are fine.\n const directoryNames = entries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);\n const templateDirectoryNames = hasDirectories ? directoryNames : [''];\n for (const templateDirectoryName of templateDirectoryNames) {\n const templateFiles = await readSourceFilesRecursively(join(templatesDirectoryPath, templateDirectoryName));\n const templateContentByPath = new Map(templateFiles.map((file) => [file.path, file.data]));\n for (const modelAnswer of modelAnswers) {\n const judgedFiles = modelAnswer.files.filter(\n (file) =>\n hasCustomJudgeTs || (file.data.trim().length > 0 && findLanguageDefinitionByPath(file.path) !== undefined)\n );\n if (judgedFiles.length > 0 && judgedFiles.every((file) => templateContentByPath.get(file.path) === file.data)) {\n warnings.push(\n `templates/${templateDirectoryName} contains every judged file of model answer \"${modelAnswer.id}\" unchanged (identical to a model answer file); templates must not pass the judge`\n );\n }\n }\n }\n}\n"],"mappings":"2rCA8BA,MAIMA,EAA0B,CAAC,UAAW,MAAO,MAAO,OAAO,EAC3DC,EAAwB,CAC5B,mCACA,4CACA,6CACF,EAiBMC,EAAyB,UAGzBC,kBAAqD,IAAI,IAAI,CAAC,YAAa,aAAa,CAAC,EAW/F,eAAsBC,EAAyB,EAAyD,CACtG,IAAMC,EAAmB,CAAC,EACpBC,EAAqB,CAAC,EACtBC,EAAS,CAAE,SAAQ,UAAS,EAE5BC,EAAwBC,EAAQC,CAAoB,EAE1D,GAAI,CAAE,MAAMC,EAAmBH,CAAqB,EAMlD,OALAH,EAAO,KACJ,MAAMO,EAAYJ,CAAqB,EACpC,4EAA4EE,IAC5E,gCAAgCA,GACtC,EACOH,EAGT,IAAMM,EAAYC,EAASN,CAAqB,EAC3CO,EAA2B,KAAKF,CAAS,GAC5CR,EAAO,KAAK,eAAeQ,EAAU,gCAAgCE,GAA4B,EAMnG,IAAK,IAAMC,KAAY,MAAMC,EAAkBT,CAAqB,EAClEH,EAAO,KACL,GAAGa,EAASV,EAAuBQ,CAAQ,EAAE,8EAC/C,EAEF,IAAMG,EAAiB,MAAMC,EAAQZ,EAAuB,CAAE,cAAe,EAAK,CAAC,EACnF,IAAK,IAAMa,KAAUF,EAAe,UAAU,EAAI,IAAOG,EAAG,KAAK,cAAcC,EAAG,IAAI,CAAC,EACjFF,EAAO,OAAO,GAAKA,EAAO,KAAK,SAAS,aAAa,GACvDhB,EAAO,KAAK,yBAAyBgB,EAAO,KAAK,yDAAyD,EAI9G,IAAMG,EAAkBC,EAAKjB,EAAuB,YAAY,EAEhE,GAAI,CAAE,MAAMkB,EAAcF,CAAe,EAIvC,OAHM,MAAMG,EAAOH,CAAe,GAChCnB,EAAO,KAAK,sEAAsE,EAE7EE,EAGT,IAAMqB,EAAc,MAAMC,EAAwBL,EAAiBnB,CAAM,EACnEyB,EAAU,MAAMC,EAAiBvB,CAAqB,EACtDwB,EAAyBJ,IAAgB,IAAA,IAAa,CAACK,EAAmCL,CAAW,EAS3GM,EAAyBN,EAAa,MARVO,EAC1B3B,EACA,CAAE,iBAAkBsB,EAAQ,iBAAkB,wBAAuB,EACrEzB,EACAC,CACF,EAGqDwB,EAAQ,iBAAkBzB,EAAQC,CAAQ,EAI/F,IAAM8B,EACJR,IAAgB,IAAA,IAAaA,EAAY,0BAA4B,IAAQA,EAAY,OAAS,eACpGS,EAAqBP,EAASF,IAAgB,IAAA,GAAWvB,EAAQC,CAAQ,EAEzE,IAAMgC,EAAe,MAAMC,EAAiB/B,EAAuBH,EAAQC,CAAQ,EAI7EkC,EAAqBF,EAAa,OAAQ,GAAW,CAEzD,GAAIR,EAAQ,iBAAkB,MAAO,GACrC,IAAMW,EAAaC,EAAO,MAAM,KAC7B,GAASC,EAA6BC,EAAK,IAAI,IAAM,IAAA,IAAaA,EAAK,KAAK,KAAK,CAAC,CAAC,OAAS,CAC/F,EAMA,OALKH,GACHpC,EAAO,KACL,2BAA2BqC,EAAO,GAAG,uGACvC,EAEKD,CACT,CAAC,EAUD,OATIL,GAAyBE,EAAa,SAAW,GACnDjC,EAAO,KAAK,qGAAqG,EAE/GuB,IACFiB,EAAqBjB,EAAaY,EAAoBnC,EAAQC,CAAQ,EACtEwC,EAAgClB,EAAaY,EAAoBnC,CAAM,GAEzE,MAAM0C,EAAkBvC,EAAuBgC,EAAoBV,EAAQ,iBAAkBzB,EAAQC,CAAQ,EAEtGC,CACT,CAQA,eAAewB,EAAiB,EAAsD,CACpF,IAAMiB,EAAa,MAAMrB,EAAOF,EAAKjB,EAAuB,UAAU,CAAC,EAGjEyC,EAA0B,MAAMC,EAA6B1C,CAAqB,EACxF,MAAO,CACL,WAAY,MAAMmB,EAAOF,EAAKjB,EAAuB,UAAU,CAAC,EAChE,gCAAiCyC,EACjC,iBAAkBD,GAAc,CAACC,EAAwB,SAAS,UAAU,CAC9E,CACF,CAQA,SAASZ,EACP,EACA,EACA,EACA,EACM,CACFP,EAAQ,gCAAgC,SAAS,UAAU,GAC7DzB,EAAO,KACL,oMACF,EAEEyB,EAAQ,gCAAgC,SAAS,UAAU,GAC7DzB,EAAO,KACL,6GACF,EAOE8C,GAAwBrB,EAAQ,kBAAoB,CAACA,EAAQ,YAC/DxB,EAAS,KACP,2MACF,CAEJ,CAEA,eAAeuB,EACb,EACA,EACyC,CACzC,IAAIuB,EACJ,GAAI,CACF,IAAIC,GACH,sBAAuBC,EAAiB,MAAMC,EAAS/B,EAAiB,MAAM,CAAC,GAEhFgC,EAAwBH,EAAMhD,CAAM,CACtC,OAASoD,EAAO,CACdpD,EAAO,KAAK,yCAAyCqD,EAAgBD,CAAK,GAAG,EAC7E,MACF,CACA,IAAME,EAASC,EAAyB,UAAUR,CAAU,EAC5D,GAAI,CAACO,EAAO,QACVtD,EAAO,KAAK,GAAGwD,EAAgBF,EAAO,MAAO,wBAAwB,CAAC,OAGxE,OAAOA,EAAO,IAChB,CAEA,eAAexB,EACb,EACA,CAAE,mBAAkB,0BACpB,EACA,EACyB,CACzB,IAAM2B,EAAyBrC,EAAKf,EAAsB,YAAY,EACtE,GAAI,CAAE,MAAME,EAAYkD,CAAsB,EAAI,MAAO,CAAC,EAE1D,IAAMC,EAAU,MAAM3C,EAAQ0C,EAAwB,CAAE,cAAe,EAAK,CAAC,EACvEE,kBAAe,IAAI,IACnBC,EAAe,GAAqC,CACxD,IAAIC,EAAWF,EAAa,IAAIG,CAAU,EAU1C,OATKD,IACH,EAAW,CACT,GAAIC,EACJ,aAAc,GACd,cAAe,GACf,eAAgB,EAClB,EACAH,EAAa,IAAIG,EAAYD,CAAQ,GAEhCA,CACT,EACA,IAAK,IAAM7C,KAAU0C,EAAQ,UAAU,EAAI,IAAOzC,EAAG,KAAK,cAAcC,EAAG,IAAI,CAAC,EAAG,CAEjF,GAAIF,EAAO,eAAe,EAAG,SAE7B,GAAIA,EAAO,OAASnB,GAA0BmB,EAAO,KAAK,WAAW,GAAGnB,EAAuB,EAAE,EAAG,CAC9FmB,EAAO,OAAS,GAAGnB,EAAuB,MAElCmB,EAAO,YAAY,GAIzB,MAD0B+C,EAAqB3C,EAAKqC,EAAwBzC,EAAO,IAAI,CAAC,EAAA,CACxE,SAAW,GAC7BhB,EAAO,KAAK,cAAcgB,EAAO,KAAK,qDAAqD,EAJ7FhB,EAAO,KAAK,cAAcgB,EAAO,KAAK,qBAAqB,EAF3DhB,EAAO,KAAK,cAAcgB,EAAO,KAAK,iEAAiE,EASzG,QACF,CACA,IAAM8C,EAAa9C,EAAO,KAAK,QAAQ,gBAAiB,EAAE,EAC1D,GAAI8C,IAAe,GAAI,CACrB9D,EAAO,KAAK,cAAcgB,EAAO,KAAK,+CAA+C,EACrF,QACF,CACA,GAAI8C,IAAe9C,EAAO,KAAM,CAE1BA,EAAO,YAAY,GAAK,mBAAmB,KAAKA,EAAO,IAAI,GAC7DhB,EAAO,KAAK,cAAcgB,EAAO,KAAK,iCAAiC,EAEzE,QACF,CACA,GAAI,CAACA,EAAO,YAAY,EAAG,CACzBhB,EAAO,KAAK,aAAa8D,EAAW,eAAe9C,EAAO,KAAK,qBAAqB,EACpF,QACF,CAIA,IAAMgD,EAAY,MAAMD,EAAqB3C,EAAKqC,EAAwBzC,EAAO,IAAI,CAAC,EACtF,GAAIgD,EAAU,SAAW,EAAG,CAC1BhE,EAAO,KAAK,aAAa8D,EAAW,IAAI9C,EAAO,KAAK,qDAAqD,EACzG,QACF,CACA,IAAM6C,EAAWD,EAAYE,CAAU,EACvC,GAAI9C,EAAO,KAAK,SAAS,MAAM,EAC7B,EAAS,aAAe,OAG1B,GAAS,cAAgB,GAEzB,IAAK,IAAMiD,KAAYD,EACjBvD,EAASwD,CAAQ,IAAM,YACzBhE,EAAS,KACP,aAAa6D,EAAW,IAAI9C,EAAO,KAAK,GAAGH,EAASO,EAAKqC,EAAwBzC,EAAO,IAAI,EAAGiD,CAAQ,EAAE,gEAC3G,EAKJ,GAAI,CAACC,EACE,IAAA,IAAMD,KAAYD,GAEjB,MADoBG,EAAKF,CAAQ,EAAA,CACvB,KAAA,SACZjE,EAAO,KACL,aAAa8D,EAAW,IAAI9C,EAAO,KAAK,GAAGH,EAASO,EAAKqC,EAAwBzC,EAAO,IAAI,EAAGiD,CAAQ,EAAE,kBAAkBG,EAAwB,6CACrJ,CAjBmB,CAqB3B,CAEA,IAAMC,EAAYX,EACf,OACE,GACC1C,EAAO,OAAO,GAAK,mBAAmB,KAAKA,EAAO,IAAI,GAAK,CAACA,EAAO,KAAK,WAAW,GAAGnB,EAAuB,EAAE,CACnH,CAAC,CACA,IAAK,GAAWmB,EAAO,IAAI,CAAC,CAC5B,UAAU,EAAI,IAAOsD,EAAG,cAAcC,CAAE,CAAC,EAC5C,IAAK,IAAMC,KAAYH,EAAW,CAChC,IAAMP,EAAaU,EAAS,QAAQ,mBAAoB,EAAE,EAC1D,GAAIV,IAAe,GAAI,CACrB9D,EAAO,KAAK,cAAcwE,EAAS,+CAA+C,EAClF,QACF,CACA,IAAMX,EAAWD,EAAYE,CAAU,EACjCW,EAAU,MAAMvB,EAAS9B,EAAKqC,EAAwBe,CAAQ,EAAG,MAAM,EAC7E,GAAIA,EAAS,SAAS,KAAK,EACzB,EAAS,MAAQC,EAAQ,QAAQ,OAC5B,GAAID,EAAS,SAAS,MAAM,EACjC,EAAS,OAASC,EAAQ,QAAQ,MAC7B,CAEL,EAAS,eAAiB,GACrBP,GACHlE,EAAO,KACL,aAAa8D,EAAW,IAAIU,EAAS,0EACvC,EAEF,GAAI,CACF,KAAK,MAAMC,CAAO,CACpB,MAAQ,CACNzE,EAAO,KAAK,aAAa8D,EAAW,IAAIU,EAAS,mBAAmB,CACtE,CACF,CACF,CAIA,IAAME,EAAY,CAAC,GAAGf,EAAa,OAAO,CAAC,CAAC,CAAC,UAAU,EAAI,IAAOgB,EAAG,GAAG,cAAcC,EAAG,EAAE,CAAC,EAKtFC,EAAuBH,EAAU,KAAKI,CAAiB,EAC7D,IAAK,IAAMjB,KAAYa,EAAW,CAIhC,GAAI,CAACI,EAAkBjB,CAAQ,GAAK,CAACA,EAAS,eAAgB,CAE5D,GAAKK,EAKMW,GACT5E,EAAS,KACP,aAAa4D,EAAS,GAAG,IAAIA,EAAS,GAAG,WAAWA,EAAS,GAAG,8CAClE,MARqB,CACrB,GAAI,CAAClC,EAAwB,SAC7B3B,EAAO,KACL,aAAa6D,EAAS,GAAG,IAAIA,EAAS,GAAG,WAAWA,EAAS,GAAG,4FAClE,CACF,CAKF,CAOA,GAAIA,EAAS,SAAW,IAAA,GAAW,CACjC,IAAMkB,EAAkBb,EAAmB,IAA6Bc,EACpEnB,EAAS,OAAO,OAASkB,GAC3B/E,EAAO,KACLkE,EACI,aAAaL,EAAS,GAAG,+BAA+BA,EAAS,OAAO,OAAO,KAAKkB,EAAgB,GACpG,aAAalB,EAAS,GAAG,+BAA+BA,EAAS,OAAO,OAAO,yDAAyDkB,EAAgB,YAC9J,CAEJ,CAGIlB,EAAS,SAAW,IAAA,IAAaA,EAAS,OAAO,SAAW,GAC9D5D,EAAS,KACP,aAAa4D,EAAS,GAAG,+HAC3B,CAEJ,CAIA,IAAMoB,kBAAiB,IAAI,IAC3B,IAAK,IAAMpB,KAAYa,EAAW,CAEhC,GAAI,CAACb,EAAS,OAASA,EAAS,aAAc,SAC9C,IAAMqB,EAAUD,EAAe,IAAIpB,EAAS,KAAK,EAC7CqB,IAAY,IAAA,GACdD,EAAe,IAAIpB,EAAS,MAAOA,EAAS,EAAE,EAE9C5D,EAAS,KAAK,cAAciF,EAAQ,OAAOrB,EAAS,GAAG,sBAAsB,CAEjF,CACA,OAAOa,CACT,CAEA,eAAe9D,EAAkB,EAA0C,CACzE,IAAMuE,EAAsB,CAAC,EAC7B,IAAK,IAAMnE,KAAU,MAAMD,EAAQqE,EAAe,CAAE,cAAe,EAAK,CAAC,EAAG,CAC1E,IAAMC,EAAYjE,EAAKgE,EAAepE,EAAO,IAAI,EAC7CA,EAAO,eAAe,EACxBmE,EAAU,KAAKE,CAAS,EACfrE,EAAO,YAAY,GAAKA,EAAO,OAAS,gBACjDmE,EAAU,KAAK,GAAI,MAAMvE,EAAkByE,CAAS,CAAE,CAE1D,CACA,OAAOF,EAAU,UAAU,EAAI,IAAOG,EAAG,cAAcC,CAAE,CAAC,CAC5D,CAGA,eAAexB,EAAqB,EAA0C,CAC5E,IAAMC,EAAsB,CAAC,EAC7B,IAAK,IAAMhD,KAAU,MAAMD,EAAQqE,EAAe,CAAE,cAAe,EAAK,CAAC,EAAG,CAC1E,GAAItF,EAA8B,IAAIkB,EAAO,IAAI,EAAG,SACpD,IAAMqE,EAAYjE,EAAKgE,EAAepE,EAAO,IAAI,EAC7CA,EAAO,OAAO,EAChBgD,EAAU,KAAKqB,CAAS,EACfrE,EAAO,YAAY,GAC5BgD,EAAU,KAAK,GAAI,MAAMD,EAAqBsB,CAAS,CAAE,CAE7D,CACA,OAAOrB,CACT,CAEA,SAASc,EAAkB,EAAiC,CAC1D,OAAOjB,EAAS,SAAW,IAAA,IAAaA,EAAS,aACnD,CAEA,SAAShC,EACP,EACA,EACA,EACA,EACA,EACM,CAIF2D,EAAc,SAAW,GAAKjE,GAAa,0BAA4B,KAGrE2C,EACFjE,EAAS,KACP,sJACF,EAEAD,EAAO,KACL,6OACF,GAGAwF,EAAc,OAAS,GACzBC,EAAgCD,EAAexF,EAAQC,CAAQ,CAEnE,CAEA,SAASwF,EACP,EACA,EACA,EACM,CACDf,EAAU,KAAM,GAAagB,EAA6B,KAAK7B,EAAS,EAAE,CAAC,GAC9E7D,EAAO,KAAK,qGAAqG,EAE/G0E,EAAU,MAAO,GAAagB,EAA6B,KAAK7B,EAAS,EAAE,CAAC,GAC9E7D,EAAO,KACL,yGACF,EAEE0E,EAAU,OAAS,GACrBzE,EAAS,KACP,QAAQyE,EAAU,OAAO,6CAC3B,CAEJ,CAEA,eAAexC,EACb,EACA,EACA,EACwB,CACxB,IAAMyD,EAA4BvE,EAAKf,EAAsB,eAAe,EAC5E,GAAI,CAAE,MAAME,EAAYoF,CAAyB,EAAI,MAAO,CAAC,EAE7D,IAAM1D,EAA8B,CAAC,EAC/ByB,EAAU,MAAM3C,EAAQ4E,EAA2B,CAAE,cAAe,EAAK,CAAC,EAChF,IAAK,IAAM3E,KAAU0C,EAAQ,UAAU,EAAI,IAAOzC,EAAG,KAAK,cAAcC,EAAG,IAAI,CAAC,EAAG,CACjF,GAAI,CAACF,EAAO,YAAY,GAAKA,EAAO,OAAS,cAAe,SAG5D,GAAI,CAACN,EAA2B,KAAKM,EAAO,IAAI,EAAG,CACjDhB,EAAO,KAAK,gCAAgCgB,EAAO,KAAK,eAAeN,GAA4B,EACnG,QACF,CACMkF,EAA2C,SAAS5E,EAAO,IAAI,GACnEf,EAAS,KACP,gCAAgCe,EAAO,KAAK,gCAAgC4E,EAAqB,KAAK,IAAI,EAAE,EAC9G,EAEF,IAAMC,EAAQ,MAAMC,EAA2B1E,EAAKuE,EAA2B3E,EAAO,IAAI,CAAC,EAEvF6E,EAAM,SAAW,EACnB7F,EAAO,KAAK,2BAA2BgB,EAAO,KAAK,8CAA8C,EAGnGiB,EAAa,KAAK,CAAE,GAAIjB,EAAO,KAAM,OAAM,CAAC,CAC9C,CACA,OAAOiB,CACT,CAEA,SAASO,EACP,EACA,EACA,EACA,EACM,CACN,IAAMuD,EAAkBC,EAAgBzE,EAAY,sBAAuB,wBAAyBvB,CAAM,EACpGiG,EAAmBD,EAAgBzE,EAAY,uBAAwB,yBAA0BvB,CAAM,EACvGkG,EAAiB3E,EAAY,qBAAqB,IAAK,GAAS4E,EAAkBC,CAAI,CAAC,CAAC,OAAO,EAErG,IAAK,GAAM,CAACC,EAASC,KAAWL,EAC1BrG,EAAsB,KAAM,GAAY0G,EAAO,KAAKC,CAAO,CAAC,GAC9DtG,EAAS,KACP,mCAAmCoG,EAAQ,oFAAoF1G,EAAwB,KAAK,IAAI,EAAE,EACpK,EAGJ,IAAK,IAAM6G,KAAQN,EACbtG,EAAsB,KAAM,GAAY2G,EAAQ,SAASC,CAAI,CAAC,GAChEvG,EAAS,KACP,8BAA8BuG,EAAK,oFAAoF7G,EAAwB,KAAK,IAAI,EAAE,EAC5J,EAIJ,IAAK,IAAM8G,KAAexE,EAAc,CACtC,IAAMyE,EAAgBC,EAA8BF,EAAY,KAAK,EAErE,IAAK,GAAM,CAACJ,EAASC,KAAWP,EACzBW,EAAc,KAAM,GAASJ,EAAO,KAAK/D,EAAK,IAAI,CAAC,GACtDvC,EAAO,KACL,qBAAqBqG,EAAQ,6CAA6CI,EAAY,GAAG,yBAC3F,EAGJ,IAAK,GAAM,CAACJ,EAASC,KAAWL,EAC9B,IAAK,IAAM1D,KAAQmE,EAAe,CAChC,IAAME,EAAUrE,EAAK,KAAK,MAAM+D,CAAM,EAClCM,GACF5G,EAAO,KACL,sBAAsBqG,EAAQ,8BAA8BI,EAAY,GAAG,GAAGlE,EAAK,KAAK,aAAaqE,EAAQ,GAAG,GAClH,CAEJ,CAEF,IAAK,IAAMC,KAAiBX,EAC1B,IAAK,IAAM3D,KAAQmE,EACbnE,EAAK,KAAK,SAASsE,CAAa,GAClC7G,EAAO,KAAK,mBAAmB6G,EAAc,iCAAiCJ,EAAY,GAAG,GAAGlE,EAAK,MAAM,CAInH,CACF,CAMA,SAASE,EACP,EACA,EACA,EACM,CACN,IAAK,IAAMgE,KAAexE,EAAc,CACtC,IAAM6E,EAAc,IAAI,IAAIL,EAAY,MAAM,IAAK,GAASlE,EAAK,IAAI,CAAC,EACtE,IAAK,IAAMwE,KAAgBxF,EAAY,4BAChCuF,EAAY,IAAIC,CAAY,GAC/B/G,EAAO,KACL,6BAA6B+G,EAAa,kCAAkCN,EAAY,GAAG,gCAC7F,CAGN,CACF,CAGA,SAASE,EAA8B,EAAmC,CACxE,OAAOd,EAAM,QAAS,GAAS,CAC7B,IAAMmB,EAAqB1E,EAA6BC,EAAK,IAAI,EACjE,GAAI,CAACyE,GAAsBzE,EAAK,KAAK,SAAS,GAAQ,EAAG,MAAO,CAAC,EACjE,IAAM0E,EAAUD,EAAmB,QACnC,MAAO,CAAC,CAAE,KAAMzE,EAAK,KAAM,KAAM0E,EAAUC,EAA2BD,EAAS1E,EAAK,IAAI,EAAIA,EAAK,IAAK,CAAC,CACzG,CAAC,CACH,CAEA,SAASyD,EAAgB,EAAmB,EAAmB,EAAsC,CACnG,IAAMmB,EAA+B,CAAC,EACtC,IAAK,IAAMf,KAAQgB,EAAO,CACxB,GAAM,CAAE,WAAYjB,EAAkBC,CAAI,EAC1C,GAAI,CACFe,EAAS,KAAK,CAACd,EAAS,IAAI,OAAOA,CAAO,CAAC,CAAC,CAC9C,MAAQ,CACNrG,EAAO,KAAK,iCAAiCqH,EAAU,IAAIhB,GAAS,CACtE,CACF,CACA,OAAOc,CACT,CAEA,eAAezE,EACb,EACA,EACA,EACA,EACA,EACe,CACf,IAAM4E,EAAyBlG,EAAKf,EAAsB,WAAW,EACrE,GAAI,CAAE,MAAME,EAAY+G,CAAsB,EAAI,OAKlD,IAAMC,GAAU,MAHMxG,EAAQuG,EAAwB,CAAE,cAAe,EAAK,CAAC,EAAA,CAGrD,OAAQ,GAAWtG,EAAO,OAAS,aAAa,EACpEuG,EAAQ,KAAM,GAAWvG,EAAO,OAAS,WAAW,GACtDf,EAAS,KAAK,+EAA+E,EAE/F,IAAMuH,EAAWD,EAAQ,KAAM,GAAWvG,EAAO,OAAO,CAAC,EACnDyG,EAAiBF,EAAQ,KAAM,GAAWvG,EAAO,YAAY,CAAC,EACpE,GAAIwG,GAAYC,EAAgB,CAC9BzH,EAAO,KACL,qJACF,EACA,MACF,CACA,IAAK,IAAMgB,KAAUuG,EAEjBvG,EAAO,YAAY,GACnBA,EAAO,OAAS,YAChB,CAAE4E,EAA2C,SAAS5E,EAAO,IAAI,GAEjEhB,EAAO,KACL,oCAAoCgB,EAAO,KAAK,6BAA6B4E,EAAqB,KAAK,IAAI,EAAE,cAC/G,EAOJ,IAAM8B,EAAiBH,EAAQ,OAAQ,GAAWvG,EAAO,YAAY,CAAC,CAAC,CAAC,IAAK,GAAWA,EAAO,IAAI,EAC7F2G,EAAyBF,EAAiBC,EAAiB,CAAC,EAAE,EACpE,IAAK,IAAME,KAAyBD,EAAwB,CAC1D,IAAME,EAAgB,MAAM/B,EAA2B1E,EAAKkG,EAAwBM,CAAqB,CAAC,EACpGE,EAAwB,IAAI,IAAID,EAAc,IAAK,GAAS,CAACtF,EAAK,KAAMA,EAAK,IAAI,CAAC,CAAC,EACzF,IAAK,IAAMkE,KAAexE,EAAc,CACtC,IAAM8F,EAActB,EAAY,MAAM,OACnC,GACCvC,GAAqB3B,EAAK,KAAK,KAAK,CAAC,CAAC,OAAS,GAAKD,EAA6BC,EAAK,IAAI,IAAM,IAAA,EACpG,EACIwF,EAAY,OAAS,GAAKA,EAAY,MAAO,GAASD,EAAsB,IAAIvF,EAAK,IAAI,IAAMA,EAAK,IAAI,GAC1GtC,EAAS,KACP,aAAa2H,EAAsB,+CAA+CnB,EAAY,GAAG,kFACnG,CAEJ,CACF,CACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/printTestCaseResult.cjs"),n=require("../helpers/parseArgs.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../types/language.cjs"),a=require("../helpers/findLanguageDefinitionByPath.cjs"),o=require("../helpers/readProblemMarkdownFrontMatter.cjs");require("../helpers/stdioJudgeRules.cjs");const s=require("../types/decisionCode.cjs"),c=require("../helpers/guiRecording.cjs"),l=require("../helpers/cleanWorkingDirectory.cjs"),u=require("../helpers/copyTestCaseFileInput.cjs"),d=require("../helpers/findEntryPointFile.cjs"),f=require("../helpers/judgeByStaticAnalysis.cjs"),p=require("../helpers/readOutputFiles.cjs"),m=require("../helpers/spawnWithTimeout.cjs");let h=require("node:path");h=e.__toESM(h,1);let g=require("zod"),_=require("node:fs/promises");_=e.__toESM(_,1);let v=require("node:os");v=e.__toESM(v,1);let y=require("@willbooster/shared-lib"),b=require("node:child_process");b=e.__toESM(b,1);let x=require("node:timers/promises");const S=[v.default.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`],C=g.z.object({language:g.z.union([g.z.string(),g.z.array(g.z.string())]).optional()});async function w(e,r){let i=n.parseArgs(process.argv);if(!i.cwd)throw Error(`cwd argument required`);let c=C.parse(i.params),m=i.cwd,h=await o.readProblemMarkdownFrontMatter(e),g=await(r.readTestCases??k)(e),_=g.length>0?g:[{id:`default`}],v=_[0]?.id??`prebuild`,b=await f.judgeByStaticAnalysis(i.cwd,h);if(b){t.printTestCaseResult({testCaseId:v,...b});return}let x=r.mainFilePath??await d.findEntryPointFile(i.cwd,c.language);if(!x){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${c.language?`: language: ${c.language}`:``}`});return}let S=a.findLanguageDefinitionByPath(x);if(!S){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.WRONG_ANSWER,stderr:`unsupported language`});return}let w={...process.env,CI:``,FORCE_COLOR:`0`},j=await T({cwd:i.cwd,language:c.language,configuredMainFilePath:r.mainFilePath});if(S.prebuild)try{await S.prebuild(i.cwd);let e=await T({cwd:i.cwd,language:c.language??E(x),configuredMainFilePath:r.mainFilePath,allowConfiguredPathFallback:!0});e&&(j=e)}catch(e){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.BUILD_ERROR,stderr:(0,y.getErrorMessage)(e)});return}if(!j){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${c.language?`: language: ${c.language}`:``}`});return}let M;if(r.prepare)try{M=await r.prepare({cwd:m,env:w,mainFilePath:j,problemMarkdownFrontMatter:h})}catch(e){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.BUILD_ERROR,stderr:(0,y.getErrorMessage)(e)});return}let N=M??await D({cwd:i.cwd,env:w,mainFilePath:j,languageDefinition:S});if(N){t.printTestCaseResult({testCaseId:v,decisionCode:N.decisionCode??s.DecisionCode.BUILD_ERROR,feedbackMarkdown:N.feedbackMarkdown,stderr:N.stderr,stdout:N.stdout,outputFiles:N.outputFiles});return}let F=await l.snapshotWorkingDirectory(i.cwd),I,L=v,R;try{I=r.runCommand?void 0:await P();let e=g.shared?.fileInputPath;for(let n of _){L=n.id,e&&await u.copyTestCaseFileInput(e,i.cwd),n.fileInputPath&&await u.copyTestCaseFileInput(n.fileInputPath,i.cwd);let a=typeof h.timeLimitMs==`number`?h.timeLimitMs/1e3:r.runTimeoutSeconds??5,o=I?{...w,DISPLAY:I.display}:w,c=await r.resolveInput?.({testCase:n,cwd:i.cwd,env:o})??n.input??``;R=c;let d=await r.command?.({testCase:n,cwd:i.cwd,env:o,mainFilePath:j})??S.command(j),f;try{let e={command:d,stdin:c,cwd:m,env:o,timeLimitSeconds:a,screenshotWaitSeconds:r.screenshotWaitSeconds??.3,stopDetectionThreshold:r.stopDetectionThreshold??5,recordsAnimation:r.recordsAnimation??!1},t=r.runCommand?await r.runCommand({testCase:n,...e}):await A(e);f={...t,recordings:t.recordings??[]}}catch(e){t.printTestCaseResult({testCaseId:n.id,decisionCode:s.DecisionCode.RUNTIME_ERROR,stdin:c,stderr:(0,y.getErrorMessage)(e)}),await l.cleanWorkingDirectory(i.cwd,F);return}let g=await p.readOutputFiles(i.cwd,h.requiredOutputFilePaths??[]),_={timeLimitSeconds:a,problemMarkdownFrontMatter:{memoryLimitByte:h.memoryLimitByte,requiredOutputFilePaths:h.requiredOutputFilePaths}},v=O({runResult:f,outputFiles:g,context:_,acceptsTimeout:r.recordsAnimation??!1}),b=v;if(v.decisionCode===s.DecisionCode.ACCEPTED)try{let e=await r.test({testCase:n,runResult:f,outputFiles:g,cwd:i.cwd,context:_});b={decisionCode:e.decisionCode??v.decisionCode,feedbackMarkdown:e.feedbackMarkdown,stderr:e.stderr,stdout:e.stdout,outputFiles:e.outputFiles}}catch(e){b={decisionCode:s.DecisionCode.RUNTIME_ERROR,stderr:(0,y.getErrorMessage)(e)}}let x=b.decisionCode??s.DecisionCode.ACCEPTED,C=b.stdout??f.stdout,T=b.stderr??f.stderr;if(t.printTestCaseResult({testCaseId:n.id,decisionCode:x,exitStatus:f.status,stdin:f.stdin||void 0,stdout:C||void 0,stderr:T||void 0,timeSeconds:f.timeSeconds,memoryBytes:f.memoryBytes,feedbackMarkdown:b.feedbackMarkdown,outputFiles:b.outputFiles??(g.length>0?g:void 0)}),await l.cleanWorkingDirectory(i.cwd,F),x!==s.DecisionCode.ACCEPTED)break}}catch(e){t.printTestCaseResult({testCaseId:L,decisionCode:s.DecisionCode.RUNTIME_ERROR,stdin:R,stderr:(0,y.getErrorMessage)(e)}),await l.cleanWorkingDirectory(i.cwd,F)}finally{await I?.dispose()}}async function T(e){if(e.configuredMainFilePath){if(await R(h.default.join(e.cwd,e.configuredMainFilePath)))return e.configuredMainFilePath;if(!e.allowConfiguredPathFallback)return}return await d.findEntryPointFile(e.cwd,e.language)}function E(e){let t=Object.entries(i.languageIdToDefinition).filter(([,t])=>t.fileExtensions.some(t=>e.endsWith(t))).map(([e])=>e);return t.length>0?t:void 0}async function D(e){let t=e.languageDefinition.buildCommand?.(e.mainFilePath);if(!t)return;let n=await m.spawnWithTimeout(t[0],t.slice(1),{cwd:e.cwd,env:e.env},10),r=(n.stderr||n.stdout).slice(0,5e4)||void 0;if(n.timeSeconds>10)return{decisionCode:s.DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,stderr:r};if(n.status!==0)return{decisionCode:s.DecisionCode.BUILD_ERROR,stderr:r};if(n.outputLimitExceeded)return{decisionCode:s.DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:r}}function O(e){if(e.runResult.stopReason===`timeout`&&!e.acceptsTimeout)return{decisionCode:s.DecisionCode.TIME_LIMIT_EXCEEDED,stderr:e.runResult.stderr};if(e.runResult.status!==0)return{decisionCode:s.DecisionCode.RUNTIME_ERROR,stderr:e.runResult.stderr};if(e.runResult.memoryBytes>(e.context.problemMarkdownFrontMatter.memoryLimitByte??1/0))return{decisionCode:s.DecisionCode.MEMORY_LIMIT_EXCEEDED,stderr:e.runResult.stderr};let t=e.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length??0;return e.outputFiles.length<t?{decisionCode:s.DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR}:{decisionCode:s.DecisionCode.ACCEPTED}}async function k(e){return await r.readTestCases(h.default.join(e,`test_cases`))}async function A(e){let t=b.default.spawn(`timeout`,[e.timeLimitSeconds.toFixed(3),...S,...e.command],{cwd:e.cwd,env:e.env,detached:process.platform!==`win32`,stdio:[`pipe`,`pipe`,`pipe`]});t.stdout.setEncoding(`utf8`),t.stderr.setEncoding(`utf8`);let n=``,r=``,i,a,o=`process_exit`,s=[],l=[],u=e.recordsAnimation?new c.GuiRecorder:void 0,d=Date.now()/1e3,f=0;t.stdout.on(`data`,e=>{n+=e}),t.stderr.on(`data`,e=>{r+=e}),t.on(`error`,e=>{a=e,i=1}),t.stdin.on(`error`,()=>{}),t.on(`close`,(e,t)=>{
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/printTestCaseResult.cjs"),n=require("../helpers/parseArgs.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../types/language.cjs"),a=require("../helpers/findLanguageDefinitionByPath.cjs"),o=require("../helpers/readProblemMarkdownFrontMatter.cjs");require("../helpers/stdioJudgeRules.cjs");const s=require("../types/decisionCode.cjs"),c=require("../helpers/guiRecording.cjs"),l=require("../helpers/cleanWorkingDirectory.cjs"),u=require("../helpers/copyTestCaseFileInput.cjs"),d=require("../helpers/findEntryPointFile.cjs"),f=require("../helpers/judgeByStaticAnalysis.cjs"),p=require("../helpers/readOutputFiles.cjs"),m=require("../helpers/spawnWithTimeout.cjs");let h=require("node:path");h=e.__toESM(h,1);let g=require("zod"),_=require("node:fs/promises");_=e.__toESM(_,1);let v=require("node:os");v=e.__toESM(v,1);let y=require("@willbooster/shared-lib"),b=require("node:child_process");b=e.__toESM(b,1);let x=require("node:timers/promises");const S=[v.default.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`],C=g.z.object({language:g.z.union([g.z.string(),g.z.array(g.z.string())]).optional()});async function w(e,r){let i=n.parseArgs(process.argv);if(!i.cwd)throw Error(`cwd argument required`);let c=C.parse(i.params),m=i.cwd,h=await o.readProblemMarkdownFrontMatter(e),g=await(r.readTestCases??k)(e),_=g.length>0?g:[{id:`default`}],v=_[0]?.id??`prebuild`,b=await f.judgeByStaticAnalysis(i.cwd,h);if(b){t.printTestCaseResult({testCaseId:v,...b});return}let x=r.mainFilePath??await d.findEntryPointFile(i.cwd,c.language);if(!x){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${c.language?`: language: ${c.language}`:``}`});return}let S=a.findLanguageDefinitionByPath(x);if(!S){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.WRONG_ANSWER,stderr:`unsupported language`});return}let w={...process.env,CI:``,FORCE_COLOR:`0`},j=await T({cwd:i.cwd,language:c.language,configuredMainFilePath:r.mainFilePath});if(S.prebuild)try{await S.prebuild(i.cwd);let e=await T({cwd:i.cwd,language:c.language??E(x),configuredMainFilePath:r.mainFilePath,allowConfiguredPathFallback:!0});e&&(j=e)}catch(e){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.BUILD_ERROR,stderr:(0,y.getErrorMessage)(e)});return}if(!j){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${c.language?`: language: ${c.language}`:``}`});return}let M;if(r.prepare)try{M=await r.prepare({cwd:m,env:w,mainFilePath:j,problemMarkdownFrontMatter:h})}catch(e){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.BUILD_ERROR,stderr:(0,y.getErrorMessage)(e)});return}let N=M??await D({cwd:i.cwd,env:w,mainFilePath:j,languageDefinition:S});if(N){t.printTestCaseResult({testCaseId:v,decisionCode:N.decisionCode??s.DecisionCode.BUILD_ERROR,feedbackMarkdown:N.feedbackMarkdown,stderr:N.stderr,stdout:N.stdout,outputFiles:N.outputFiles});return}let F=await l.snapshotWorkingDirectory(i.cwd),I,L=v,R;try{I=r.runCommand?void 0:await P();let e=g.shared?.fileInputPath;for(let n of _){L=n.id,e&&await u.copyTestCaseFileInput(e,i.cwd),n.fileInputPath&&await u.copyTestCaseFileInput(n.fileInputPath,i.cwd);let a=typeof h.timeLimitMs==`number`?h.timeLimitMs/1e3:r.runTimeoutSeconds??5,o=I?{...w,DISPLAY:I.display}:w,c=await r.resolveInput?.({testCase:n,cwd:i.cwd,env:o})??n.input??``;R=c;let d=await r.command?.({testCase:n,cwd:i.cwd,env:o,mainFilePath:j})??S.command(j),f;try{let e={command:d,stdin:c,cwd:m,env:o,timeLimitSeconds:a,screenshotWaitSeconds:r.screenshotWaitSeconds??.3,stopDetectionThreshold:r.stopDetectionThreshold??5,recordsAnimation:r.recordsAnimation??!1},t=r.runCommand?await r.runCommand({testCase:n,...e}):await A(e);f={...t,recordings:t.recordings??[]}}catch(e){t.printTestCaseResult({testCaseId:n.id,decisionCode:s.DecisionCode.RUNTIME_ERROR,stdin:c,stderr:(0,y.getErrorMessage)(e)}),await l.cleanWorkingDirectory(i.cwd,F);return}let g=await p.readOutputFiles(i.cwd,h.requiredOutputFilePaths??[]),_={timeLimitSeconds:a,problemMarkdownFrontMatter:{memoryLimitByte:h.memoryLimitByte,requiredOutputFilePaths:h.requiredOutputFilePaths}},v=O({runResult:f,outputFiles:g,context:_,acceptsTimeout:r.recordsAnimation??!1}),b=v;if(v.decisionCode===s.DecisionCode.ACCEPTED)try{let e=await r.test({testCase:n,runResult:f,outputFiles:g,cwd:i.cwd,context:_});b={decisionCode:e.decisionCode??v.decisionCode,feedbackMarkdown:e.feedbackMarkdown,stderr:e.stderr,stdout:e.stdout,outputFiles:e.outputFiles}}catch(e){b={decisionCode:s.DecisionCode.RUNTIME_ERROR,stderr:(0,y.getErrorMessage)(e)}}let x=b.decisionCode??s.DecisionCode.ACCEPTED,C=b.stdout??f.stdout,T=b.stderr??f.stderr;if(t.printTestCaseResult({testCaseId:n.id,decisionCode:x,exitStatus:f.status,stdin:f.stdin||void 0,stdout:C||void 0,stderr:T||void 0,timeSeconds:f.timeSeconds,memoryBytes:f.memoryBytes,feedbackMarkdown:b.feedbackMarkdown,outputFiles:b.outputFiles??(g.length>0?g:void 0)}),await l.cleanWorkingDirectory(i.cwd,F),x!==s.DecisionCode.ACCEPTED)break}}catch(e){t.printTestCaseResult({testCaseId:L,decisionCode:s.DecisionCode.RUNTIME_ERROR,stdin:R,stderr:(0,y.getErrorMessage)(e)}),await l.cleanWorkingDirectory(i.cwd,F)}finally{await I?.dispose()}}async function T(e){if(e.configuredMainFilePath){if(await R(h.default.join(e.cwd,e.configuredMainFilePath)))return e.configuredMainFilePath;if(!e.allowConfiguredPathFallback)return}return await d.findEntryPointFile(e.cwd,e.language)}function E(e){let t=Object.entries(i.languageIdToDefinition).filter(([,t])=>t.fileExtensions.some(t=>e.endsWith(t))).map(([e])=>e);return t.length>0?t:void 0}async function D(e){let t=e.languageDefinition.buildCommand?.(e.mainFilePath);if(!t)return;let n=await m.spawnWithTimeout(t[0],t.slice(1),{cwd:e.cwd,env:e.env},10),r=(n.stderr||n.stdout).slice(0,5e4)||void 0;if(n.timeSeconds>10)return{decisionCode:s.DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,stderr:r};if(n.status!==0)return{decisionCode:s.DecisionCode.BUILD_ERROR,stderr:r};if(n.outputLimitExceeded)return{decisionCode:s.DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:r}}function O(e){if(e.runResult.stopReason===`timeout`&&!e.acceptsTimeout)return{decisionCode:s.DecisionCode.TIME_LIMIT_EXCEEDED,stderr:e.runResult.stderr};if(e.runResult.status!==0)return{decisionCode:s.DecisionCode.RUNTIME_ERROR,stderr:e.runResult.stderr};if(e.runResult.memoryBytes>(e.context.problemMarkdownFrontMatter.memoryLimitByte??1/0))return{decisionCode:s.DecisionCode.MEMORY_LIMIT_EXCEEDED,stderr:e.runResult.stderr};let t=e.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length??0;return e.outputFiles.length<t?{decisionCode:s.DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR}:{decisionCode:s.DecisionCode.ACCEPTED}}async function k(e){return await r.readTestCases(h.default.join(e,`test_cases`))}async function A(e){let t=b.default.spawn(`timeout`,[e.timeLimitSeconds.toFixed(3),...S,...e.command],{cwd:e.cwd,env:e.env,detached:process.platform!==`win32`,stdio:[`pipe`,`pipe`,`pipe`]});t.stdout.setEncoding(`utf8`),t.stderr.setEncoding(`utf8`);let n=``,r=``,i,a,o=`process_exit`,s=[],l=[],u=e.recordsAnimation?new c.GuiRecorder:void 0,d=Date.now()/1e3,f=0;t.stdout.on(`data`,e=>{n+=e}),t.stderr.on(`data`,e=>{r+=e}),t.on(`error`,e=>{a=e,i=1}),t.stdin.on(`error`,()=>{}),t.on(`close`,(e,t)=>{e===124?(o=`timeout`,i=0):t?(i=1,r=r?`${r}\nprocess terminated by signal: ${t}`:`process terminated by signal: ${t}`):i=e??1});try{e.stdin&&t.stdin.write(e.stdin),t.stdin.end();let n=0;for(;i===void 0;){let r=Date.now()/1e3-d+e.screenshotWaitSeconds+n;if(u&&r>=e.timeLimitSeconds){o=`timeout`,i=0;break}await(0,x.setTimeout)(e.screenshotWaitSeconds*1e3),f=Math.max(f,I(t.pid));let a=Date.now(),c=j(e.env.DISPLAY),p=Date.now();n=(p-a)/1e3;for(let{windowId:e,isSinglePixel:t,screenshot:n}of c)t||u?.add(e,n,p);if(l=c.map(({screenshot:e})=>e).toSorted((e,t)=>e.data.length-t.data.length),l.length>0){let t=l.map(e=>e.data).toSorted();if(s.unshift(t),s.length=Math.min(s.length,e.stopDetectionThreshold),s.length===e.stopDetectionThreshold&&s.every(e=>e.length===t.length&&e.every((e,n)=>e===t[n]))){o=`stable_screenshot`,i=0;break}}if(Date.now()/1e3-d>e.timeLimitSeconds){o=`timeout`,i=0;break}}o!==`process_exit`&&t.exitCode===null&&(t.removeAllListeners(`close`),t.removeAllListeners(`error`))}finally{await F(t)}if(a)throw a;let{memoryBytes:p,stderr:m,timeSeconds:h}=N(r,d,f);return{stdin:e.stdin,stdout:n.trimEnd(),stderr:m,status:i,timeSeconds:h,memoryBytes:p,screenshots:l,recordings:u?.build(e.screenshotWaitSeconds*1e3)??[],stopReason:o}}function j(e){let t={...process.env,DISPLAY:e??process.env.DISPLAY},n=b.default.spawnSync(`xwininfo`,[`-root`,`-tree`],{encoding:`utf8`,env:t});if(n.error)throw n.error;if(n.status!==0||!n.stdout)return[];let r=[];for(let{windowId:e,isSinglePixel:i}of M(n.stdout)){let n=b.default.spawnSync(`maim`,[`-i`,e],{env:t});if(n.error)throw n.error;if(n.status!==0||n.stdout.length===0)continue;let a=b.default.spawnSync(`xdotool`,[`getwindowname`,e],{encoding:`utf8`,env:t});if(a.error)throw a.error;let o=a.stdout.trim().replaceAll(/[\s/]/g,`_`);r.push({windowId:e,isSinglePixel:i,screenshot:{path:`${o||`window`}_${e}.png`,data:n.stdout.toString(`base64`),encoding:`base64`}})}return r}function M(e){let t=[],n=e.split(`
|
|
2
2
|
`);for(let e of n){if(e.includes(`Root window id:`)||e.includes(`Parent window id:`)||e.includes(`()`))continue;let n=/^\s{5}(0x[\da-f]+) /.exec(e);n?.[1]&&t.push({windowId:Number.parseInt(n[1],16).toString(),isSinglePixel:/ 1x1[+-]\d+[+-]\d+ {2}[+-]\d+[+-]\d+\s*$/.test(e)})}return t}function N(e,t,n){let r=/(?:^|\n)(\d+\.\d+) (\d+)\s*$/.exec(e),i=r?e.slice(0,r.index).trimEnd():e.trimEnd(),a=Number(r?.[2])*1024||0;return{stderr:i,timeSeconds:Number(r?.[1])||Date.now()/1e3-t,memoryBytes:Math.max(a,n)}}async function P(){if(process.platform!==`linux`)throw Error(`GUI screenshot capture is supported only on Linux.`);for(let e=90;e<100;e++){let t=`:${e}`,n,r=b.default.spawn(`Xvfb`,[t,`-screen`,`0`,`1280x1024x24`,`-ac`],{stdio:`ignore`});if(r.on(`error`,e=>{n=e}),await(0,x.setTimeout)(300),n)throw n;if(r.exitCode===null)return{display:t,dispose:async()=>{r.killed||(r.kill(`SIGTERM`),await(0,x.setTimeout)(100),r.exitCode===null&&r.kill(`SIGKILL`))}}}throw Error(`Xvfb could not be started.`)}async function F(e){e.pid&&(L(e.pid,`SIGTERM`),await(0,x.setTimeout)(200),e.exitCode===null&&L(e.pid,`SIGKILL`))}function I(e){if(!e||process.platform!==`linux`)return 0;let t=b.default.spawnSync(`ps`,[`-o`,`rss=`,`--no-headers`,`--pgroup`,String(e)],{encoding:`utf8`});return t.error||t.status!==0||!t.stdout?0:t.stdout.split(`
|
|
3
3
|
`).map(e=>Number(e.trim())).filter(e=>Number.isFinite(e)&&e>0).reduce((e,t)=>e+t*1024,0)}function L(e,t){try{if(process.platform!==`win32`){process.kill(-e,t);return}}catch{}try{process.kill(e,t)}catch{}}async function R(e){try{return await _.default.access(e),!0}catch{return!1}}exports.guiCommandJudgePreset=w;
|
|
4
4
|
//# sourceMappingURL=guiCommand.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guiCommand.cjs","names":["os","z","parseArgs","readProblemMarkdownFrontMatter","judgeByStaticAnalysis","findEntryPointFile","DecisionCode","findLanguageDefinitionByPath","getErrorMessage","snapshotWorkingDirectory","copyTestCaseFileInput","cleanWorkingDirectory","readOutputFiles","path","languageIdToDefinition","spawnWithTimeout","readFileTestCases","childProcess","GuiRecorder","wait","fs","require_runtime","require_printTestCaseResult","require_parseArgs","require_readTestCases","require_language","require_findLanguageDefinitionByPath","require_readProblemMarkdownFrontMatter","require_decisionCode","require_guiRecording","require_cleanWorkingDirectory","require_copyTestCaseFileInput","require_findEntryPointFile","require_judgeByStaticAnalysis","require_readOutputFiles","require_spawnWithTimeout","node_path","zod","node_fs_promises","node_os","_willbooster_shared_lib","node_child_process","node_timers_promises","TIME_COMMAND","judgeParamsSchema","guiCommandJudgePreset","args","params","submissionDir","problemMarkdownFrontMatter","problemDir","configuredTestCases","options","readGuiTestCases","testCases","prebuildTestCaseId","staticAnalysisResult","initialMainFilePath","languageDefinition","env","resolvedMainFilePath","resolveMainFilePath","prebuiltMainFilePath","inferLanguageIdsByPath","error","customPrepareResult","prepareResult","runDefaultPrepare","cwdSnapshot","displayServer","currentTestCaseId","currentStdin","ensureDisplayServer","sharedFileInputPath","testCase","timeLimitSeconds","runEnv","stdin","command","runResult","runContext","result","spawnGuiProgram","outputFiles","judgeContext","baseJudgeResult","evaluateGuiRunResult","judgeResult","extendedJudgeResult","decisionCode","stdout","stderr","context","pathExists","languageIds","definition","filePath","ext","languageId","buildCommand","buildResult","buildOutput","requiredOutputFilesCount","child","exitCode","spawnError","stopReason","screenshotSignaturesHistory","screenshots","recorder","startTimeSeconds","sampledMemoryBytes","chunk","code","signal","captureSeconds","nextCaptureEndSeconds","readProcessGroupMemoryBytes","captureStartMs","capturedWindows","takeScreenshots","capturedAtMs","isSinglePixel","windowId","screenshot","a","b","screenshotSignatures","file","files","index","stopProcess","normalizedStderr","parseTimedStderr","display","xwininfo","extractTopLevelWindows","windowNameResult","windowName","windows","lines","line","match","parsedMemoryBytes","displayNumber","xvfb","killProcessGroup","processGroupId","value","sum"],"sources":["../../src/presets/guiCommand.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as wait } from 'node:timers/promises';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { z } from 'zod';\n\nimport { cleanWorkingDirectory, snapshotWorkingDirectory } from '../helpers/cleanWorkingDirectory.js';\nimport { copyTestCaseFileInput } from '../helpers/copyTestCaseFileInput.js';\nimport { findEntryPointFile } from '../helpers/findEntryPointFile.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { GuiRecorder } from '../helpers/guiRecording.js';\nimport type { GuiRecordingFile } from '../helpers/guiRecording.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport { spawnWithTimeout } from '../helpers/spawnWithTimeout.js';\nimport { MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { languageIdToDefinition } from '../types/language.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst BUILD_TIMEOUT_SECONDS = 10;\nconst JUDGE_DEFAULT_TIMEOUT_SECONDS = 5;\nconst SCREENSHOT_WAIT_SECONDS = 0.3;\nconst XVFB_STARTUP_WAIT_SECONDS = 0.3;\nconst XVFB_SHUTDOWN_WAIT_SECONDS = 0.1;\nconst PROCESS_SHUTDOWN_WAIT_SECONDS = 0.2;\nconst STOP_DETECTION_THRESHOLD = 5;\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\n/** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */\ninterface BaseGuiTestCase {\n id: string;\n /** Standard input (`test_cases/<id>.in`). */\n input?: string;\n /** Directory copied into the working directory before the run (`test_cases/<id>.fin/`). */\n fileInputPath?: string;\n}\n\n/** A test case read from `test_cases/` by the default reader. */\nexport interface GuiTestCase extends BaseGuiTestCase {\n /** Expected standard output (`test_cases/<id>.out`), for the problem's `test` to compare. */\n output?: string;\n /** Directory of expected output files (`test_cases/<id>.fout/`), for the problem's `test` to compare. */\n fileOutputPath?: string;\n}\n\nexport interface GuiScreenshotFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\nexport type { GuiRecordingFile } from '../helpers/guiRecording.js';\n\nexport interface GuiCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n screenshots: GuiScreenshotFile[];\n /**\n * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each\n * window that kept changing during the run; a window that only appeared and was painted has none.\n */\n recordings?: GuiRecordingFile[];\n stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\n}\n\ninterface CapturedWindow {\n windowId: string;\n isSinglePixel: boolean;\n screenshot: GuiScreenshotFile;\n}\n\ninterface GuiJudgeContext {\n timeLimitSeconds: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\ntype GuiJudgeCaseResult = Pick<\n TestCaseResult,\n 'decisionCode' | 'feedbackMarkdown' | 'stderr' | 'stdout' | 'outputFiles'\n>;\n\nexport interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase = GuiTestCase> {\n mainFilePath?: string;\n runTimeoutSeconds?: number;\n screenshotWaitSeconds?: number;\n stopDetectionThreshold?: number;\n /**\n * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends\n * the recording instead of failing the run: a run stopped by it reaches `test` with\n * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.\n */\n recordsAnimation?: boolean;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n prepare?: (context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n problemMarkdownFrontMatter: ProblemMarkdownFrontMatter;\n }) => Promise<Partial<GuiJudgeCaseResult> | undefined> | Partial<GuiJudgeCaseResult> | undefined;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n command?: (context: {\n testCase: TTestCase;\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n }) => Promise<readonly [string, ...string[]]> | readonly [string, ...string[]];\n runCommand?: (context: {\n testCase: TTestCase;\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: Required<GuiCommandRunResult>;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */\n cwd: string;\n context: GuiJudgeContext;\n }) => Promise<Partial<GuiJudgeCaseResult>> | Partial<GuiJudgeCaseResult>;\n}\n\n/**\n * A preset function for judging GUI programs by collecting screenshots while the program runs.\n *\n * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also\n * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { DecisionCode } from '@exercode/problem-utils';\n * import { guiCommandJudgePreset } from '@exercode/problem-utils/presets/guiCommand';\n *\n * await guiCommandJudgePreset(import.meta.dirname, {\n * mainFilePath: 'Main.java',\n * readTestCases: async () => [{ id: 'default' }],\n * test: ({ runResult }) => {\n * return runResult.screenshots.length > 0\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n */\nexport async function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(\n problemDir: string,\n options: GuiCommandJudgePresetOptions<TTestCase>\n): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n const submissionDir = args.cwd;\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const configuredTestCases = await (options.readTestCases ?? readGuiTestCases<TTestCase>)(problemDir);\n const testCases =\n configuredTestCases.length > 0 ? configuredTestCases : ([{ id: 'default' }] as unknown as readonly TTestCase[]);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n\n const staticAnalysisResult = await judgeByStaticAnalysis(args.cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return;\n }\n\n const initialMainFilePath = options.mainFilePath ?? (await findEntryPointFile(args.cwd, params.language));\n if (!initialMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n const languageDefinition = findLanguageDefinitionByPath(initialMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return;\n }\n\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let resolvedMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language,\n configuredMainFilePath: options.mainFilePath,\n });\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(args.cwd);\n const prebuiltMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language ?? inferLanguageIdsByPath(initialMainFilePath),\n configuredMainFilePath: options.mainFilePath,\n allowConfiguredPathFallback: true,\n });\n if (prebuiltMainFilePath) resolvedMainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n if (!resolvedMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n let customPrepareResult: Partial<GuiJudgeCaseResult> | undefined;\n if (options.prepare) {\n try {\n customPrepareResult = await options.prepare({\n cwd: submissionDir,\n env,\n mainFilePath: resolvedMainFilePath,\n problemMarkdownFrontMatter,\n });\n } catch (error) {\n // Like the `prebuild` step above: report the failed build rather than letting the throw end the\n // run resultless.\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n const prepareResult =\n customPrepareResult ??\n (await runDefaultPrepare({\n cwd: args.cwd,\n env,\n mainFilePath: resolvedMainFilePath,\n languageDefinition,\n }));\n if (prepareResult) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: prepareResult.decisionCode ?? DecisionCode.BUILD_ERROR,\n feedbackMarkdown: prepareResult.feedbackMarkdown,\n stderr: prepareResult.stderr,\n stdout: prepareResult.stdout,\n outputFiles: prepareResult.outputFiles,\n });\n return;\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(args.cwd);\n let displayServer: Awaited<ReturnType<typeof ensureDisplayServer>> | undefined;\n let currentTestCaseId = prebuildTestCaseId;\n let currentStdin: string | undefined;\n try {\n displayServer = options.runCommand ? undefined : await ensureDisplayServer();\n const sharedFileInputPath = (configuredTestCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n for (const testCase of testCases) {\n currentTestCaseId = testCase.id;\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, args.cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, args.cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : (options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS);\n\n const runEnv = displayServer ? { ...env, DISPLAY: displayServer.display } : env;\n const stdin = (await options.resolveInput?.({ testCase, cwd: args.cwd, env: runEnv })) ?? testCase.input ?? '';\n currentStdin = stdin;\n const command =\n (await options.command?.({ testCase, cwd: args.cwd, env: runEnv, mainFilePath: resolvedMainFilePath })) ??\n languageDefinition.command(resolvedMainFilePath);\n\n let runResult: Required<GuiCommandRunResult>;\n try {\n const runContext = {\n command,\n stdin,\n cwd: submissionDir,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n recordsAnimation: options.recordsAnimation ?? false,\n };\n const result = options.runCommand\n ? await options.runCommand({ testCase, ...runContext })\n : await spawnGuiProgram(runContext);\n runResult = { ...result, recordings: result.recordings ?? [] };\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n return;\n }\n\n const outputFiles = await readOutputFiles(args.cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: GuiJudgeContext = {\n timeLimitSeconds,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateGuiRunResult({\n runResult,\n outputFiles,\n context: judgeContext,\n acceptsTimeout: options.recordsAnimation ?? false,\n });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test({\n testCase,\n runResult,\n outputFiles,\n cwd: args.cwd,\n context: judgeContext,\n });\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n stdout: extendedJudgeResult.stdout,\n outputFiles: extendedJudgeResult.outputFiles,\n };\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: getErrorMessage(error),\n };\n }\n }\n\n const decisionCode = judgeResult.decisionCode ?? DecisionCode.ACCEPTED;\n const stdout = judgeResult.stdout ?? runResult.stdout;\n const stderr = judgeResult.stderr ?? runResult.stderr;\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode,\n exitStatus: runResult.status,\n stdin: runResult.stdin || undefined,\n stdout: stdout || undefined,\n stderr: stderr || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n feedbackMarkdown: judgeResult.feedbackMarkdown,\n outputFiles: judgeResult.outputFiles ?? (outputFiles.length > 0 ? outputFiles : undefined),\n });\n\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n if (decisionCode !== DecisionCode.ACCEPTED) break;\n }\n } catch (error) {\n printTestCaseResult({\n testCaseId: currentTestCaseId,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: currentStdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n } finally {\n await displayServer?.dispose();\n }\n}\n\nasync function resolveMainFilePath(context: {\n cwd: string;\n language?: string | string[];\n configuredMainFilePath?: string;\n allowConfiguredPathFallback?: boolean;\n}): Promise<string | undefined> {\n if (context.configuredMainFilePath) {\n const resolvedPath = path.join(context.cwd, context.configuredMainFilePath);\n if (await pathExists(resolvedPath)) return context.configuredMainFilePath;\n if (!context.allowConfiguredPathFallback) return undefined;\n }\n\n return await findEntryPointFile(context.cwd, context.language);\n}\n\nfunction inferLanguageIdsByPath(filePath: string): string[] | undefined {\n const languageIds = Object.entries(languageIdToDefinition)\n .filter(([, definition]) => definition.fileExtensions.some((ext) => filePath.endsWith(ext)))\n .map(([languageId]) => languageId);\n return languageIds.length > 0 ? languageIds : undefined;\n}\n\nasync function runDefaultPrepare(context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n languageDefinition: NonNullable<ReturnType<typeof findLanguageDefinitionByPath>>;\n}): Promise<Partial<GuiJudgeCaseResult> | undefined> {\n const buildCommand = context.languageDefinition.buildCommand?.(context.mainFilePath);\n if (!buildCommand) return undefined;\n\n const buildResult = await spawnWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, env: context.env },\n BUILD_TIMEOUT_SECONDS\n );\n const buildOutput = (buildResult.stderr || buildResult.stdout).slice(0, MAX_STDOUT_LENGTH) || undefined;\n\n if (buildResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.status !== 0) {\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.outputLimitExceeded) {\n return {\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n return undefined;\n}\n\nfunction evaluateGuiRunResult(context: {\n runResult: GuiCommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: GuiJudgeContext;\n acceptsTimeout: boolean;\n}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout' && !context.acceptsTimeout) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n if (context.runResult.status !== 0) {\n return {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: context.runResult.stderr,\n };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return {\n decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n const requiredOutputFilesCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFilesCount) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR,\n };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n\nasync function readGuiTestCases<TTestCase extends BaseGuiTestCase>(problemDir: string): Promise<readonly TTestCase[]> {\n // The default reader yields the base shape; a narrower TTestCase must come from `options.readTestCases`.\n return (await readFileTestCases(path.join(problemDir, 'test_cases'))) as unknown as readonly TTestCase[];\n}\n\nasync function spawnGuiProgram(context: {\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n}): Promise<GuiCommandRunResult> {\n const child = childProcess.spawn(\n 'timeout',\n [context.timeLimitSeconds.toFixed(3), ...TIME_COMMAND, ...context.command],\n {\n cwd: context.cwd,\n env: context.env,\n detached: process.platform !== 'win32',\n stdio: ['pipe', 'pipe', 'pipe'],\n }\n );\n\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | undefined;\n let spawnError: Error | undefined;\n let stopReason: GuiCommandRunResult['stopReason'] = 'process_exit';\n const screenshotSignaturesHistory: string[][] = [];\n let screenshots: GuiScreenshotFile[] = [];\n const recorder = context.recordsAnimation ? new GuiRecorder() : undefined;\n const startTimeSeconds = Date.now() / 1000;\n let sampledMemoryBytes = 0;\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n child.on('error', (error) => {\n spawnError = error;\n exitCode = 1;\n });\n // A child that exits before the input is fully written makes the write fail; without a listener\n // that EPIPE would be an unhandled stream error terminating the harness mid-run.\n child.stdin.on('error', () => {\n // The submission simply stopped reading its input; the exit handling below reports the result.\n });\n child.on('close', (code, signal) => {\n if (code === 124) {\n stopReason = 'timeout';\n exitCode = 0;\n return;\n }\n if (signal) {\n exitCode = 1;\n stderr = stderr\n ? `${stderr}\\nprocess terminated by signal: ${signal}`\n : `process terminated by signal: ${signal}`;\n return;\n }\n exitCode = code ?? 1;\n });\n\n // Stop the program even when a helper throws (e.g. `xwininfo` missing); the spawned process\n // would otherwise outlive this run.\n try {\n if (context.stdin) child.stdin.write(context.stdin);\n child.stdin.end();\n\n let captureSeconds = 0;\n while (exitCode === undefined) {\n // A recorded run that times out reaches `test`, so its last capture must show the program's\n // windows: stop it before a capture that `timeout` would kill the program during.\n const nextCaptureEndSeconds =\n Date.now() / 1000 - startTimeSeconds + context.screenshotWaitSeconds + captureSeconds;\n if (recorder && nextCaptureEndSeconds >= context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const captureStartMs = Date.now();\n const capturedWindows = takeScreenshots(context.env.DISPLAY);\n const capturedAtMs = Date.now();\n captureSeconds = (capturedAtMs - captureStartMs) / 1000;\n for (const { windowId, isSinglePixel, screenshot } of capturedWindows) {\n // Java creates an unmapped 1x1 window per program, of which `maim` captures the whole screen instead.\n if (!isSinglePixel) recorder?.add(windowId, screenshot, capturedAtMs);\n }\n screenshots = capturedWindows\n .map(({ screenshot }) => screenshot)\n .toSorted((a, b) => a.data.length - b.data.length);\n\n if (screenshots.length > 0) {\n const screenshotSignatures = screenshots.map((file) => file.data).toSorted();\n screenshotSignaturesHistory.unshift(screenshotSignatures);\n screenshotSignaturesHistory.length = Math.min(\n screenshotSignaturesHistory.length,\n context.stopDetectionThreshold\n );\n if (\n screenshotSignaturesHistory.length === context.stopDetectionThreshold &&\n screenshotSignaturesHistory.every(\n (files) =>\n files.length === screenshotSignatures.length &&\n files.every((file, index) => file === screenshotSignatures[index])\n )\n ) {\n stopReason = 'stable_screenshot';\n exitCode = 0;\n break;\n }\n }\n\n if (Date.now() / 1000 - startTimeSeconds > context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n }\n\n if (stopReason !== 'process_exit' && child.exitCode === null) {\n child.removeAllListeners('close');\n child.removeAllListeners('error');\n }\n } finally {\n await stopProcess(child);\n }\n if (spawnError) throw spawnError;\n const {\n memoryBytes,\n stderr: normalizedStderr,\n timeSeconds,\n } = parseTimedStderr(stderr, startTimeSeconds, sampledMemoryBytes);\n\n return {\n stdin: context.stdin,\n stdout: stdout.trimEnd(),\n stderr: normalizedStderr,\n status: exitCode,\n timeSeconds,\n memoryBytes,\n screenshots,\n recordings: recorder?.build(context.screenshotWaitSeconds * 1000) ?? [],\n stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): CapturedWindow[] {\n // Node omits environment entries whose value is undefined, so a missing display is simply not set.\n const env = { ...process.env, DISPLAY: display ?? process.env.DISPLAY };\n const xwininfo = childProcess.spawnSync('xwininfo', ['-root', '-tree'], { encoding: 'utf8', env });\n if (xwininfo.error) throw xwininfo.error;\n if (xwininfo.status !== 0 || !xwininfo.stdout) return [];\n\n const capturedWindows: CapturedWindow[] = [];\n for (const { windowId, isSinglePixel } of extractTopLevelWindows(xwininfo.stdout)) {\n const screenshot = childProcess.spawnSync('maim', ['-i', windowId], { env });\n if (screenshot.error) throw screenshot.error;\n if (screenshot.status !== 0 || screenshot.stdout.length === 0) continue;\n\n const windowNameResult = childProcess.spawnSync('xdotool', ['getwindowname', windowId], { encoding: 'utf8', env });\n if (windowNameResult.error) throw windowNameResult.error;\n const windowName = windowNameResult.stdout.trim().replaceAll(/[\\s/]/g, '_');\n\n capturedWindows.push({\n windowId,\n isSinglePixel,\n screenshot: {\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n },\n });\n }\n\n return capturedWindows;\n}\n\nfunction extractTopLevelWindows(stdout: string): Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] {\n const windows: Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] = [];\n const lines = stdout.split('\\n');\n for (const line of lines) {\n if (line.includes('Root window id:') || line.includes('Parent window id:') || line.includes('()')) continue;\n\n const match = /^\\s{5}(0x[\\da-f]+) /.exec(line);\n if (!match?.[1]) continue;\n windows.push({\n windowId: Number.parseInt(match[1], 16).toString(),\n // A line ends with the geometry, e.g. `400x240+0+0 +0+0`.\n isSinglePixel: / 1x1[+-]\\d+[+-]\\d+ {2}[+-]\\d+[+-]\\d+\\s*$/.test(line),\n });\n }\n return windows;\n}\n\nfunction parseTimedStderr(\n stderr: string,\n startTimeSeconds: number,\n sampledMemoryBytes: number\n): Pick<GuiCommandRunResult, 'stderr' | 'timeSeconds' | 'memoryBytes'> {\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(stderr);\n const normalizedStderr = match ? stderr.slice(0, match.index).trimEnd() : stderr.trimEnd();\n const parsedMemoryBytes = Number(match?.[2]) * 1024 || 0;\n return {\n stderr: normalizedStderr,\n timeSeconds: Number(match?.[1]) || Date.now() / 1000 - startTimeSeconds,\n memoryBytes: Math.max(parsedMemoryBytes, sampledMemoryBytes),\n };\n}\n\nasync function ensureDisplayServer(): Promise<{ display: string; dispose: () => Promise<void> }> {\n if (process.platform !== 'linux') {\n throw new Error('GUI screenshot capture is supported only on Linux.');\n }\n\n for (let displayNumber = 90; displayNumber < 100; displayNumber++) {\n const display = `:${displayNumber}`;\n let spawnError: Error | undefined;\n const xvfb = childProcess.spawn('Xvfb', [display, '-screen', '0', '1280x1024x24', '-ac'], { stdio: 'ignore' });\n xvfb.on('error', (error) => {\n spawnError = error;\n });\n\n await wait(XVFB_STARTUP_WAIT_SECONDS * 1000);\n if (spawnError) throw spawnError;\n if (xvfb.exitCode !== null) continue;\n\n return {\n display,\n dispose: async () => {\n if (!xvfb.killed) {\n xvfb.kill('SIGTERM');\n await wait(XVFB_SHUTDOWN_WAIT_SECONDS * 1000);\n if (xvfb.exitCode === null) xvfb.kill('SIGKILL');\n }\n },\n };\n }\n\n throw new Error('Xvfb could not be started.');\n}\n\nasync function stopProcess(child: childProcess.ChildProcess): Promise<void> {\n if (!child.pid) return;\n killProcessGroup(child.pid, 'SIGTERM');\n await wait(PROCESS_SHUTDOWN_WAIT_SECONDS * 1000);\n if (child.exitCode === null) killProcessGroup(child.pid, 'SIGKILL');\n}\n\nfunction readProcessGroupMemoryBytes(processGroupId: number | undefined): number {\n if (!processGroupId || process.platform !== 'linux') return 0;\n\n const result = childProcess.spawnSync('ps', ['-o', 'rss=', '--no-headers', '--pgroup', String(processGroupId)], {\n encoding: 'utf8',\n });\n if (result.error || result.status !== 0 || !result.stdout) return 0;\n\n return result.stdout\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter((value) => Number.isFinite(value) && value > 0)\n .reduce((sum, value) => sum + value * 1024, 0);\n}\n\nfunction killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-processGroupId, signal);\n return;\n }\n } catch {\n // The process group may already be gone. Fall back to the direct PID below.\n }\n\n try {\n process.kill(processGroupId, signal);\n } catch {\n // The direct child may also already be gone.\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"guiCommand.cjs","names":["os","z","parseArgs","readProblemMarkdownFrontMatter","judgeByStaticAnalysis","findEntryPointFile","DecisionCode","findLanguageDefinitionByPath","getErrorMessage","snapshotWorkingDirectory","copyTestCaseFileInput","cleanWorkingDirectory","readOutputFiles","path","languageIdToDefinition","spawnWithTimeout","readFileTestCases","childProcess","GuiRecorder","wait","fs","require_runtime","require_printTestCaseResult","require_parseArgs","require_readTestCases","require_language","require_findLanguageDefinitionByPath","require_readProblemMarkdownFrontMatter","require_decisionCode","require_guiRecording","require_cleanWorkingDirectory","require_copyTestCaseFileInput","require_findEntryPointFile","require_judgeByStaticAnalysis","require_readOutputFiles","require_spawnWithTimeout","node_path","zod","node_fs_promises","node_os","_willbooster_shared_lib","node_child_process","node_timers_promises","TIME_COMMAND","judgeParamsSchema","guiCommandJudgePreset","args","params","submissionDir","problemMarkdownFrontMatter","problemDir","configuredTestCases","options","readGuiTestCases","testCases","prebuildTestCaseId","staticAnalysisResult","initialMainFilePath","languageDefinition","env","resolvedMainFilePath","resolveMainFilePath","prebuiltMainFilePath","inferLanguageIdsByPath","error","customPrepareResult","prepareResult","runDefaultPrepare","cwdSnapshot","displayServer","currentTestCaseId","currentStdin","ensureDisplayServer","sharedFileInputPath","testCase","timeLimitSeconds","runEnv","stdin","command","runResult","runContext","result","spawnGuiProgram","outputFiles","judgeContext","baseJudgeResult","evaluateGuiRunResult","judgeResult","extendedJudgeResult","decisionCode","stdout","stderr","context","pathExists","languageIds","definition","filePath","ext","languageId","buildCommand","buildResult","buildOutput","requiredOutputFilesCount","child","exitCode","spawnError","stopReason","screenshotSignaturesHistory","screenshots","recorder","startTimeSeconds","sampledMemoryBytes","chunk","code","signal","captureSeconds","nextCaptureEndSeconds","readProcessGroupMemoryBytes","captureStartMs","capturedWindows","takeScreenshots","capturedAtMs","isSinglePixel","windowId","screenshot","a","b","screenshotSignatures","file","files","index","stopProcess","normalizedStderr","parseTimedStderr","display","xwininfo","extractTopLevelWindows","windowNameResult","windowName","windows","lines","line","match","parsedMemoryBytes","displayNumber","xvfb","killProcessGroup","processGroupId","value","sum"],"sources":["../../src/presets/guiCommand.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as wait } from 'node:timers/promises';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { z } from 'zod';\n\nimport { cleanWorkingDirectory, snapshotWorkingDirectory } from '../helpers/cleanWorkingDirectory.js';\nimport { copyTestCaseFileInput } from '../helpers/copyTestCaseFileInput.js';\nimport { findEntryPointFile } from '../helpers/findEntryPointFile.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { GuiRecorder } from '../helpers/guiRecording.js';\nimport type { GuiRecordingFile } from '../helpers/guiRecording.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readOutputFiles } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases as readFileTestCases } from '../helpers/readTestCases.js';\nimport { spawnWithTimeout } from '../helpers/spawnWithTimeout.js';\nimport { MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { languageIdToDefinition } from '../types/language.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst BUILD_TIMEOUT_SECONDS = 10;\nconst JUDGE_DEFAULT_TIMEOUT_SECONDS = 5;\nconst SCREENSHOT_WAIT_SECONDS = 0.3;\nconst XVFB_STARTUP_WAIT_SECONDS = 0.3;\nconst XVFB_SHUTDOWN_WAIT_SECONDS = 0.1;\nconst PROCESS_SHUTDOWN_WAIT_SECONDS = 0.2;\nconst STOP_DETECTION_THRESHOLD = 5;\nconst TIME_COMMAND = [os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time', '--format', '%e %M'] as const;\n\nconst judgeParamsSchema = z.object({\n language: z.union([z.string(), z.array(z.string())]).optional(),\n});\n\n/** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */\ninterface BaseGuiTestCase {\n id: string;\n /** Standard input (`test_cases/<id>.in`). */\n input?: string;\n /** Directory copied into the working directory before the run (`test_cases/<id>.fin/`). */\n fileInputPath?: string;\n}\n\n/** A test case read from `test_cases/` by the default reader. */\nexport interface GuiTestCase extends BaseGuiTestCase {\n /** Expected standard output (`test_cases/<id>.out`), for the problem's `test` to compare. */\n output?: string;\n /** Directory of expected output files (`test_cases/<id>.fout/`), for the problem's `test` to compare. */\n fileOutputPath?: string;\n}\n\nexport interface GuiScreenshotFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\nexport type { GuiRecordingFile } from '../helpers/guiRecording.js';\n\nexport interface GuiCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n screenshots: GuiScreenshotFile[];\n /**\n * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each\n * window that kept changing during the run; a window that only appeared and was painted has none.\n */\n recordings?: GuiRecordingFile[];\n stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\n}\n\ninterface CapturedWindow {\n windowId: string;\n isSinglePixel: boolean;\n screenshot: GuiScreenshotFile;\n}\n\ninterface GuiJudgeContext {\n timeLimitSeconds: number;\n problemMarkdownFrontMatter: Pick<ProblemMarkdownFrontMatter, 'memoryLimitByte' | 'requiredOutputFilePaths'>;\n}\n\ntype GuiJudgeCaseResult = Pick<\n TestCaseResult,\n 'decisionCode' | 'feedbackMarkdown' | 'stderr' | 'stdout' | 'outputFiles'\n>;\n\nexport interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase = GuiTestCase> {\n mainFilePath?: string;\n runTimeoutSeconds?: number;\n screenshotWaitSeconds?: number;\n stopDetectionThreshold?: number;\n /**\n * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends\n * the recording instead of failing the run: a run stopped by it reaches `test` with\n * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.\n */\n recordsAnimation?: boolean;\n readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;\n prepare?: (context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n problemMarkdownFrontMatter: ProblemMarkdownFrontMatter;\n }) => Promise<Partial<GuiJudgeCaseResult> | undefined> | Partial<GuiJudgeCaseResult> | undefined;\n resolveInput?: (context: { testCase: TTestCase; cwd: string; env: NodeJS.ProcessEnv }) => Promise<string> | string;\n command?: (context: {\n testCase: TTestCase;\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n }) => Promise<readonly [string, ...string[]]> | readonly [string, ...string[]];\n runCommand?: (context: {\n testCase: TTestCase;\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: Required<GuiCommandRunResult>;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */\n cwd: string;\n context: GuiJudgeContext;\n }) => Promise<Partial<GuiJudgeCaseResult>> | Partial<GuiJudgeCaseResult>;\n}\n\n/**\n * A preset function for judging GUI programs by collecting screenshots while the program runs.\n *\n * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also\n * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { DecisionCode } from '@exercode/problem-utils';\n * import { guiCommandJudgePreset } from '@exercode/problem-utils/presets/guiCommand';\n *\n * await guiCommandJudgePreset(import.meta.dirname, {\n * mainFilePath: 'Main.java',\n * readTestCases: async () => [{ id: 'default' }],\n * test: ({ runResult }) => {\n * return runResult.screenshots.length > 0\n * ? { decisionCode: DecisionCode.ACCEPTED }\n * : { decisionCode: DecisionCode.WRONG_ANSWER };\n * },\n * });\n * ```\n */\nexport async function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(\n problemDir: string,\n options: GuiCommandJudgePresetOptions<TTestCase>\n): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n const submissionDir = args.cwd;\n\n const problemMarkdownFrontMatter = await readProblemMarkdownFrontMatter(problemDir);\n const configuredTestCases = await (options.readTestCases ?? readGuiTestCases<TTestCase>)(problemDir);\n const testCases =\n configuredTestCases.length > 0 ? configuredTestCases : ([{ id: 'default' }] as unknown as readonly TTestCase[]);\n const prebuildTestCaseId = testCases[0]?.id ?? 'prebuild';\n\n const staticAnalysisResult = await judgeByStaticAnalysis(args.cwd, problemMarkdownFrontMatter);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId: prebuildTestCaseId, ...staticAnalysisResult });\n return;\n }\n\n const initialMainFilePath = options.mainFilePath ?? (await findEntryPointFile(args.cwd, params.language));\n if (!initialMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n const languageDefinition = findLanguageDefinitionByPath(initialMainFilePath);\n if (!languageDefinition) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: 'unsupported language',\n });\n return;\n }\n\n const env = { ...process.env, CI: '', FORCE_COLOR: '0' };\n\n let resolvedMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language,\n configuredMainFilePath: options.mainFilePath,\n });\n if (languageDefinition.prebuild) {\n try {\n await languageDefinition.prebuild(args.cwd);\n const prebuiltMainFilePath = await resolveMainFilePath({\n cwd: args.cwd,\n language: params.language ?? inferLanguageIdsByPath(initialMainFilePath),\n configuredMainFilePath: options.mainFilePath,\n allowConfiguredPathFallback: true,\n });\n if (prebuiltMainFilePath) resolvedMainFilePath = prebuiltMainFilePath;\n } catch (error) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n if (!resolvedMainFilePath) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n stderr: options.mainFilePath\n ? `required main file not found: ${options.mainFilePath}`\n : `main file not found${params.language ? `: language: ${params.language}` : ''}`,\n });\n return;\n }\n\n let customPrepareResult: Partial<GuiJudgeCaseResult> | undefined;\n if (options.prepare) {\n try {\n customPrepareResult = await options.prepare({\n cwd: submissionDir,\n env,\n mainFilePath: resolvedMainFilePath,\n problemMarkdownFrontMatter,\n });\n } catch (error) {\n // Like the `prebuild` step above: report the failed build rather than letting the throw end the\n // run resultless.\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: getErrorMessage(error),\n });\n return;\n }\n }\n const prepareResult =\n customPrepareResult ??\n (await runDefaultPrepare({\n cwd: args.cwd,\n env,\n mainFilePath: resolvedMainFilePath,\n languageDefinition,\n }));\n if (prepareResult) {\n printTestCaseResult({\n testCaseId: prebuildTestCaseId,\n decisionCode: prepareResult.decisionCode ?? DecisionCode.BUILD_ERROR,\n feedbackMarkdown: prepareResult.feedbackMarkdown,\n stderr: prepareResult.stderr,\n stdout: prepareResult.stdout,\n outputFiles: prepareResult.outputFiles,\n });\n return;\n }\n\n const cwdSnapshot = await snapshotWorkingDirectory(args.cwd);\n let displayServer: Awaited<ReturnType<typeof ensureDisplayServer>> | undefined;\n let currentTestCaseId = prebuildTestCaseId;\n let currentStdin: string | undefined;\n try {\n displayServer = options.runCommand ? undefined : await ensureDisplayServer();\n const sharedFileInputPath = (configuredTestCases as { shared?: { fileInputPath?: string } }).shared?.fileInputPath;\n for (const testCase of testCases) {\n currentTestCaseId = testCase.id;\n if (sharedFileInputPath) await copyTestCaseFileInput(sharedFileInputPath, args.cwd);\n if (testCase.fileInputPath) await copyTestCaseFileInput(testCase.fileInputPath, args.cwd);\n\n const timeLimitSeconds =\n typeof problemMarkdownFrontMatter.timeLimitMs === 'number'\n ? problemMarkdownFrontMatter.timeLimitMs / 1000\n : (options.runTimeoutSeconds ?? JUDGE_DEFAULT_TIMEOUT_SECONDS);\n\n const runEnv = displayServer ? { ...env, DISPLAY: displayServer.display } : env;\n const stdin = (await options.resolveInput?.({ testCase, cwd: args.cwd, env: runEnv })) ?? testCase.input ?? '';\n currentStdin = stdin;\n const command =\n (await options.command?.({ testCase, cwd: args.cwd, env: runEnv, mainFilePath: resolvedMainFilePath })) ??\n languageDefinition.command(resolvedMainFilePath);\n\n let runResult: Required<GuiCommandRunResult>;\n try {\n const runContext = {\n command,\n stdin,\n cwd: submissionDir,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n recordsAnimation: options.recordsAnimation ?? false,\n };\n const result = options.runCommand\n ? await options.runCommand({ testCase, ...runContext })\n : await spawnGuiProgram(runContext);\n runResult = { ...result, recordings: result.recordings ?? [] };\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n return;\n }\n\n const outputFiles = await readOutputFiles(args.cwd, problemMarkdownFrontMatter.requiredOutputFilePaths ?? []);\n const judgeContext: GuiJudgeContext = {\n timeLimitSeconds,\n problemMarkdownFrontMatter: {\n memoryLimitByte: problemMarkdownFrontMatter.memoryLimitByte,\n requiredOutputFilePaths: problemMarkdownFrontMatter.requiredOutputFilePaths,\n },\n };\n const baseJudgeResult = evaluateGuiRunResult({\n runResult,\n outputFiles,\n context: judgeContext,\n acceptsTimeout: options.recordsAnimation ?? false,\n });\n let judgeResult = baseJudgeResult;\n if (baseJudgeResult.decisionCode === DecisionCode.ACCEPTED) {\n try {\n const extendedJudgeResult = await options.test({\n testCase,\n runResult,\n outputFiles,\n cwd: args.cwd,\n context: judgeContext,\n });\n judgeResult = {\n decisionCode: extendedJudgeResult.decisionCode ?? baseJudgeResult.decisionCode,\n feedbackMarkdown: extendedJudgeResult.feedbackMarkdown,\n stderr: extendedJudgeResult.stderr,\n stdout: extendedJudgeResult.stdout,\n outputFiles: extendedJudgeResult.outputFiles,\n };\n } catch (error) {\n judgeResult = {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: getErrorMessage(error),\n };\n }\n }\n\n const decisionCode = judgeResult.decisionCode ?? DecisionCode.ACCEPTED;\n const stdout = judgeResult.stdout ?? runResult.stdout;\n const stderr = judgeResult.stderr ?? runResult.stderr;\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode,\n exitStatus: runResult.status,\n stdin: runResult.stdin || undefined,\n stdout: stdout || undefined,\n stderr: stderr || undefined,\n timeSeconds: runResult.timeSeconds,\n memoryBytes: runResult.memoryBytes,\n feedbackMarkdown: judgeResult.feedbackMarkdown,\n outputFiles: judgeResult.outputFiles ?? (outputFiles.length > 0 ? outputFiles : undefined),\n });\n\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n if (decisionCode !== DecisionCode.ACCEPTED) break;\n }\n } catch (error) {\n printTestCaseResult({\n testCaseId: currentTestCaseId,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: currentStdin,\n stderr: getErrorMessage(error),\n });\n await cleanWorkingDirectory(args.cwd, cwdSnapshot);\n } finally {\n await displayServer?.dispose();\n }\n}\n\nasync function resolveMainFilePath(context: {\n cwd: string;\n language?: string | string[];\n configuredMainFilePath?: string;\n allowConfiguredPathFallback?: boolean;\n}): Promise<string | undefined> {\n if (context.configuredMainFilePath) {\n const resolvedPath = path.join(context.cwd, context.configuredMainFilePath);\n if (await pathExists(resolvedPath)) return context.configuredMainFilePath;\n if (!context.allowConfiguredPathFallback) return undefined;\n }\n\n return await findEntryPointFile(context.cwd, context.language);\n}\n\nfunction inferLanguageIdsByPath(filePath: string): string[] | undefined {\n const languageIds = Object.entries(languageIdToDefinition)\n .filter(([, definition]) => definition.fileExtensions.some((ext) => filePath.endsWith(ext)))\n .map(([languageId]) => languageId);\n return languageIds.length > 0 ? languageIds : undefined;\n}\n\nasync function runDefaultPrepare(context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n mainFilePath: string;\n languageDefinition: NonNullable<ReturnType<typeof findLanguageDefinitionByPath>>;\n}): Promise<Partial<GuiJudgeCaseResult> | undefined> {\n const buildCommand = context.languageDefinition.buildCommand?.(context.mainFilePath);\n if (!buildCommand) return undefined;\n\n const buildResult = await spawnWithTimeout(\n buildCommand[0],\n buildCommand.slice(1),\n { cwd: context.cwd, env: context.env },\n BUILD_TIMEOUT_SECONDS\n );\n const buildOutput = (buildResult.stderr || buildResult.stdout).slice(0, MAX_STDOUT_LENGTH) || undefined;\n\n if (buildResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.status !== 0) {\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: buildOutput,\n };\n }\n\n if (buildResult.outputLimitExceeded) {\n return {\n decisionCode: DecisionCode.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: buildOutput,\n };\n }\n\n return undefined;\n}\n\nfunction evaluateGuiRunResult(context: {\n runResult: GuiCommandRunResult;\n outputFiles: NonNullable<TestCaseResult['outputFiles']>;\n context: GuiJudgeContext;\n acceptsTimeout: boolean;\n}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout' && !context.acceptsTimeout) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n if (context.runResult.status !== 0) {\n return {\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stderr: context.runResult.stderr,\n };\n }\n\n if (\n context.runResult.memoryBytes >\n (context.context.problemMarkdownFrontMatter.memoryLimitByte ?? Number.POSITIVE_INFINITY)\n ) {\n return {\n decisionCode: DecisionCode.MEMORY_LIMIT_EXCEEDED,\n stderr: context.runResult.stderr,\n };\n }\n\n const requiredOutputFilesCount = context.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length ?? 0;\n if (context.outputFiles.length < requiredOutputFilesCount) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_OUTPUT_FILE_ERROR,\n };\n }\n\n return { decisionCode: DecisionCode.ACCEPTED };\n}\n\nasync function readGuiTestCases<TTestCase extends BaseGuiTestCase>(problemDir: string): Promise<readonly TTestCase[]> {\n // The default reader yields the base shape; a narrower TTestCase must come from `options.readTestCases`.\n return (await readFileTestCases(path.join(problemDir, 'test_cases'))) as unknown as readonly TTestCase[];\n}\n\nasync function spawnGuiProgram(context: {\n command: readonly [string, ...string[]];\n stdin: string;\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n screenshotWaitSeconds: number;\n stopDetectionThreshold: number;\n recordsAnimation: boolean;\n}): Promise<GuiCommandRunResult> {\n const child = childProcess.spawn(\n 'timeout',\n [context.timeLimitSeconds.toFixed(3), ...TIME_COMMAND, ...context.command],\n {\n cwd: context.cwd,\n env: context.env,\n detached: process.platform !== 'win32',\n stdio: ['pipe', 'pipe', 'pipe'],\n }\n );\n\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | undefined;\n let spawnError: Error | undefined;\n let stopReason: GuiCommandRunResult['stopReason'] = 'process_exit';\n const screenshotSignaturesHistory: string[][] = [];\n let screenshots: GuiScreenshotFile[] = [];\n const recorder = context.recordsAnimation ? new GuiRecorder() : undefined;\n const startTimeSeconds = Date.now() / 1000;\n let sampledMemoryBytes = 0;\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n });\n child.on('error', (error) => {\n spawnError = error;\n exitCode = 1;\n });\n // A child that exits before the input is fully written makes the write fail; without a listener\n // that EPIPE would be an unhandled stream error terminating the harness mid-run.\n child.stdin.on('error', () => {\n // The submission simply stopped reading its input; the exit handling below reports the result.\n });\n child.on('close', (code, signal) => {\n if (code === 124) {\n stopReason = 'timeout';\n exitCode = 0;\n return;\n }\n if (signal) {\n exitCode = 1;\n stderr = stderr\n ? `${stderr}\\nprocess terminated by signal: ${signal}`\n : `process terminated by signal: ${signal}`;\n return;\n }\n exitCode = code ?? 1;\n });\n\n // Stop the program even when a helper throws (e.g. `xwininfo` missing); the spawned process\n // would otherwise outlive this run.\n try {\n if (context.stdin) child.stdin.write(context.stdin);\n child.stdin.end();\n\n let captureSeconds = 0;\n while (exitCode === undefined) {\n // A recorded run that times out reaches `test`, so its last capture must show the program's\n // windows: stop it before a capture that `timeout` would kill the program during.\n const nextCaptureEndSeconds =\n Date.now() / 1000 - startTimeSeconds + context.screenshotWaitSeconds + captureSeconds;\n if (recorder && nextCaptureEndSeconds >= context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const captureStartMs = Date.now();\n const capturedWindows = takeScreenshots(context.env.DISPLAY);\n const capturedAtMs = Date.now();\n captureSeconds = (capturedAtMs - captureStartMs) / 1000;\n for (const { windowId, isSinglePixel, screenshot } of capturedWindows) {\n // Java creates an unmapped 1x1 window per program, of which `maim` captures the whole screen instead.\n if (!isSinglePixel) recorder?.add(windowId, screenshot, capturedAtMs);\n }\n screenshots = capturedWindows\n .map(({ screenshot }) => screenshot)\n .toSorted((a, b) => a.data.length - b.data.length);\n\n if (screenshots.length > 0) {\n const screenshotSignatures = screenshots.map((file) => file.data).toSorted();\n screenshotSignaturesHistory.unshift(screenshotSignatures);\n screenshotSignaturesHistory.length = Math.min(\n screenshotSignaturesHistory.length,\n context.stopDetectionThreshold\n );\n if (\n screenshotSignaturesHistory.length === context.stopDetectionThreshold &&\n screenshotSignaturesHistory.every(\n (files) =>\n files.length === screenshotSignatures.length &&\n files.every((file, index) => file === screenshotSignatures[index])\n )\n ) {\n stopReason = 'stable_screenshot';\n exitCode = 0;\n break;\n }\n }\n\n if (Date.now() / 1000 - startTimeSeconds > context.timeLimitSeconds) {\n stopReason = 'timeout';\n exitCode = 0;\n break;\n }\n }\n\n if (stopReason !== 'process_exit' && child.exitCode === null) {\n child.removeAllListeners('close');\n child.removeAllListeners('error');\n }\n } finally {\n await stopProcess(child);\n }\n if (spawnError) throw spawnError;\n const {\n memoryBytes,\n stderr: normalizedStderr,\n timeSeconds,\n } = parseTimedStderr(stderr, startTimeSeconds, sampledMemoryBytes);\n\n return {\n stdin: context.stdin,\n stdout: stdout.trimEnd(),\n stderr: normalizedStderr,\n status: exitCode,\n timeSeconds,\n memoryBytes,\n screenshots,\n recordings: recorder?.build(context.screenshotWaitSeconds * 1000) ?? [],\n stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): CapturedWindow[] {\n // Node omits environment entries whose value is undefined, so a missing display is simply not set.\n const env = { ...process.env, DISPLAY: display ?? process.env.DISPLAY };\n const xwininfo = childProcess.spawnSync('xwininfo', ['-root', '-tree'], { encoding: 'utf8', env });\n if (xwininfo.error) throw xwininfo.error;\n if (xwininfo.status !== 0 || !xwininfo.stdout) return [];\n\n const capturedWindows: CapturedWindow[] = [];\n for (const { windowId, isSinglePixel } of extractTopLevelWindows(xwininfo.stdout)) {\n const screenshot = childProcess.spawnSync('maim', ['-i', windowId], { env });\n if (screenshot.error) throw screenshot.error;\n if (screenshot.status !== 0 || screenshot.stdout.length === 0) continue;\n\n const windowNameResult = childProcess.spawnSync('xdotool', ['getwindowname', windowId], { encoding: 'utf8', env });\n if (windowNameResult.error) throw windowNameResult.error;\n const windowName = windowNameResult.stdout.trim().replaceAll(/[\\s/]/g, '_');\n\n capturedWindows.push({\n windowId,\n isSinglePixel,\n screenshot: {\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n },\n });\n }\n\n return capturedWindows;\n}\n\nfunction extractTopLevelWindows(stdout: string): Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] {\n const windows: Pick<CapturedWindow, 'windowId' | 'isSinglePixel'>[] = [];\n const lines = stdout.split('\\n');\n for (const line of lines) {\n if (line.includes('Root window id:') || line.includes('Parent window id:') || line.includes('()')) continue;\n\n const match = /^\\s{5}(0x[\\da-f]+) /.exec(line);\n if (!match?.[1]) continue;\n windows.push({\n windowId: Number.parseInt(match[1], 16).toString(),\n // A line ends with the geometry, e.g. `400x240+0+0 +0+0`.\n isSinglePixel: / 1x1[+-]\\d+[+-]\\d+ {2}[+-]\\d+[+-]\\d+\\s*$/.test(line),\n });\n }\n return windows;\n}\n\nfunction parseTimedStderr(\n stderr: string,\n startTimeSeconds: number,\n sampledMemoryBytes: number\n): Pick<GuiCommandRunResult, 'stderr' | 'timeSeconds' | 'memoryBytes'> {\n const match = /(?:^|\\n)(\\d+\\.\\d+) (\\d+)\\s*$/.exec(stderr);\n const normalizedStderr = match ? stderr.slice(0, match.index).trimEnd() : stderr.trimEnd();\n const parsedMemoryBytes = Number(match?.[2]) * 1024 || 0;\n return {\n stderr: normalizedStderr,\n timeSeconds: Number(match?.[1]) || Date.now() / 1000 - startTimeSeconds,\n memoryBytes: Math.max(parsedMemoryBytes, sampledMemoryBytes),\n };\n}\n\nasync function ensureDisplayServer(): Promise<{ display: string; dispose: () => Promise<void> }> {\n if (process.platform !== 'linux') {\n throw new Error('GUI screenshot capture is supported only on Linux.');\n }\n\n for (let displayNumber = 90; displayNumber < 100; displayNumber++) {\n const display = `:${displayNumber}`;\n let spawnError: Error | undefined;\n const xvfb = childProcess.spawn('Xvfb', [display, '-screen', '0', '1280x1024x24', '-ac'], { stdio: 'ignore' });\n xvfb.on('error', (error) => {\n spawnError = error;\n });\n\n await wait(XVFB_STARTUP_WAIT_SECONDS * 1000);\n if (spawnError) throw spawnError;\n if (xvfb.exitCode !== null) continue;\n\n return {\n display,\n dispose: async () => {\n if (!xvfb.killed) {\n xvfb.kill('SIGTERM');\n await wait(XVFB_SHUTDOWN_WAIT_SECONDS * 1000);\n if (xvfb.exitCode === null) xvfb.kill('SIGKILL');\n }\n },\n };\n }\n\n throw new Error('Xvfb could not be started.');\n}\n\nasync function stopProcess(child: childProcess.ChildProcess): Promise<void> {\n if (!child.pid) return;\n killProcessGroup(child.pid, 'SIGTERM');\n await wait(PROCESS_SHUTDOWN_WAIT_SECONDS * 1000);\n if (child.exitCode === null) killProcessGroup(child.pid, 'SIGKILL');\n}\n\nfunction readProcessGroupMemoryBytes(processGroupId: number | undefined): number {\n if (!processGroupId || process.platform !== 'linux') return 0;\n\n const result = childProcess.spawnSync('ps', ['-o', 'rss=', '--no-headers', '--pgroup', String(processGroupId)], {\n encoding: 'utf8',\n });\n if (result.error || result.status !== 0 || !result.stdout) return 0;\n\n return result.stdout\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter((value) => Number.isFinite(value) && value > 0)\n .reduce((sum, value) => sum + value * 1024, 0);\n}\n\nfunction killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-processGroupId, signal);\n return;\n }\n } catch {\n // The process group may already be gone. Fall back to the direct PID below.\n }\n\n try {\n process.kill(processGroupId, signal);\n } catch {\n // The direct child may also already be gone.\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{printTestCaseResult as e}from"../helpers/printTestCaseResult.js";import{parseArgs as t}from"../helpers/parseArgs.js";import{readTestCases as n}from"../helpers/readTestCases.js";import{languageIdToDefinition as r}from"../types/language.js";import{findLanguageDefinitionByPath as i}from"../helpers/findLanguageDefinitionByPath.js";import{readProblemMarkdownFrontMatter as a}from"../helpers/readProblemMarkdownFrontMatter.js";import"../helpers/stdioJudgeRules.js";import{DecisionCode as o}from"../types/decisionCode.js";import{GuiRecorder as s}from"../helpers/guiRecording.js";import{cleanWorkingDirectory as c,snapshotWorkingDirectory as l}from"../helpers/cleanWorkingDirectory.js";import{copyTestCaseFileInput as u}from"../helpers/copyTestCaseFileInput.js";import{findEntryPointFile as d}from"../helpers/findEntryPointFile.js";import{judgeByStaticAnalysis as f}from"../helpers/judgeByStaticAnalysis.js";import{readOutputFiles as p}from"../helpers/readOutputFiles.js";import{spawnWithTimeout as m}from"../helpers/spawnWithTimeout.js";import h from"node:path";import{z as g}from"zod";import _ from"node:fs/promises";import v from"node:os";import{getErrorMessage as y}from"@willbooster/shared-lib";import b from"node:child_process";import{setTimeout as x}from"node:timers/promises";const S=[v.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`],C=g.object({language:g.union([g.string(),g.array(g.string())]).optional()});async function w(n,r){let s=t(process.argv);if(!s.cwd)throw Error(`cwd argument required`);let m=C.parse(s.params),h=s.cwd,g=await a(n),_=await(r.readTestCases??k)(n),v=_.length>0?_:[{id:`default`}],b=v[0]?.id??`prebuild`,x=await f(s.cwd,g);if(x){e({testCaseId:b,...x});return}let S=r.mainFilePath??await d(s.cwd,m.language);if(!S){e({testCaseId:b,decisionCode:o.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${m.language?`: language: ${m.language}`:``}`});return}let w=i(S);if(!w){e({testCaseId:b,decisionCode:o.WRONG_ANSWER,stderr:`unsupported language`});return}let j={...process.env,CI:``,FORCE_COLOR:`0`},M=await T({cwd:s.cwd,language:m.language,configuredMainFilePath:r.mainFilePath});if(w.prebuild)try{await w.prebuild(s.cwd);let e=await T({cwd:s.cwd,language:m.language??E(S),configuredMainFilePath:r.mainFilePath,allowConfiguredPathFallback:!0});e&&(M=e)}catch(t){e({testCaseId:b,decisionCode:o.BUILD_ERROR,stderr:y(t)});return}if(!M){e({testCaseId:b,decisionCode:o.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${m.language?`: language: ${m.language}`:``}`});return}let N;if(r.prepare)try{N=await r.prepare({cwd:h,env:j,mainFilePath:M,problemMarkdownFrontMatter:g})}catch(t){e({testCaseId:b,decisionCode:o.BUILD_ERROR,stderr:y(t)});return}let F=N??await D({cwd:s.cwd,env:j,mainFilePath:M,languageDefinition:w});if(F){e({testCaseId:b,decisionCode:F.decisionCode??o.BUILD_ERROR,feedbackMarkdown:F.feedbackMarkdown,stderr:F.stderr,stdout:F.stdout,outputFiles:F.outputFiles});return}let I=await l(s.cwd),L,R=b,z;try{L=r.runCommand?void 0:await P();let t=_.shared?.fileInputPath;for(let n of v){R=n.id,t&&await u(t,s.cwd),n.fileInputPath&&await u(n.fileInputPath,s.cwd);let i=typeof g.timeLimitMs==`number`?g.timeLimitMs/1e3:r.runTimeoutSeconds??5,a=L?{...j,DISPLAY:L.display}:j,l=await r.resolveInput?.({testCase:n,cwd:s.cwd,env:a})??n.input??``;z=l;let d=await r.command?.({testCase:n,cwd:s.cwd,env:a,mainFilePath:M})??w.command(M),f;try{let e={command:d,stdin:l,cwd:h,env:a,timeLimitSeconds:i,screenshotWaitSeconds:r.screenshotWaitSeconds??.3,stopDetectionThreshold:r.stopDetectionThreshold??5,recordsAnimation:r.recordsAnimation??!1},t=r.runCommand?await r.runCommand({testCase:n,...e}):await A(e);f={...t,recordings:t.recordings??[]}}catch(t){e({testCaseId:n.id,decisionCode:o.RUNTIME_ERROR,stdin:l,stderr:y(t)}),await c(s.cwd,I);return}let m=await p(s.cwd,g.requiredOutputFilePaths??[]),_={timeLimitSeconds:i,problemMarkdownFrontMatter:{memoryLimitByte:g.memoryLimitByte,requiredOutputFilePaths:g.requiredOutputFilePaths}},v=O({runResult:f,outputFiles:m,context:_,acceptsTimeout:r.recordsAnimation??!1}),b=v;if(v.decisionCode===o.ACCEPTED)try{let e=await r.test({testCase:n,runResult:f,outputFiles:m,cwd:s.cwd,context:_});b={decisionCode:e.decisionCode??v.decisionCode,feedbackMarkdown:e.feedbackMarkdown,stderr:e.stderr,stdout:e.stdout,outputFiles:e.outputFiles}}catch(e){b={decisionCode:o.RUNTIME_ERROR,stderr:y(e)}}let x=b.decisionCode??o.ACCEPTED,S=b.stdout??f.stdout,C=b.stderr??f.stderr;if(e({testCaseId:n.id,decisionCode:x,exitStatus:f.status,stdin:f.stdin||void 0,stdout:S||void 0,stderr:C||void 0,timeSeconds:f.timeSeconds,memoryBytes:f.memoryBytes,feedbackMarkdown:b.feedbackMarkdown,outputFiles:b.outputFiles??(m.length>0?m:void 0)}),await c(s.cwd,I),x!==o.ACCEPTED)break}}catch(t){e({testCaseId:R,decisionCode:o.RUNTIME_ERROR,stdin:z,stderr:y(t)}),await c(s.cwd,I)}finally{await L?.dispose()}}async function T(e){if(e.configuredMainFilePath){if(await R(h.join(e.cwd,e.configuredMainFilePath)))return e.configuredMainFilePath;if(!e.allowConfiguredPathFallback)return}return await d(e.cwd,e.language)}function E(e){let t=Object.entries(r).filter(([,t])=>t.fileExtensions.some(t=>e.endsWith(t))).map(([e])=>e);return t.length>0?t:void 0}async function D(e){let t=e.languageDefinition.buildCommand?.(e.mainFilePath);if(!t)return;let n=await m(t[0],t.slice(1),{cwd:e.cwd,env:e.env},10),r=(n.stderr||n.stdout).slice(0,5e4)||void 0;if(n.timeSeconds>10)return{decisionCode:o.BUILD_TIME_LIMIT_EXCEEDED,stderr:r};if(n.status!==0)return{decisionCode:o.BUILD_ERROR,stderr:r};if(n.outputLimitExceeded)return{decisionCode:o.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:r}}function O(e){if(e.runResult.stopReason===`timeout`&&!e.acceptsTimeout)return{decisionCode:o.TIME_LIMIT_EXCEEDED,stderr:e.runResult.stderr};if(e.runResult.status!==0)return{decisionCode:o.RUNTIME_ERROR,stderr:e.runResult.stderr};if(e.runResult.memoryBytes>(e.context.problemMarkdownFrontMatter.memoryLimitByte??1/0))return{decisionCode:o.MEMORY_LIMIT_EXCEEDED,stderr:e.runResult.stderr};let t=e.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length??0;return e.outputFiles.length<t?{decisionCode:o.MISSING_REQUIRED_OUTPUT_FILE_ERROR}:{decisionCode:o.ACCEPTED}}async function k(e){return await n(h.join(e,`test_cases`))}async function A(e){let t=b.spawn(`timeout`,[e.timeLimitSeconds.toFixed(3),...S,...e.command],{cwd:e.cwd,env:e.env,detached:process.platform!==`win32`,stdio:[`pipe`,`pipe`,`pipe`]});t.stdout.setEncoding(`utf8`),t.stderr.setEncoding(`utf8`);let n=``,r=``,i,a,o=`process_exit`,c=[],l=[],u=e.recordsAnimation?new s:void 0,d=Date.now()/1e3,f=0;t.stdout.on(`data`,e=>{n+=e}),t.stderr.on(`data`,e=>{r+=e}),t.on(`error`,e=>{a=e,i=1}),t.stdin.on(`error`,()=>{}),t.on(`close`,(e,t)=>{
|
|
1
|
+
import{printTestCaseResult as e}from"../helpers/printTestCaseResult.js";import{parseArgs as t}from"../helpers/parseArgs.js";import{readTestCases as n}from"../helpers/readTestCases.js";import{languageIdToDefinition as r}from"../types/language.js";import{findLanguageDefinitionByPath as i}from"../helpers/findLanguageDefinitionByPath.js";import{readProblemMarkdownFrontMatter as a}from"../helpers/readProblemMarkdownFrontMatter.js";import"../helpers/stdioJudgeRules.js";import{DecisionCode as o}from"../types/decisionCode.js";import{GuiRecorder as s}from"../helpers/guiRecording.js";import{cleanWorkingDirectory as c,snapshotWorkingDirectory as l}from"../helpers/cleanWorkingDirectory.js";import{copyTestCaseFileInput as u}from"../helpers/copyTestCaseFileInput.js";import{findEntryPointFile as d}from"../helpers/findEntryPointFile.js";import{judgeByStaticAnalysis as f}from"../helpers/judgeByStaticAnalysis.js";import{readOutputFiles as p}from"../helpers/readOutputFiles.js";import{spawnWithTimeout as m}from"../helpers/spawnWithTimeout.js";import h from"node:path";import{z as g}from"zod";import _ from"node:fs/promises";import v from"node:os";import{getErrorMessage as y}from"@willbooster/shared-lib";import b from"node:child_process";import{setTimeout as x}from"node:timers/promises";const S=[v.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`],C=g.object({language:g.union([g.string(),g.array(g.string())]).optional()});async function w(n,r){let s=t(process.argv);if(!s.cwd)throw Error(`cwd argument required`);let m=C.parse(s.params),h=s.cwd,g=await a(n),_=await(r.readTestCases??k)(n),v=_.length>0?_:[{id:`default`}],b=v[0]?.id??`prebuild`,x=await f(s.cwd,g);if(x){e({testCaseId:b,...x});return}let S=r.mainFilePath??await d(s.cwd,m.language);if(!S){e({testCaseId:b,decisionCode:o.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${m.language?`: language: ${m.language}`:``}`});return}let w=i(S);if(!w){e({testCaseId:b,decisionCode:o.WRONG_ANSWER,stderr:`unsupported language`});return}let j={...process.env,CI:``,FORCE_COLOR:`0`},M=await T({cwd:s.cwd,language:m.language,configuredMainFilePath:r.mainFilePath});if(w.prebuild)try{await w.prebuild(s.cwd);let e=await T({cwd:s.cwd,language:m.language??E(S),configuredMainFilePath:r.mainFilePath,allowConfiguredPathFallback:!0});e&&(M=e)}catch(t){e({testCaseId:b,decisionCode:o.BUILD_ERROR,stderr:y(t)});return}if(!M){e({testCaseId:b,decisionCode:o.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,stderr:r.mainFilePath?`required main file not found: ${r.mainFilePath}`:`main file not found${m.language?`: language: ${m.language}`:``}`});return}let N;if(r.prepare)try{N=await r.prepare({cwd:h,env:j,mainFilePath:M,problemMarkdownFrontMatter:g})}catch(t){e({testCaseId:b,decisionCode:o.BUILD_ERROR,stderr:y(t)});return}let F=N??await D({cwd:s.cwd,env:j,mainFilePath:M,languageDefinition:w});if(F){e({testCaseId:b,decisionCode:F.decisionCode??o.BUILD_ERROR,feedbackMarkdown:F.feedbackMarkdown,stderr:F.stderr,stdout:F.stdout,outputFiles:F.outputFiles});return}let I=await l(s.cwd),L,R=b,z;try{L=r.runCommand?void 0:await P();let t=_.shared?.fileInputPath;for(let n of v){R=n.id,t&&await u(t,s.cwd),n.fileInputPath&&await u(n.fileInputPath,s.cwd);let i=typeof g.timeLimitMs==`number`?g.timeLimitMs/1e3:r.runTimeoutSeconds??5,a=L?{...j,DISPLAY:L.display}:j,l=await r.resolveInput?.({testCase:n,cwd:s.cwd,env:a})??n.input??``;z=l;let d=await r.command?.({testCase:n,cwd:s.cwd,env:a,mainFilePath:M})??w.command(M),f;try{let e={command:d,stdin:l,cwd:h,env:a,timeLimitSeconds:i,screenshotWaitSeconds:r.screenshotWaitSeconds??.3,stopDetectionThreshold:r.stopDetectionThreshold??5,recordsAnimation:r.recordsAnimation??!1},t=r.runCommand?await r.runCommand({testCase:n,...e}):await A(e);f={...t,recordings:t.recordings??[]}}catch(t){e({testCaseId:n.id,decisionCode:o.RUNTIME_ERROR,stdin:l,stderr:y(t)}),await c(s.cwd,I);return}let m=await p(s.cwd,g.requiredOutputFilePaths??[]),_={timeLimitSeconds:i,problemMarkdownFrontMatter:{memoryLimitByte:g.memoryLimitByte,requiredOutputFilePaths:g.requiredOutputFilePaths}},v=O({runResult:f,outputFiles:m,context:_,acceptsTimeout:r.recordsAnimation??!1}),b=v;if(v.decisionCode===o.ACCEPTED)try{let e=await r.test({testCase:n,runResult:f,outputFiles:m,cwd:s.cwd,context:_});b={decisionCode:e.decisionCode??v.decisionCode,feedbackMarkdown:e.feedbackMarkdown,stderr:e.stderr,stdout:e.stdout,outputFiles:e.outputFiles}}catch(e){b={decisionCode:o.RUNTIME_ERROR,stderr:y(e)}}let x=b.decisionCode??o.ACCEPTED,S=b.stdout??f.stdout,C=b.stderr??f.stderr;if(e({testCaseId:n.id,decisionCode:x,exitStatus:f.status,stdin:f.stdin||void 0,stdout:S||void 0,stderr:C||void 0,timeSeconds:f.timeSeconds,memoryBytes:f.memoryBytes,feedbackMarkdown:b.feedbackMarkdown,outputFiles:b.outputFiles??(m.length>0?m:void 0)}),await c(s.cwd,I),x!==o.ACCEPTED)break}}catch(t){e({testCaseId:R,decisionCode:o.RUNTIME_ERROR,stdin:z,stderr:y(t)}),await c(s.cwd,I)}finally{await L?.dispose()}}async function T(e){if(e.configuredMainFilePath){if(await R(h.join(e.cwd,e.configuredMainFilePath)))return e.configuredMainFilePath;if(!e.allowConfiguredPathFallback)return}return await d(e.cwd,e.language)}function E(e){let t=Object.entries(r).filter(([,t])=>t.fileExtensions.some(t=>e.endsWith(t))).map(([e])=>e);return t.length>0?t:void 0}async function D(e){let t=e.languageDefinition.buildCommand?.(e.mainFilePath);if(!t)return;let n=await m(t[0],t.slice(1),{cwd:e.cwd,env:e.env},10),r=(n.stderr||n.stdout).slice(0,5e4)||void 0;if(n.timeSeconds>10)return{decisionCode:o.BUILD_TIME_LIMIT_EXCEEDED,stderr:r};if(n.status!==0)return{decisionCode:o.BUILD_ERROR,stderr:r};if(n.outputLimitExceeded)return{decisionCode:o.BUILD_OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:r}}function O(e){if(e.runResult.stopReason===`timeout`&&!e.acceptsTimeout)return{decisionCode:o.TIME_LIMIT_EXCEEDED,stderr:e.runResult.stderr};if(e.runResult.status!==0)return{decisionCode:o.RUNTIME_ERROR,stderr:e.runResult.stderr};if(e.runResult.memoryBytes>(e.context.problemMarkdownFrontMatter.memoryLimitByte??1/0))return{decisionCode:o.MEMORY_LIMIT_EXCEEDED,stderr:e.runResult.stderr};let t=e.context.problemMarkdownFrontMatter.requiredOutputFilePaths?.length??0;return e.outputFiles.length<t?{decisionCode:o.MISSING_REQUIRED_OUTPUT_FILE_ERROR}:{decisionCode:o.ACCEPTED}}async function k(e){return await n(h.join(e,`test_cases`))}async function A(e){let t=b.spawn(`timeout`,[e.timeLimitSeconds.toFixed(3),...S,...e.command],{cwd:e.cwd,env:e.env,detached:process.platform!==`win32`,stdio:[`pipe`,`pipe`,`pipe`]});t.stdout.setEncoding(`utf8`),t.stderr.setEncoding(`utf8`);let n=``,r=``,i,a,o=`process_exit`,c=[],l=[],u=e.recordsAnimation?new s:void 0,d=Date.now()/1e3,f=0;t.stdout.on(`data`,e=>{n+=e}),t.stderr.on(`data`,e=>{r+=e}),t.on(`error`,e=>{a=e,i=1}),t.stdin.on(`error`,()=>{}),t.on(`close`,(e,t)=>{e===124?(o=`timeout`,i=0):t?(i=1,r=r?`${r}\nprocess terminated by signal: ${t}`:`process terminated by signal: ${t}`):i=e??1});try{e.stdin&&t.stdin.write(e.stdin),t.stdin.end();let n=0;for(;i===void 0;){let r=Date.now()/1e3-d+e.screenshotWaitSeconds+n;if(u&&r>=e.timeLimitSeconds){o=`timeout`,i=0;break}await x(e.screenshotWaitSeconds*1e3),f=Math.max(f,I(t.pid));let a=Date.now(),s=j(e.env.DISPLAY),p=Date.now();n=(p-a)/1e3;for(let{windowId:e,isSinglePixel:t,screenshot:n}of s)t||u?.add(e,n,p);if(l=s.map(({screenshot:e})=>e).toSorted((e,t)=>e.data.length-t.data.length),l.length>0){let t=l.map(e=>e.data).toSorted();if(c.unshift(t),c.length=Math.min(c.length,e.stopDetectionThreshold),c.length===e.stopDetectionThreshold&&c.every(e=>e.length===t.length&&e.every((e,n)=>e===t[n]))){o=`stable_screenshot`,i=0;break}}if(Date.now()/1e3-d>e.timeLimitSeconds){o=`timeout`,i=0;break}}o!==`process_exit`&&t.exitCode===null&&(t.removeAllListeners(`close`),t.removeAllListeners(`error`))}finally{await F(t)}if(a)throw a;let{memoryBytes:p,stderr:m,timeSeconds:h}=N(r,d,f);return{stdin:e.stdin,stdout:n.trimEnd(),stderr:m,status:i,timeSeconds:h,memoryBytes:p,screenshots:l,recordings:u?.build(e.screenshotWaitSeconds*1e3)??[],stopReason:o}}function j(e){let t={...process.env,DISPLAY:e??process.env.DISPLAY},n=b.spawnSync(`xwininfo`,[`-root`,`-tree`],{encoding:`utf8`,env:t});if(n.error)throw n.error;if(n.status!==0||!n.stdout)return[];let r=[];for(let{windowId:e,isSinglePixel:i}of M(n.stdout)){let n=b.spawnSync(`maim`,[`-i`,e],{env:t});if(n.error)throw n.error;if(n.status!==0||n.stdout.length===0)continue;let a=b.spawnSync(`xdotool`,[`getwindowname`,e],{encoding:`utf8`,env:t});if(a.error)throw a.error;let o=a.stdout.trim().replaceAll(/[\s/]/g,`_`);r.push({windowId:e,isSinglePixel:i,screenshot:{path:`${o||`window`}_${e}.png`,data:n.stdout.toString(`base64`),encoding:`base64`}})}return r}function M(e){let t=[],n=e.split(`
|
|
2
2
|
`);for(let e of n){if(e.includes(`Root window id:`)||e.includes(`Parent window id:`)||e.includes(`()`))continue;let n=/^\s{5}(0x[\da-f]+) /.exec(e);n?.[1]&&t.push({windowId:Number.parseInt(n[1],16).toString(),isSinglePixel:/ 1x1[+-]\d+[+-]\d+ {2}[+-]\d+[+-]\d+\s*$/.test(e)})}return t}function N(e,t,n){let r=/(?:^|\n)(\d+\.\d+) (\d+)\s*$/.exec(e),i=r?e.slice(0,r.index).trimEnd():e.trimEnd(),a=Number(r?.[2])*1024||0;return{stderr:i,timeSeconds:Number(r?.[1])||Date.now()/1e3-t,memoryBytes:Math.max(a,n)}}async function P(){if(process.platform!==`linux`)throw Error(`GUI screenshot capture is supported only on Linux.`);for(let e=90;e<100;e++){let t=`:${e}`,n,r=b.spawn(`Xvfb`,[t,`-screen`,`0`,`1280x1024x24`,`-ac`],{stdio:`ignore`});if(r.on(`error`,e=>{n=e}),await x(300),n)throw n;if(r.exitCode===null)return{display:t,dispose:async()=>{r.killed||(r.kill(`SIGTERM`),await x(100),r.exitCode===null&&r.kill(`SIGKILL`))}}}throw Error(`Xvfb could not be started.`)}async function F(e){e.pid&&(L(e.pid,`SIGTERM`),await x(200),e.exitCode===null&&L(e.pid,`SIGKILL`))}function I(e){if(!e||process.platform!==`linux`)return 0;let t=b.spawnSync(`ps`,[`-o`,`rss=`,`--no-headers`,`--pgroup`,String(e)],{encoding:`utf8`});return t.error||t.status!==0||!t.stdout?0:t.stdout.split(`
|
|
3
3
|
`).map(e=>Number(e.trim())).filter(e=>Number.isFinite(e)&&e>0).reduce((e,t)=>e+t*1024,0)}function L(e,t){try{if(process.platform!==`win32`){process.kill(-e,t);return}}catch{}try{process.kill(e,t)}catch{}}async function R(e){try{return await _.access(e),!0}catch{return!1}}export{w as guiCommandJudgePreset};
|
|
4
4
|
//# sourceMappingURL=guiCommand.js.map
|