@exercode/problem-utils 4.2.6 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.js +1 -1
  4. package/dist/cli/checkAllProblems.js.map +1 -1
  5. package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
  6. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  7. package/dist/helpers/checkProblemDirIsolation.js +1 -1
  8. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  9. package/dist/helpers/findModelAnswerDirs.cjs +1 -1
  10. package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
  11. package/dist/helpers/findModelAnswerDirs.js +1 -1
  12. package/dist/helpers/findModelAnswerDirs.js.map +1 -1
  13. package/dist/helpers/guiRecording.cjs +2 -0
  14. package/dist/helpers/guiRecording.cjs.map +1 -0
  15. package/dist/helpers/guiRecording.d.ts +32 -0
  16. package/dist/helpers/guiRecording.js +2 -0
  17. package/dist/helpers/guiRecording.js.map +1 -0
  18. package/dist/helpers/judgeByStaticAnalysis.cjs +1 -1
  19. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  20. package/dist/helpers/judgeByStaticAnalysis.js +1 -1
  21. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  22. package/dist/helpers/readTestCases.cjs +1 -1
  23. package/dist/helpers/readTestCases.cjs.map +1 -1
  24. package/dist/helpers/readTestCases.js +1 -1
  25. package/dist/helpers/readTestCases.js.map +1 -1
  26. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
  27. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  28. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
  29. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  30. package/dist/helpers/runHarnessProcess.cjs +1 -1
  31. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  32. package/dist/helpers/runHarnessProcess.js +1 -1
  33. package/dist/helpers/runHarnessProcess.js.map +1 -1
  34. package/dist/helpers/spawnWithLimits.cjs +1 -1
  35. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  36. package/dist/helpers/spawnWithLimits.js +1 -1
  37. package/dist/helpers/spawnWithLimits.js.map +1 -1
  38. package/dist/index.cjs +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +1 -1
  41. package/dist/learningMaterial/fsHelpers.cjs +1 -1
  42. package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
  43. package/dist/learningMaterial/fsHelpers.js +1 -1
  44. package/dist/learningMaterial/fsHelpers.js.map +1 -1
  45. package/dist/learningMaterial/validateContest.cjs +1 -1
  46. package/dist/learningMaterial/validateContest.cjs.map +1 -1
  47. package/dist/learningMaterial/validateContest.js +1 -1
  48. package/dist/learningMaterial/validateContest.js.map +1 -1
  49. package/dist/learningMaterial/validateCourse.cjs +1 -1
  50. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  51. package/dist/learningMaterial/validateCourse.js +1 -1
  52. package/dist/learningMaterial/validateCourse.js.map +1 -1
  53. package/dist/learningMaterial/validateMaterial.cjs +3 -3
  54. package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
  55. package/dist/learningMaterial/validateMaterial.js +3 -3
  56. package/dist/learningMaterial/validateMaterial.js.map +1 -1
  57. package/dist/learningMaterial/validateProblem.cjs +1 -1
  58. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  59. package/dist/learningMaterial/validateProblem.js +1 -1
  60. package/dist/learningMaterial/validateProblem.js.map +1 -1
  61. package/dist/learningMaterial/validationResult.cjs +1 -1
  62. package/dist/learningMaterial/validationResult.cjs.map +1 -1
  63. package/dist/learningMaterial/validationResult.js +1 -1
  64. package/dist/learningMaterial/validationResult.js.map +1 -1
  65. package/dist/presets/command.cjs +1 -1
  66. package/dist/presets/command.cjs.map +1 -1
  67. package/dist/presets/command.js +1 -1
  68. package/dist/presets/command.js.map +1 -1
  69. package/dist/presets/evaluation.cjs +2 -2
  70. package/dist/presets/evaluation.cjs.map +1 -1
  71. package/dist/presets/evaluation.js +2 -2
  72. package/dist/presets/evaluation.js.map +1 -1
  73. package/dist/presets/guiCommand.cjs +3 -3
  74. package/dist/presets/guiCommand.cjs.map +1 -1
  75. package/dist/presets/guiCommand.d.ts +17 -3
  76. package/dist/presets/guiCommand.js +3 -3
  77. package/dist/presets/guiCommand.js.map +1 -1
  78. package/dist/presets/springBoot.cjs +4 -4
  79. package/dist/presets/springBoot.cjs.map +1 -1
  80. package/dist/presets/springBoot.js +4 -4
  81. package/dist/presets/springBoot.js.map +1 -1
  82. package/dist/presets/stdio.cjs +1 -1
  83. package/dist/presets/stdio.cjs.map +1 -1
  84. package/dist/presets/stdio.js +1 -1
  85. package/dist/presets/stdio.js.map +1 -1
  86. package/dist/presets/tomcat.cjs +4 -4
  87. package/dist/presets/tomcat.cjs.map +1 -1
  88. package/dist/presets/tomcat.js +4 -4
  89. package/dist/presets/tomcat.js.map +1 -1
  90. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"evaluation.js","names":[],"sources":["../../src/presets/evaluation.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { CsvParseError, type CsvParseErrorReason, parseCsv, parseCsvRecords } from '../helpers/parseCsv.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { passesIsolationCheckInDebugMode } from '../helpers/runIsolationCheckInDebugMode.js';\nimport {\n printDebugCwdBanner,\n printDebugExpectationFailureBanner,\n resolveCwds,\n type ResolvedCwd,\n} from '../helpers/resolveCwds.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst DEFAULT_SUBMISSION_FILE_PATH = 'submission.csv';\nconst TEST_CASE_ID = 'evaluation';\nconst MAX_LISTED_IDS = 10;\n// Learner-controlled cells are echoed into the feedback, so clip them to keep the result line small.\nconst MAX_ECHOED_CELL_LENGTH = 50;\nconst CSV_PARSE_ERROR_MESSAGES: Record<CsvParseErrorReason, string> = {\n unterminated_quote: '引用符(`\"`)が閉じられていません',\n unexpected_quote: '引用符(`\"`)は値全体を囲む位置にしか書けません',\n text_after_quoted_field: '閉じ引用符の後に区切り文字以外の文字があります',\n};\n\nexport interface EvaluationMetric {\n /** Label shown with the score, e.g. `RMSLE`. */\n label: string;\n isHigherBetter: boolean;\n /**\n * Returns an error message when a predicted value cannot be scored (e.g. it is not a number).\n * The message is appended to the row's line number and the offending value, so phrase it as a\n * predicate such as `数値ではありません`. It also validates the ground truth when it is loaded.\n */\n validatePrediction?: (prediction: string) => string | undefined;\n /** Computes the score from predictions and answers aligned by id. Both are raw CSV cell strings. */\n compute: (predictions: readonly string[], answers: readonly string[]) => number;\n}\n\nexport interface EvaluationJudgePresetOptions {\n /** CSV file holding the hidden ground truth, relative to the problem directory. */\n answerFilePath: string;\n /** CSV file the submission must contain. Defaults to `submission.csv`. */\n submissionFilePath?: string;\n /** Column that identifies each row in both CSV files. */\n idColumn: string;\n /** Column holding the answer in the ground truth and the prediction in the submission. */\n targetColumn: string;\n metric: EvaluationMetric;\n /**\n * Score required for acceptance: the submission is accepted when its score is at least this value\n * for a higher-is-better metric and at most this value otherwise. Every valid submission is\n * accepted when omitted.\n */\n acceptableScore?: number;\n}\n\nexport const evaluationMetrics = {\n rmsle: {\n label: 'RMSLE',\n isHigherBetter: false,\n validatePrediction: (prediction: string) =>\n validateFiniteNumber(prediction) ?? (Number(prediction) < 0 ? '負の値です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(\n mean(\n predictions.map(\n (prediction, index) => (Math.log1p(Number(prediction)) - Math.log1p(Number(answers[index]))) ** 2\n )\n )\n ),\n },\n rmse: {\n label: 'RMSE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(mean(predictions.map((prediction, index) => (Number(prediction) - Number(answers[index])) ** 2))),\n },\n mae: {\n label: 'MAE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => Math.abs(Number(prediction) - Number(answers[index])))),\n },\n accuracy: {\n label: 'Accuracy',\n isHigherBetter: true,\n validatePrediction: (prediction: string) => (prediction.trim() === '' ? '空です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => (prediction.trim() === answers[index]?.trim() ? 1 : 0))),\n },\n} as const satisfies Record<string, EvaluationMetric>;\n\n/**\n * A preset for Kaggle-style model evaluation problems: the submission is a CSV of predictions for a\n * hidden test set, and the judge reports the metric value as `score` instead of comparing outputs.\n * The problem directory keeps the ground truth CSV, so it is never shipped to learners.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { evaluationJudgePreset, evaluationMetrics } from '@exercode/problem-utils/presets/evaluation';\n *\n * await evaluationJudgePreset(import.meta.dirname, {\n * answerFilePath: 'evaluation/answer.csv',\n * idColumn: 'Id',\n * targetColumn: 'TradePrice',\n * metric: evaluationMetrics.rmsle,\n * acceptableScore: 0.5,\n * });\n * ```\n *\n * Run `bun judge.ts model_answers/default` to judge one submission, or `bun judge.ts` to judge every\n * `<problemDir>/model_answers/*` and `<problemDir>/model_answers.fails/*` directory for debugging.\n */\nexport async function evaluationJudgePreset(problemDir: string, options: EvaluationJudgePresetOptions): Promise<void> {\n const args = parseArgs(process.argv);\n const { cwds, isDebugMode } = await resolveCwds(problemDir, args.cwd);\n\n if (isDebugMode && !(await passesIsolationCheckInDebugMode(problemDir, cwds, args.params))) {\n process.exitCode = 1;\n return;\n }\n\n const answer = await readAnswer(path.join(problemDir, options.answerFilePath), options);\n for (const resolvedCwd of cwds) {\n if (isDebugMode) printDebugCwdBanner(problemDir, resolvedCwd);\n const result = await judgeSubmission(problemDir, resolvedCwd.cwd, answer, options);\n printTestCaseResult({ testCaseId: TEST_CASE_ID, ...result });\n if (isDebugMode && !matchesExpectedResult(resolvedCwd, result)) {\n process.exitCode = 1;\n printDebugExpectationFailureBanner(problemDir, resolvedCwd);\n }\n }\n}\n\nasync function judgeSubmission(\n problemDir: string,\n cwd: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Promise<Omit<TestCaseResult, 'testCaseId'>> {\n const staticAnalysisResult = await judgeByStaticAnalysis(cwd, await readProblemMarkdownFrontMatter(problemDir));\n if (staticAnalysisResult) return staticAnalysisResult;\n\n const submissionFilePath = options.submissionFilePath ?? DEFAULT_SUBMISSION_FILE_PATH;\n let submissionText: string;\n try {\n const resolvedPath = path.join(cwd, submissionFilePath);\n submissionText = await fs.readFile(resolvedPath, 'utf8');\n } catch {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n feedbackMarkdown: `\\`${submissionFilePath}\\`が見つかりません。予測結果のCSVファイルを\\`${submissionFilePath}\\`という名前で提出してください。`,\n };\n }\n\n const predictions = readPredictions(submissionText, submissionFilePath, answer, options);\n if (typeof predictions === 'string') {\n return { decisionCode: DecisionCode.WRONG_ANSWER, feedbackMarkdown: predictions };\n }\n\n const answerValues = [...answer.values()];\n const score = options.metric.compute(\n [...answer.keys()].map((id) => predictions.get(id) as string),\n answerValues\n );\n if (!Number.isFinite(score)) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n feedbackMarkdown: `予測値が極端なため、${options.metric.label}を計算できませんでした。予測値を見直してください。`,\n };\n }\n const isAccepted =\n options.acceptableScore === undefined ||\n (options.metric.isHigherBetter ? score >= options.acceptableScore : score <= options.acceptableScore);\n return {\n decisionCode: isAccepted ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER,\n score,\n scoreLabel: options.metric.label,\n feedbackMarkdown: buildScoreMarkdown(score, answerValues.length, isAccepted, options),\n };\n}\n\nfunction buildScoreMarkdown(\n score: number,\n rowCount: number,\n isAccepted: boolean,\n options: EvaluationJudgePresetOptions\n): string {\n const { label, isHigherBetter } = options.metric;\n const precision = findDistinguishingPrecision(score, options.acceptableScore);\n const acceptanceLine =\n options.acceptableScore === undefined\n ? ''\n : `\\n合格基準: ${label} ${isHigherBetter ? '≧' : '≦'} ${formatScore(options.acceptableScore, precision)}(${isAccepted ? '達成' : '未達成'})`;\n return `| 指標 | スコア |\n| ---- | ------ |\n| ${label} | ${formatScore(score, precision)} |\n\n評価件数: ${rowCount.toLocaleString('en-US')}件${acceptanceLine}\n`;\n}\n\n/**\n * Finds the number of significant digits at which the score and the threshold render differently\n * (or stop changing), so the feedback never shows a pair that contradicts the decision.\n */\nfunction findDistinguishingPrecision(score: number, threshold: number | undefined): number {\n const minimumPrecision = 6;\n if (threshold === undefined || score === threshold) return minimumPrecision;\n for (let precision = minimumPrecision; precision < 17; precision++) {\n if (formatScore(score, precision) !== formatScore(threshold, precision)) return precision;\n }\n return 17;\n}\n\nfunction formatScore(score: number, precision: number): string {\n return Number(score.toPrecision(precision)).toString();\n}\n\nfunction readPredictions(\n submissionText: string,\n submissionFilePath: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Map<string, string> | string {\n let records: ReturnType<typeof parseCsvRecords>;\n try {\n records = parseCsvRecords(submissionText);\n } catch (error) {\n const detail =\n error instanceof CsvParseError ? `${error.lineNumber}行目付近: ${CSV_PARSE_ERROR_MESSAGES[error.reason]}` : '';\n return `\\`${submissionFilePath}\\`をCSVとして読み込めませんでした。${detail}`;\n }\n const columns = findColumns(records[0]?.cells ?? [], options);\n if (!columns) {\n return `\\`${submissionFilePath}\\`の1行目には\\`${options.idColumn}\\`列と\\`${options.targetColumn}\\`列のヘッダーが必要です。`;\n }\n\n const predictions = new Map<string, string>();\n const problems: string[] = [];\n let problemCount = 0;\n const reportProblem = (problem: string): void => {\n problemCount++;\n if (problems.length < MAX_LISTED_IDS) problems.push(problem);\n };\n for (const { cells: row, lineNumber } of records.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const prediction = row[columns.targetIndex] ?? '';\n if (!answer.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行は評価対象ではありません`);\n } else if (predictions.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行が重複しています`);\n } else {\n const validationError = options.metric.validatePrediction?.(prediction);\n if (validationError) {\n reportProblem(`${lineNumber}行目: \\`${options.targetColumn}\\`の値\\`${clip(prediction)}\\`は${validationError}`);\n }\n predictions.set(id, prediction);\n }\n }\n const missingIds = [...answer.keys()].filter((id) => !predictions.has(id));\n if (missingIds.length > 0) {\n // Missing rows are the most actionable problem, so they must survive the listing cap.\n problemCount++;\n problems.unshift(\n `\\`${options.idColumn}\\`が${listIds(missingIds)}の行がありません(不足 ${missingIds.length.toLocaleString('en-US')}件)`\n );\n }\n if (problemCount > 0) {\n return `\\`${submissionFilePath}\\`の内容に問題があります。\n\n${problems.map((problem) => `- ${problem}`).join('\\n')}${problemCount > problems.length ? `\\n- ほか${(problemCount - problems.length).toLocaleString('en-US')}件` : ''}\n`;\n }\n return predictions;\n}\n\nasync function readAnswer(answerFilePath: string, options: EvaluationJudgePresetOptions): Promise<Map<string, string>> {\n const rows = parseCsv(await fs.readFile(answerFilePath, 'utf8'));\n const columns = findColumns(rows[0] ?? [], options);\n if (!columns) {\n throw new Error(`answer file must have ${options.idColumn} and ${options.targetColumn} columns: ${answerFilePath}`);\n }\n const answer = new Map<string, string>();\n for (const row of rows.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const value = row[columns.targetIndex] ?? '';\n if (!id) throw new Error(`empty ${options.idColumn} in answer file: ${answerFilePath}`);\n if (answer.has(id)) throw new Error(`duplicate ${options.idColumn} in answer file: ${id}`);\n const validationError = options.metric.validatePrediction?.(value);\n if (validationError)\n throw new Error(`invalid ${options.targetColumn} in answer file for ${id}: ${validationError}`);\n answer.set(id, value);\n }\n if (answer.size === 0) throw new Error(`answer file has no rows: ${answerFilePath}`);\n return answer;\n}\n\nfunction findColumns(\n header: readonly string[],\n options: EvaluationJudgePresetOptions\n): { idIndex: number; targetIndex: number } | undefined {\n const names = header.map((name) => name.trim());\n const idIndex = names.indexOf(options.idColumn);\n const targetIndex = names.indexOf(options.targetColumn);\n return idIndex === -1 || targetIndex === -1 ? undefined : { idIndex, targetIndex };\n}\n\nfunction isBlankRow(row: readonly string[]): boolean {\n return row.every((cell) => cell.trim() === '');\n}\n\nfunction listIds(ids: readonly string[]): string {\n const listed = ids.slice(0, MAX_LISTED_IDS).map((id) => `\\`${clip(id)}\\``);\n return ids.length > MAX_LISTED_IDS ? `${listed.join(', ')}, ...` : listed.join(', ');\n}\n\nfunction clip(value: string): string {\n // Control characters and backticks would break the markdown list item and inline code the value is echoed in.\n const printable = value.replaceAll(/[\\p{Cc}]/gu, ' ').replaceAll('`', \"'\");\n if (printable.trim() === '') return '(空)';\n return printable.length > MAX_ECHOED_CELL_LENGTH ? `${printable.slice(0, MAX_ECHOED_CELL_LENGTH)}…` : printable;\n}\n\nfunction matchesExpectedResult(resolvedCwd: ResolvedCwd, result: Pick<TestCaseResult, 'decisionCode'>): boolean {\n return (result.decisionCode === DecisionCode.ACCEPTED) === (resolvedCwd.expectedResult === 'accepted');\n}\n\nfunction validateFiniteNumber(prediction: string): string | undefined {\n return Number.isFinite(Number(prediction)) && prediction.trim() !== '' ? undefined : '数値ではありません';\n}\n\nfunction mean(values: readonly number[]): number {\n return values.reduce((sum, value) => sum + value, 0) / values.length;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"evaluation.js","names":[],"sources":["../../src/presets/evaluation.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { CsvParseError, type CsvParseErrorReason, parseCsv, parseCsvRecords } from '../helpers/parseCsv.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { passesIsolationCheckInDebugMode } from '../helpers/runIsolationCheckInDebugMode.js';\nimport {\n printDebugCwdBanner,\n printDebugExpectationFailureBanner,\n resolveCwds,\n type ResolvedCwd,\n} from '../helpers/resolveCwds.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst DEFAULT_SUBMISSION_FILE_PATH = 'submission.csv';\nconst TEST_CASE_ID = 'evaluation';\nconst MAX_LISTED_IDS = 10;\n// Learner-controlled cells are echoed into the feedback, so clip them to keep the result line small.\nconst MAX_ECHOED_CELL_LENGTH = 50;\nconst CSV_PARSE_ERROR_MESSAGES: Record<CsvParseErrorReason, string> = {\n unterminated_quote: '引用符(`\"`)が閉じられていません',\n unexpected_quote: '引用符(`\"`)は値全体を囲む位置にしか書けません',\n text_after_quoted_field: '閉じ引用符の後に区切り文字以外の文字があります',\n};\n\nexport interface EvaluationMetric {\n /** Label shown with the score, e.g. `RMSLE`. */\n label: string;\n isHigherBetter: boolean;\n /**\n * Returns an error message when a predicted value cannot be scored (e.g. it is not a number).\n * The message is appended to the row's line number and the offending value, so phrase it as a\n * predicate such as `数値ではありません`. It also validates the ground truth when it is loaded.\n */\n validatePrediction?: (prediction: string) => string | undefined;\n /** Computes the score from predictions and answers aligned by id. Both are raw CSV cell strings. */\n compute: (predictions: readonly string[], answers: readonly string[]) => number;\n}\n\nexport interface EvaluationJudgePresetOptions {\n /** CSV file holding the hidden ground truth, relative to the problem directory. */\n answerFilePath: string;\n /** CSV file the submission must contain. Defaults to `submission.csv`. */\n submissionFilePath?: string;\n /** Column that identifies each row in both CSV files. */\n idColumn: string;\n /** Column holding the answer in the ground truth and the prediction in the submission. */\n targetColumn: string;\n metric: EvaluationMetric;\n /**\n * Score required for acceptance: the submission is accepted when its score is at least this value\n * for a higher-is-better metric and at most this value otherwise. Every valid submission is\n * accepted when omitted.\n */\n acceptableScore?: number;\n}\n\nexport const evaluationMetrics = {\n rmsle: {\n label: 'RMSLE',\n isHigherBetter: false,\n validatePrediction: (prediction: string) =>\n validateFiniteNumber(prediction) ?? (Number(prediction) < 0 ? '負の値です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(\n mean(\n predictions.map(\n (prediction, index) => (Math.log1p(Number(prediction)) - Math.log1p(Number(answers[index]))) ** 2\n )\n )\n ),\n },\n rmse: {\n label: 'RMSE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(mean(predictions.map((prediction, index) => (Number(prediction) - Number(answers[index])) ** 2))),\n },\n mae: {\n label: 'MAE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => Math.abs(Number(prediction) - Number(answers[index])))),\n },\n accuracy: {\n label: 'Accuracy',\n isHigherBetter: true,\n validatePrediction: (prediction: string) => (prediction.trim() === '' ? '空です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => (prediction.trim() === answers[index]?.trim() ? 1 : 0))),\n },\n} as const satisfies Record<string, EvaluationMetric>;\n\n/**\n * A preset for Kaggle-style model evaluation problems: the submission is a CSV of predictions for a\n * hidden test set, and the judge reports the metric value as `score` instead of comparing outputs.\n * The problem directory keeps the ground truth CSV, so it is never shipped to learners.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { evaluationJudgePreset, evaluationMetrics } from '@exercode/problem-utils/presets/evaluation';\n *\n * await evaluationJudgePreset(import.meta.dirname, {\n * answerFilePath: 'evaluation/answer.csv',\n * idColumn: 'Id',\n * targetColumn: 'TradePrice',\n * metric: evaluationMetrics.rmsle,\n * acceptableScore: 0.5,\n * });\n * ```\n *\n * Run `bun judge.ts model_answers/default` to judge one submission, or `bun judge.ts` to judge every\n * `<problemDir>/model_answers/*` and `<problemDir>/model_answers.fails/*` directory for debugging.\n */\nexport async function evaluationJudgePreset(problemDir: string, options: EvaluationJudgePresetOptions): Promise<void> {\n const args = parseArgs(process.argv);\n const { cwds, isDebugMode } = await resolveCwds(problemDir, args.cwd);\n\n if (isDebugMode && !(await passesIsolationCheckInDebugMode(problemDir, cwds, args.params))) {\n process.exitCode = 1;\n return;\n }\n\n const answer = await readAnswer(path.join(problemDir, options.answerFilePath), options);\n for (const resolvedCwd of cwds) {\n if (isDebugMode) printDebugCwdBanner(problemDir, resolvedCwd);\n const result = await judgeSubmission(problemDir, resolvedCwd.cwd, answer, options);\n printTestCaseResult({ testCaseId: TEST_CASE_ID, ...result });\n if (isDebugMode && !matchesExpectedResult(resolvedCwd, result)) {\n process.exitCode = 1;\n printDebugExpectationFailureBanner(problemDir, resolvedCwd);\n }\n }\n}\n\nasync function judgeSubmission(\n problemDir: string,\n cwd: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Promise<Omit<TestCaseResult, 'testCaseId'>> {\n const staticAnalysisResult = await judgeByStaticAnalysis(cwd, await readProblemMarkdownFrontMatter(problemDir));\n if (staticAnalysisResult) return staticAnalysisResult;\n\n const submissionFilePath = options.submissionFilePath ?? DEFAULT_SUBMISSION_FILE_PATH;\n let submissionText: string;\n try {\n const resolvedPath = path.join(cwd, submissionFilePath);\n submissionText = await fs.readFile(resolvedPath, 'utf8');\n } catch {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n feedbackMarkdown: `\\`${submissionFilePath}\\`が見つかりません。予測結果のCSVファイルを\\`${submissionFilePath}\\`という名前で提出してください。`,\n };\n }\n\n const predictions = readPredictions(submissionText, submissionFilePath, answer, options);\n if (typeof predictions === 'string') {\n return { decisionCode: DecisionCode.WRONG_ANSWER, feedbackMarkdown: predictions };\n }\n\n const answerValues = [...answer.values()];\n const score = options.metric.compute(\n [...answer.keys()].map((id) => predictions.get(id) as string),\n answerValues\n );\n if (!Number.isFinite(score)) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n feedbackMarkdown: `予測値が極端なため、${options.metric.label}を計算できませんでした。予測値を見直してください。`,\n };\n }\n const isAccepted =\n options.acceptableScore === undefined ||\n (options.metric.isHigherBetter ? score >= options.acceptableScore : score <= options.acceptableScore);\n return {\n decisionCode: isAccepted ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER,\n score,\n scoreLabel: options.metric.label,\n feedbackMarkdown: buildScoreMarkdown(score, answerValues.length, isAccepted, options),\n };\n}\n\nfunction buildScoreMarkdown(\n score: number,\n rowCount: number,\n isAccepted: boolean,\n options: EvaluationJudgePresetOptions\n): string {\n const { label, isHigherBetter } = options.metric;\n const precision = findDistinguishingPrecision(score, options.acceptableScore);\n const acceptanceLine =\n options.acceptableScore === undefined\n ? ''\n : `\\n合格基準: ${label} ${isHigherBetter ? '≧' : '≦'} ${formatScore(options.acceptableScore, precision)}(${isAccepted ? '達成' : '未達成'})`;\n return `| 指標 | スコア |\n| ---- | ------ |\n| ${label} | ${formatScore(score, precision)} |\n\n評価件数: ${rowCount.toLocaleString('en-US')}件${acceptanceLine}\n`;\n}\n\n/**\n * Finds the number of significant digits at which the score and the threshold render differently\n * (or stop changing), so the feedback never shows a pair that contradicts the decision.\n */\nfunction findDistinguishingPrecision(score: number, threshold: number | undefined): number {\n const minimumPrecision = 6;\n if (threshold === undefined || score === threshold) return minimumPrecision;\n for (let precision = minimumPrecision; precision < 17; precision++) {\n if (formatScore(score, precision) !== formatScore(threshold, precision)) return precision;\n }\n return 17;\n}\n\nfunction formatScore(score: number, precision: number): string {\n return Number(score.toPrecision(precision)).toString();\n}\n\nfunction readPredictions(\n submissionText: string,\n submissionFilePath: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Map<string, string> | string {\n let records: ReturnType<typeof parseCsvRecords>;\n try {\n records = parseCsvRecords(submissionText);\n } catch (error) {\n const detail =\n error instanceof CsvParseError ? `${error.lineNumber}行目付近: ${CSV_PARSE_ERROR_MESSAGES[error.reason]}` : '';\n return `\\`${submissionFilePath}\\`をCSVとして読み込めませんでした。${detail}`;\n }\n const columns = findColumns(records[0]?.cells ?? [], options);\n if (!columns) {\n return `\\`${submissionFilePath}\\`の1行目には\\`${options.idColumn}\\`列と\\`${options.targetColumn}\\`列のヘッダーが必要です。`;\n }\n\n const predictions = new Map<string, string>();\n const problems: string[] = [];\n let problemCount = 0;\n const reportProblem = (problem: string): void => {\n problemCount++;\n if (problems.length < MAX_LISTED_IDS) problems.push(problem);\n };\n for (const { cells: row, lineNumber } of records.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const prediction = row[columns.targetIndex] ?? '';\n if (!answer.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行は評価対象ではありません`);\n } else if (predictions.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行が重複しています`);\n } else {\n const validationError = options.metric.validatePrediction?.(prediction);\n if (validationError) {\n reportProblem(`${lineNumber}行目: \\`${options.targetColumn}\\`の値\\`${clip(prediction)}\\`は${validationError}`);\n }\n predictions.set(id, prediction);\n }\n }\n const missingIds = [...answer.keys()].filter((id) => !predictions.has(id));\n if (missingIds.length > 0) {\n // Missing rows are the most actionable problem, so they must survive the listing cap.\n problemCount++;\n problems.unshift(\n `\\`${options.idColumn}\\`が${listIds(missingIds)}の行がありません(不足 ${missingIds.length.toLocaleString('en-US')}件)`\n );\n }\n if (problemCount > 0) {\n return `\\`${submissionFilePath}\\`の内容に問題があります。\n\n${problems.map((problem) => `- ${problem}`).join('\\n')}${problemCount > problems.length ? `\\n- ほか${(problemCount - problems.length).toLocaleString('en-US')}件` : ''}\n`;\n }\n return predictions;\n}\n\nasync function readAnswer(answerFilePath: string, options: EvaluationJudgePresetOptions): Promise<Map<string, string>> {\n const rows = parseCsv(await fs.readFile(answerFilePath, 'utf8'));\n const columns = findColumns(rows[0] ?? [], options);\n if (!columns) {\n throw new Error(`answer file must have ${options.idColumn} and ${options.targetColumn} columns: ${answerFilePath}`);\n }\n const answer = new Map<string, string>();\n for (const row of rows.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const value = row[columns.targetIndex] ?? '';\n if (!id) throw new Error(`empty ${options.idColumn} in answer file: ${answerFilePath}`);\n if (answer.has(id)) throw new Error(`duplicate ${options.idColumn} in answer file: ${id}`);\n const validationError = options.metric.validatePrediction?.(value);\n if (validationError)\n throw new Error(`invalid ${options.targetColumn} in answer file for ${id}: ${validationError}`);\n answer.set(id, value);\n }\n if (answer.size === 0) throw new Error(`answer file has no rows: ${answerFilePath}`);\n return answer;\n}\n\nfunction findColumns(\n header: readonly string[],\n options: EvaluationJudgePresetOptions\n): { idIndex: number; targetIndex: number } | undefined {\n const names = header.map((name) => name.trim());\n const idIndex = names.indexOf(options.idColumn);\n const targetIndex = names.indexOf(options.targetColumn);\n return idIndex === -1 || targetIndex === -1 ? undefined : { idIndex, targetIndex };\n}\n\nfunction isBlankRow(row: readonly string[]): boolean {\n return row.every((cell) => cell.trim() === '');\n}\n\nfunction listIds(ids: readonly string[]): string {\n const listed = ids.slice(0, MAX_LISTED_IDS).map((id) => `\\`${clip(id)}\\``);\n return ids.length > MAX_LISTED_IDS ? `${listed.join(', ')}, ...` : listed.join(', ');\n}\n\nfunction clip(value: string): string {\n // Control characters and backticks would break the markdown list item and inline code the value is echoed in.\n const printable = value.replaceAll(/[\\p{Cc}]/gu, ' ').replaceAll('`', \"'\");\n if (printable.trim() === '') return '(空)';\n return printable.length > MAX_ECHOED_CELL_LENGTH ? `${printable.slice(0, MAX_ECHOED_CELL_LENGTH)}…` : printable;\n}\n\nfunction matchesExpectedResult(resolvedCwd: ResolvedCwd, result: Pick<TestCaseResult, 'decisionCode'>): boolean {\n return (result.decisionCode === DecisionCode.ACCEPTED) === (resolvedCwd.expectedResult === 'accepted');\n}\n\nfunction validateFiniteNumber(prediction: string): string | undefined {\n return Number.isFinite(Number(prediction)) && prediction.trim() !== '' ? undefined : '数値ではありません';\n}\n\nfunction mean(values: readonly number[]): number {\n return values.reduce((sum, value) => sum + value, 0) / values.length;\n}\n"],"mappings":"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"}
@@ -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/cleanWorkingDirectory.cjs"),l=require("../helpers/copyTestCaseFileInput.cjs"),u=require("../helpers/findEntryPointFile.cjs"),d=require("../helpers/judgeByStaticAnalysis.cjs"),f=require("../helpers/readOutputFiles.cjs"),p=require("../helpers/spawnWithTimeout.cjs");let m=require("node:path");m=e.__toESM(m,1);let h=require("zod"),g=require("node:fs/promises");g=e.__toESM(g,1);let _=require("node:os");_=e.__toESM(_,1);let v=require("node:child_process");v=e.__toESM(v,1);let y=require("node:timers/promises");const b=.3,x=[_.default.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`],S=h.z.object({language:h.z.union([h.z.string(),h.z.array(h.z.string())]).optional()});async function C(e,r){let i=n.parseArgs(process.argv);if(!i.cwd)throw Error(`cwd argument required`);let p=S.parse(i.params),m=i.cwd,h=await o.readProblemMarkdownFrontMatter(e),g=await(r.readTestCases??O)(e),_=g.length>0?g:[{id:`default`}],v=_[0]?.id??`prebuild`,y=await d.judgeByStaticAnalysis(i.cwd,h);if(y){t.printTestCaseResult({testCaseId:v,...y});return}let x=r.mainFilePath??await u.findEntryPointFile(i.cwd,p.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${p.language?`: language: ${p.language}`:``}`});return}let C=a.findLanguageDefinitionByPath(x);if(!C){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.WRONG_ANSWER,stderr:`unsupported language`});return}let A={...process.env,CI:``,FORCE_COLOR:`0`},j=await w({cwd:i.cwd,language:p.language,configuredMainFilePath:r.mainFilePath});if(C.prebuild)try{await C.prebuild(i.cwd);let e=await w({cwd:i.cwd,language:p.language??T(x),configuredMainFilePath:r.mainFilePath,allowConfiguredPathFallback:!0});e&&(j=e)}catch(e){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.BUILD_ERROR,stderr:R(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${p.language?`: language: ${p.language}`:``}`});return}let M;if(r.prepare)try{M=await r.prepare({cwd:m,env:A,mainFilePath:j,problemMarkdownFrontMatter:h})}catch(e){t.printTestCaseResult({testCaseId:v,decisionCode:s.DecisionCode.BUILD_ERROR,stderr:R(e)});return}let P=M??await E({cwd:i.cwd,env:A,mainFilePath:j,languageDefinition:C});if(P){t.printTestCaseResult({testCaseId:v,decisionCode:P.decisionCode??s.DecisionCode.BUILD_ERROR,feedbackMarkdown:P.feedbackMarkdown,stderr:P.stderr,stdout:P.stdout,outputFiles:P.outputFiles});return}let F=await c.snapshotWorkingDirectory(i.cwd),I,L=v,z;try{I=r.runCommand?void 0:await N();let e=g.shared?.fileInputPath;for(let n of _){L=n.id,e&&await l.copyTestCaseFileInput(e,i.cwd),n.fileInputPath&&await l.copyTestCaseFileInput(n.fileInputPath,i.cwd);let a=typeof h.timeLimitMs==`number`?h.timeLimitMs/1e3:r.runTimeoutSeconds??5,o=I?{...A,DISPLAY:I.display}:A,u=await r.resolveInput?.({testCase:n,cwd:i.cwd,env:o})??n.input??``;z=u;let d=await r.command?.({testCase:n,cwd:i.cwd,env:o,mainFilePath:j})??C.command(j),p;try{p=r.runCommand?await r.runCommand({testCase:n,command:d,stdin:u,cwd:m,env:o,timeLimitSeconds:a,screenshotWaitSeconds:r.screenshotWaitSeconds??b,stopDetectionThreshold:r.stopDetectionThreshold??5}):await k({command:d,stdin:u,cwd:i.cwd,env:o,timeLimitSeconds:a,screenshotWaitSeconds:r.screenshotWaitSeconds??b,stopDetectionThreshold:r.stopDetectionThreshold??5})}catch(e){t.printTestCaseResult({testCaseId:n.id,decisionCode:s.DecisionCode.RUNTIME_ERROR,stdin:u,stderr:R(e)}),await c.cleanWorkingDirectory(i.cwd,F);return}let g=await f.readOutputFiles(i.cwd,h.requiredOutputFilePaths??[]),_={timeLimitSeconds:a,problemMarkdownFrontMatter:{memoryLimitByte:h.memoryLimitByte,requiredOutputFilePaths:h.requiredOutputFilePaths}},v=D({runResult:p,outputFiles:g,context:_}),y=v;if(v.decisionCode===s.DecisionCode.ACCEPTED)try{let e=await r.test({testCase:n,runResult:p,outputFiles:g,cwd:i.cwd,context:_});y={decisionCode:e.decisionCode??v.decisionCode,feedbackMarkdown:e.feedbackMarkdown,stderr:e.stderr,stdout:e.stdout,outputFiles:e.outputFiles}}catch(e){y={decisionCode:s.DecisionCode.RUNTIME_ERROR,stderr:R(e)}}let x=y.decisionCode??s.DecisionCode.ACCEPTED,S=y.stdout??p.stdout,w=y.stderr??p.stderr;if(t.printTestCaseResult({testCaseId:n.id,decisionCode:x,exitStatus:p.status,stdin:p.stdin||void 0,stdout:S||void 0,stderr:w||void 0,timeSeconds:p.timeSeconds,memoryBytes:p.memoryBytes,feedbackMarkdown:y.feedbackMarkdown,outputFiles:y.outputFiles??(g.length>0?g:void 0)}),await c.cleanWorkingDirectory(i.cwd,F),x!==s.DecisionCode.ACCEPTED)break}}catch(e){t.printTestCaseResult({testCaseId:L,decisionCode:s.DecisionCode.RUNTIME_ERROR,stdin:z,stderr:R(e)}),await c.cleanWorkingDirectory(i.cwd,F)}finally{await I?.dispose()}}async function w(e){if(e.configuredMainFilePath){if(await L(m.default.join(e.cwd,e.configuredMainFilePath)))return e.configuredMainFilePath;if(!e.allowConfiguredPathFallback)return}return await u.findEntryPointFile(e.cwd,e.language)}function T(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 E(e){let t=e.languageDefinition.buildCommand?.(e.mainFilePath);if(!t)return;let n=await p.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 D(e){if(e.runResult.stopReason===`timeout`)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 O(e){return await r.readTestCases(m.default.join(e,`test_cases`))}async function k(e){let t=v.default.spawn(`timeout`,[e.timeLimitSeconds.toFixed(3),...x,...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=[],c=[],l=Date.now()/1e3,u=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)=>{if(e===124){o=`timeout`,i=0;return}if(t){i=1,r=r?`${r}\nprocess terminated by signal: ${t}`:`process terminated by signal: ${t}`;return}i=e??1});try{for(e.stdin&&t.stdin.write(e.stdin),t.stdin.end();i===void 0;){if(await(0,y.setTimeout)(e.screenshotWaitSeconds*1e3),u=Math.max(u,F(t.pid)),c=A(e.env.DISPLAY).toSorted((e,t)=>e.data.length-t.data.length),c.length>0){let t=c.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-l>e.timeLimitSeconds){o=`timeout`,i=0;break}}o!==`process_exit`&&t.exitCode===null&&(t.removeAllListeners(`close`),t.removeAllListeners(`error`))}finally{await P(t)}if(a)throw a;let{memoryBytes:d,stderr:f,timeSeconds:p}=M(r,l,u);return{stdin:e.stdin,stdout:n.trimEnd(),stderr:f,status:i,timeSeconds:p,memoryBytes:d,screenshots:c,stopReason:o}}function A(e){let t={...process.env,DISPLAY:e??process.env.DISPLAY},n=v.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 e of j(n.stdout)){let n=v.default.spawnSync(`maim`,[`-i`,e],{env:t});if(n.error)throw n.error;if(n.status!==0||n.stdout.length===0)continue;let i=v.default.spawnSync(`xdotool`,[`getwindowname`,e],{encoding:`utf8`,env:t});if(i.error)throw i.error;let a=i.stdout.trim().replaceAll(/[\s/]/g,`_`);r.push({path:`${a||`window`}_${e}.png`,data:n.stdout.toString(`base64`),encoding:`base64`})}return r}function j(e){let t=[],n=e.split(`
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(Number.parseInt(n[1],16).toString())}return t}function M(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 N(){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=v.default.spawn(`Xvfb`,[t,`-screen`,`0`,`1280x1024x24`,`-ac`],{stdio:`ignore`});if(r.on(`error`,e=>{n=e}),await(0,y.setTimeout)(300),n)throw n;if(r.exitCode===null)return{display:t,dispose:async()=>{r.killed||(r.kill(`SIGTERM`),await(0,y.setTimeout)(100),r.exitCode===null&&r.kill(`SIGKILL`))}}}throw Error(`Xvfb could not be started.`)}async function P(e){e.pid&&(I(e.pid,`SIGTERM`),await(0,y.setTimeout)(200),e.exitCode===null&&I(e.pid,`SIGKILL`))}function F(e){if(!e||process.platform!==`linux`)return 0;let t=v.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
- `).map(e=>Number(e.trim())).filter(e=>Number.isFinite(e)&&e>0).reduce((e,t)=>e+t*1024,0)}function I(e,t){try{if(process.platform!==`win32`){process.kill(-e,t);return}}catch{}try{process.kill(e,t)}catch{}}async function L(e){try{return await g.default.access(e),!0}catch{return!1}}function R(e){return e instanceof Error?e.message:String(e)}exports.guiCommandJudgePreset=C;
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)=>{if(e===124){o=`timeout`,i=0;return}if(t){i=1,r=r?`${r}\nprocess terminated by signal: ${t}`:`process terminated by signal: ${t}`;return}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
+ `);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
+ `).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","snapshotWorkingDirectory","copyTestCaseFileInput","cleanWorkingDirectory","readOutputFiles","path","languageIdToDefinition","spawnWithTimeout","readFileTestCases","childProcess","wait","fs"],"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';\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 { 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 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 stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';\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 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 }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;\n test: (context: {\n testCase: TTestCase;\n runResult: 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`.\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: errorToMessage(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: errorToMessage(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: GuiCommandRunResult;\n try {\n runResult = options.runCommand\n ? await options.runCommand({\n testCase,\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 })\n : await spawnGuiProgram({\n command,\n stdin,\n cwd: args.cwd,\n env: runEnv,\n timeLimitSeconds,\n screenshotWaitSeconds: options.screenshotWaitSeconds ?? SCREENSHOT_WAIT_SECONDS,\n stopDetectionThreshold: options.stopDetectionThreshold ?? STOP_DETECTION_THRESHOLD,\n });\n } catch (error) {\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin,\n stderr: errorToMessage(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({ runResult, outputFiles, context: judgeContext });\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: errorToMessage(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: errorToMessage(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}): Partial<GuiJudgeCaseResult> {\n if (context.runResult.stopReason === 'timeout') {\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}): 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 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 while (exitCode === undefined) {\n await wait(context.screenshotWaitSeconds * 1000);\n sampledMemoryBytes = Math.max(sampledMemoryBytes, readProcessGroupMemoryBytes(child.pid));\n const currentScreenshots = takeScreenshots(context.env.DISPLAY);\n screenshots = currentScreenshots.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 stopReason,\n };\n}\n\nfunction takeScreenshots(display: string | undefined): GuiScreenshotFile[] {\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 screenshots: GuiScreenshotFile[] = [];\n for (const windowId of extractTopLevelWindowIds(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 screenshots.push({\n path: `${windowName || 'window'}_${windowId}.png`,\n data: screenshot.stdout.toString('base64'),\n encoding: 'base64',\n });\n }\n\n return screenshots;\n}\n\nfunction extractTopLevelWindowIds(stdout: string): string[] {\n const windowIds: string[] = [];\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 windowIds.push(Number.parseInt(match[1], 16).toString());\n }\n return windowIds;\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\nfunction errorToMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\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"],"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,3 +1,4 @@
1
+ import type { GuiRecordingFile } from '../helpers/guiRecording.js';
1
2
  import type { ProblemMarkdownFrontMatter } from '../types/problem.js';
2
3
  import type { TestCaseResult } from '../types/testCaseResult.js';
3
4
  /** What every GUI test case needs; a custom `readTestCases` may add any fields of its own. */
@@ -20,6 +21,7 @@ export interface GuiScreenshotFile {
20
21
  data: string;
21
22
  encoding: 'base64';
22
23
  }
24
+ export type { GuiRecordingFile } from '../helpers/guiRecording.js';
23
25
  export interface GuiCommandRunResult {
24
26
  stdin: string;
25
27
  stdout: string;
@@ -28,6 +30,11 @@ export interface GuiCommandRunResult {
28
30
  timeSeconds: number;
29
31
  memoryBytes: number;
30
32
  screenshots: GuiScreenshotFile[];
33
+ /**
34
+ * With `recordsAnimation`, an animated PNG (`<window name>_<window id>_recording.png`) of each
35
+ * window that kept changing during the run; a window that only appeared and was painted has none.
36
+ */
37
+ recordings?: GuiRecordingFile[];
31
38
  stopReason: 'process_exit' | 'stable_screenshot' | 'timeout';
32
39
  }
33
40
  interface GuiJudgeContext {
@@ -40,6 +47,12 @@ export interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase
40
47
  runTimeoutSeconds?: number;
41
48
  screenshotWaitSeconds?: number;
42
49
  stopDetectionThreshold?: number;
50
+ /**
51
+ * Records the windows into `runResult.recordings` for a grader to watch. The time limit then ends
52
+ * the recording instead of failing the run: a run stopped by it reaches `test` with
53
+ * `stopReason: 'timeout'` rather than being reported as `TIME_LIMIT_EXCEEDED`.
54
+ */
55
+ recordsAnimation?: boolean;
43
56
  readTestCases?: (problemDir: string) => Promise<readonly TTestCase[]>;
44
57
  prepare?: (context: {
45
58
  cwd: string;
@@ -67,10 +80,11 @@ export interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase
67
80
  timeLimitSeconds: number;
68
81
  screenshotWaitSeconds: number;
69
82
  stopDetectionThreshold: number;
83
+ recordsAnimation: boolean;
70
84
  }) => Promise<GuiCommandRunResult> | GuiCommandRunResult;
71
85
  test: (context: {
72
86
  testCase: TTestCase;
73
- runResult: GuiCommandRunResult;
87
+ runResult: Required<GuiCommandRunResult>;
74
88
  outputFiles: NonNullable<TestCaseResult['outputFiles']>;
75
89
  /** The submission's working directory, e.g. for `compareExpectedOutputFiles(cwd, testCase.fileOutputPath)`. */
76
90
  cwd: string;
@@ -80,7 +94,8 @@ export interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase
80
94
  /**
81
95
  * A preset function for judging GUI programs by collecting screenshots while the program runs.
82
96
  *
83
- * Keep problem-specific logic in `prepare`, `command`, and `test`.
97
+ * Keep problem-specific logic in `prepare`, `command`, and `test`. Set `recordsAnimation` to also
98
+ * hand `test` an animated PNG of each window that kept changing, e.g. to attach to `outputFiles`.
84
99
  *
85
100
  * @example
86
101
  * Create `judge.ts`:
@@ -100,4 +115,3 @@ export interface GuiCommandJudgePresetOptions<TTestCase extends BaseGuiTestCase
100
115
  * ```
101
116
  */
102
117
  export declare function guiCommandJudgePreset<TTestCase extends BaseGuiTestCase = GuiTestCase>(problemDir: string, options: GuiCommandJudgePresetOptions<TTestCase>): Promise<void>;
103
- export {};
@@ -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{cleanWorkingDirectory as s,snapshotWorkingDirectory as c}from"../helpers/cleanWorkingDirectory.js";import{copyTestCaseFileInput as l}from"../helpers/copyTestCaseFileInput.js";import{findEntryPointFile as u}from"../helpers/findEntryPointFile.js";import{judgeByStaticAnalysis as d}from"../helpers/judgeByStaticAnalysis.js";import{readOutputFiles as f}from"../helpers/readOutputFiles.js";import{spawnWithTimeout as p}from"../helpers/spawnWithTimeout.js";import m from"node:path";import{z as h}from"zod";import g from"node:fs/promises";import _ from"node:os";import v from"node:child_process";import{setTimeout as y}from"node:timers/promises";const b=.3,x=[_.platform()===`darwin`?`gtime`:`/usr/bin/time`,`--format`,`%e %M`],S=h.object({language:h.union([h.string(),h.array(h.string())]).optional()});async function C(n,r){let p=t(process.argv);if(!p.cwd)throw Error(`cwd argument required`);let m=S.parse(p.params),h=p.cwd,g=await a(n),_=await(r.readTestCases??O)(n),v=_.length>0?_:[{id:`default`}],y=v[0]?.id??`prebuild`,x=await d(p.cwd,g);if(x){e({testCaseId:y,...x});return}let C=r.mainFilePath??await u(p.cwd,m.language);if(!C){e({testCaseId:y,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 A=i(C);if(!A){e({testCaseId:y,decisionCode:o.WRONG_ANSWER,stderr:`unsupported language`});return}let j={...process.env,CI:``,FORCE_COLOR:`0`},M=await w({cwd:p.cwd,language:m.language,configuredMainFilePath:r.mainFilePath});if(A.prebuild)try{await A.prebuild(p.cwd);let e=await w({cwd:p.cwd,language:m.language??T(C),configuredMainFilePath:r.mainFilePath,allowConfiguredPathFallback:!0});e&&(M=e)}catch(t){e({testCaseId:y,decisionCode:o.BUILD_ERROR,stderr:R(t)});return}if(!M){e({testCaseId:y,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 P;if(r.prepare)try{P=await r.prepare({cwd:h,env:j,mainFilePath:M,problemMarkdownFrontMatter:g})}catch(t){e({testCaseId:y,decisionCode:o.BUILD_ERROR,stderr:R(t)});return}let F=P??await E({cwd:p.cwd,env:j,mainFilePath:M,languageDefinition:A});if(F){e({testCaseId:y,decisionCode:F.decisionCode??o.BUILD_ERROR,feedbackMarkdown:F.feedbackMarkdown,stderr:F.stderr,stdout:F.stdout,outputFiles:F.outputFiles});return}let I=await c(p.cwd),L,z=y,B;try{L=r.runCommand?void 0:await N();let t=_.shared?.fileInputPath;for(let n of v){z=n.id,t&&await l(t,p.cwd),n.fileInputPath&&await l(n.fileInputPath,p.cwd);let i=typeof g.timeLimitMs==`number`?g.timeLimitMs/1e3:r.runTimeoutSeconds??5,a=L?{...j,DISPLAY:L.display}:j,c=await r.resolveInput?.({testCase:n,cwd:p.cwd,env:a})??n.input??``;B=c;let u=await r.command?.({testCase:n,cwd:p.cwd,env:a,mainFilePath:M})??A.command(M),d;try{d=r.runCommand?await r.runCommand({testCase:n,command:u,stdin:c,cwd:h,env:a,timeLimitSeconds:i,screenshotWaitSeconds:r.screenshotWaitSeconds??b,stopDetectionThreshold:r.stopDetectionThreshold??5}):await k({command:u,stdin:c,cwd:p.cwd,env:a,timeLimitSeconds:i,screenshotWaitSeconds:r.screenshotWaitSeconds??b,stopDetectionThreshold:r.stopDetectionThreshold??5})}catch(t){e({testCaseId:n.id,decisionCode:o.RUNTIME_ERROR,stdin:c,stderr:R(t)}),await s(p.cwd,I);return}let m=await f(p.cwd,g.requiredOutputFilePaths??[]),_={timeLimitSeconds:i,problemMarkdownFrontMatter:{memoryLimitByte:g.memoryLimitByte,requiredOutputFilePaths:g.requiredOutputFilePaths}},v=D({runResult:d,outputFiles:m,context:_}),y=v;if(v.decisionCode===o.ACCEPTED)try{let e=await r.test({testCase:n,runResult:d,outputFiles:m,cwd:p.cwd,context:_});y={decisionCode:e.decisionCode??v.decisionCode,feedbackMarkdown:e.feedbackMarkdown,stderr:e.stderr,stdout:e.stdout,outputFiles:e.outputFiles}}catch(e){y={decisionCode:o.RUNTIME_ERROR,stderr:R(e)}}let x=y.decisionCode??o.ACCEPTED,S=y.stdout??d.stdout,C=y.stderr??d.stderr;if(e({testCaseId:n.id,decisionCode:x,exitStatus:d.status,stdin:d.stdin||void 0,stdout:S||void 0,stderr:C||void 0,timeSeconds:d.timeSeconds,memoryBytes:d.memoryBytes,feedbackMarkdown:y.feedbackMarkdown,outputFiles:y.outputFiles??(m.length>0?m:void 0)}),await s(p.cwd,I),x!==o.ACCEPTED)break}}catch(t){e({testCaseId:z,decisionCode:o.RUNTIME_ERROR,stdin:B,stderr:R(t)}),await s(p.cwd,I)}finally{await L?.dispose()}}async function w(e){if(e.configuredMainFilePath){if(await L(m.join(e.cwd,e.configuredMainFilePath)))return e.configuredMainFilePath;if(!e.allowConfiguredPathFallback)return}return await u(e.cwd,e.language)}function T(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 E(e){let t=e.languageDefinition.buildCommand?.(e.mainFilePath);if(!t)return;let n=await p(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 D(e){if(e.runResult.stopReason===`timeout`)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 O(e){return await n(m.join(e,`test_cases`))}async function k(e){let t=v.spawn(`timeout`,[e.timeLimitSeconds.toFixed(3),...x,...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=[],c=[],l=Date.now()/1e3,u=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)=>{if(e===124){o=`timeout`,i=0;return}if(t){i=1,r=r?`${r}\nprocess terminated by signal: ${t}`:`process terminated by signal: ${t}`;return}i=e??1});try{for(e.stdin&&t.stdin.write(e.stdin),t.stdin.end();i===void 0;){if(await y(e.screenshotWaitSeconds*1e3),u=Math.max(u,F(t.pid)),c=A(e.env.DISPLAY).toSorted((e,t)=>e.data.length-t.data.length),c.length>0){let t=c.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-l>e.timeLimitSeconds){o=`timeout`,i=0;break}}o!==`process_exit`&&t.exitCode===null&&(t.removeAllListeners(`close`),t.removeAllListeners(`error`))}finally{await P(t)}if(a)throw a;let{memoryBytes:d,stderr:f,timeSeconds:p}=M(r,l,u);return{stdin:e.stdin,stdout:n.trimEnd(),stderr:f,status:i,timeSeconds:p,memoryBytes:d,screenshots:c,stopReason:o}}function A(e){let t={...process.env,DISPLAY:e??process.env.DISPLAY},n=v.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 e of j(n.stdout)){let n=v.spawnSync(`maim`,[`-i`,e],{env:t});if(n.error)throw n.error;if(n.status!==0||n.stdout.length===0)continue;let i=v.spawnSync(`xdotool`,[`getwindowname`,e],{encoding:`utf8`,env:t});if(i.error)throw i.error;let a=i.stdout.trim().replaceAll(/[\s/]/g,`_`);r.push({path:`${a||`window`}_${e}.png`,data:n.stdout.toString(`base64`),encoding:`base64`})}return r}function j(e){let t=[],n=e.split(`
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(Number.parseInt(n[1],16).toString())}return t}function M(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 N(){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=v.spawn(`Xvfb`,[t,`-screen`,`0`,`1280x1024x24`,`-ac`],{stdio:`ignore`});if(r.on(`error`,e=>{n=e}),await y(300),n)throw n;if(r.exitCode===null)return{display:t,dispose:async()=>{r.killed||(r.kill(`SIGTERM`),await y(100),r.exitCode===null&&r.kill(`SIGKILL`))}}}throw Error(`Xvfb could not be started.`)}async function P(e){e.pid&&(I(e.pid,`SIGTERM`),await y(200),e.exitCode===null&&I(e.pid,`SIGKILL`))}function F(e){if(!e||process.platform!==`linux`)return 0;let t=v.spawnSync(`ps`,[`-o`,`rss=`,`--no-headers`,`--pgroup`,String(e)],{encoding:`utf8`});return t.error||t.status!==0||!t.stdout?0:t.stdout.split(`
3
- `).map(e=>Number(e.trim())).filter(e=>Number.isFinite(e)&&e>0).reduce((e,t)=>e+t*1024,0)}function I(e,t){try{if(process.platform!==`win32`){process.kill(-e,t);return}}catch{}try{process.kill(e,t)}catch{}}async function L(e){try{return await g.access(e),!0}catch{return!1}}function R(e){return e instanceof Error?e.message:String(e)}export{C as guiCommandJudgePreset};
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)=>{if(e===124){o=`timeout`,i=0;return}if(t){i=1,r=r?`${r}\nprocess terminated by signal: ${t}`:`process terminated by signal: ${t}`;return}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
+ `);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
+ `).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