sanket3280/code-execution
0
1const axios = require('axios');2const { applyWrappers } = require('./execution');3const heClient = require('./execution/hackerEarthClient');4const { LanguageExecutorFactory } = require('./execution/languageExecutors');5const AnswerVerifier = require('../utils/answerVerifiers');6 7// NEW: Import modular test harness and validators8const { createTestHarness, supportsOptimizedExecution, parseOptimizedOutput } = require('./testHarness');9const { validateMultipleOutputs, compareOutputs } = require('./validators/outputValidator');10 11// DOCKER: Import Docker executor12const dockerExecutor = require('./execution/dockerExecutor');13const USE_DOCKER = process.env.USE_DOCKER_EXECUTOR === 'true';14 15// Debug log16console.log('\n' + '='.repeat(80));17console.log('๐ง CODE EVALUATION SERVICE INITIALIZED');18console.log('='.repeat(80));19console.log(`๐ณ Docker Executor: ${USE_DOCKER ? 'ENABLED โ
' : 'DISABLED โ'}`);20console.log(`๐ HackerEarth API: ${USE_DOCKER ? 'FALLBACK' : 'PRIMARY'}`);21console.log(`๐ Environment: ${process.env.NODE_ENV || 'development'}`);22console.log('='.repeat(80) + '\n');23 24// HackerEarth V4 API Configuration25const CODE_EVAL_URL = process.env.CODE_EVAL_URL || 'https://api.hackerearth.com/v4/partner/code-evaluation/submissions/';26const CLIENT_SECRET = process.env.CLIENT_SECRET;27 28// Status codes mapping29const STATUS_CODES = {30 'AC': 'Accepted',31 'TLE': 'Time Limit Exceeded',32 'MLE': 'Memory Limit Exceeded',33 'RE': 'Runtime Error',34 'CE': 'Compilation Error',35 'REQUEST_FAILED': 'Request Failed'36};37 38/**39 * Parse test input from various formats40 */41const parseTestInput = (raw) => {42 try {43 return JSON.parse(raw);44 } catch (e) {45 const result = {};46 const lines = String(raw || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean);47 for (const line of lines) {48 const m = line.match(/^(\w+)\s*=\s*(.+)$/);49 if (m) {50 const key = m[1];51 let val = m[2].trim();52 const normalized = val.replace(/'/g, '"');53 try {54 result[key] = JSON.parse(normalized);55 } catch (e) {56 const num = Number(val);57 if (!Number.isNaN(num)) {58 result[key] = num;59 } else {60 result[key] = val.replace(/^\"|\"$/g, '');61 }62 }63 } else if (!result.x && /^-?\d+(?:\.\d+)?$/.test(line)) {64 result.x = Number(line);65 }66 }67 return result;68 }69};70 71 72/**73 * Submit code - Docker with HackerEarth fallback74 */75const submitCode = async (sourceCode, language, testInput, memoryLimit = 262144, timeLimit = 5, options = {}) => {76 // Debug: Show executor status77 console.log('\n' + 'โ'.repeat(80));78 console.log('โก CODE EXECUTION REQUEST');79 console.log('โ'.repeat(80));80 console.log(`๐ง Docker Enabled: ${USE_DOCKER ? 'โ
YES' : 'โ NO'}`);81 console.log(`๐ฆ Language: ${language}`);82 console.log(`โฑ๏ธ Time Limit: ${timeLimit}s`);83 console.log('โ'.repeat(80));84 85 // Try Docker first if enabled86 if (USE_DOCKER) {87 try {88 console.log('\n' + '='.repeat(80));89 console.log('๐ณ ATTEMPTING DOCKER EXECUTION');90 console.log('='.repeat(80));91 const dockerResult = await submitCodeDocker(sourceCode, language, testInput, memoryLimit, timeLimit, options);92 console.log('\n' + '='.repeat(80));93 console.log('โ
DOCKER EXECUTION SUCCESSFUL');94 console.log('='.repeat(80) + '\n');95 return dockerResult;96 } catch (dockerError) {97 console.log('\n' + '='.repeat(80));98 console.log('โ ๏ธ DOCKER EXECUTION FAILED');99 console.log('๐ FALLING BACK TO HACKEREARTH API');100 console.log('='.repeat(80));101 console.log('โ Error:', dockerError.message);102 console.log('='.repeat(80) + '\n');103 // Fall through to HackerEarth104 }105 } else {106 console.log('\n' + '='.repeat(80));107 console.log('โน๏ธ DOCKER DISABLED - Using HackerEarth');108 console.log('='.repeat(80));109 }110 111 // Use HackerEarth (default or fallback)112 console.log('\n' + '='.repeat(80));113 console.log('๐ USING HACKEREARTH API');114 console.log('='.repeat(80) + '\n');115 return await submitCodeHackerEarth(sourceCode, language, testInput, memoryLimit, timeLimit, options);116};117 118/**119 * Docker-based execution120 */121const submitCodeDocker = async (sourceCode, language, testInput, memoryLimit, timeLimit, options) => {122 let processedSourceCode = sourceCode;123 124 // Apply wrappers125 try {126 const wrapped = applyWrappers({ sourceCode, language, topic: options.topic });127 if (wrapped && wrapped.sourceCode) {128 processedSourceCode = wrapped.sourceCode;129 }130 } catch (e) { /* noop */ }131 132 console.log(`๐ฆ Language: ${language}`);133 console.log(`โฑ๏ธ Time Limit: ${timeLimit}s`);134 console.log(`๐พ Memory Limit: ${Math.floor(memoryLimit / 1024)}MB`);135 136 // Execute in Docker137 const result = await dockerExecutor.executeCode({138 sourceCode: processedSourceCode,139 language,140 input: testInput,141 timeLimit,142 memoryLimit: Math.floor(memoryLimit / 1024)143 });144 145 console.log(`๐ Execution Time: ${result.executionTime}ms`);146 console.log(`โ
Status: ${result.status}`);147 148 // Convert to HackerEarth-compatible format149 return {150 he_id: `docker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,151 result: {152 run_status: {153 status: result.status,154 output: result.output,155 stderr: result.error,156 time_used: result.executionTime,157 memory_used: result.memoryUsed158 }159 },160 request_status: {161 code: result.success ? 'REQUEST_COMPLETED' : 'REQUEST_FAILED'162 }163 };164};165 166/**167 * HackerEarth execution (original implementation)168 */169const submitCodeHackerEarth = async (sourceCode, language, testInput, memoryLimit, timeLimit, options) => {170 try {171 let processedSourceCode = sourceCode;172 let usedRunner = false;173 174 // Apply topic-specific wrappers175 try {176 const wrapped = applyWrappers({ sourceCode, language, topic: options.topic });177 if (wrapped && wrapped.sourceCode) {178 processedSourceCode = wrapped.sourceCode;179 usedRunner = wrapped.usedRunner || usedRunner;180 }181 } catch (e) { /* noop */ }182 183 const inputObj = parseTestInput(testInput);184 185 // Process based on language using optimized executors186 if (testInput !== 'all_test_cases') {187 const result = LanguageExecutorFactory.processCode(processedSourceCode, language, inputObj);188 processedSourceCode = result.processedSourceCode;189 usedRunner = result.usedRunner || usedRunner;190 }191 192 const response = await heClient.submitToHackerEarth({193 source: processedSourceCode,194 language,195 memoryLimit,196 timeLimit,197 input: testInput,198 usedRunner199 });200 201 return response;202 } catch (error) {203 console.error('โ Error submitting code:', error.message);204 console.error('โ Error details:', error.response?.data);205 throw error;206 }207};208 209/**210 * Get status of code execution from HackerEarth API211 */212const getStatus = async (heId) => heClient.getStatus(heId);213 214/**215 * Poll for completion of code execution216 */217const pollForCompletion = async (heId, maxAttempts = 10, interval = 1000) => heClient.pollForCompletion(heId, maxAttempts, interval);218 219/**220 * Get output content from HackerEarth URL221 */222const getOutputContent = async (outputUrl) => heClient.extractOutput({ output: outputUrl });223 224/**225 * Normalize output for easier comparison and UI display226 */227const normalizeOutputForCompare = heClient.normalizeOutputForCompare;228 229/**230 * Extract output text from run_status regardless of field name231 */232const extractOutput = async (runStatus) => heClient.extractOutput(runStatus);233 234/**235 * Execute code against multiple test cases - OPTIMIZED VERSION236 */237const executeTestCases = async (sourceCode, language, testCases, options = {}) => {238 console.log(`๐ Running all test cases (${testCases.length} total) for submission`);239 240 // Auto-enable optimized batch execution for compiled languages241 // BUT respect explicit runAllTestCases: false to prevent infinite loops242 const shouldUseOptimized = options.runAllTestCases === true ||243 (options.runAllTestCases !== false && (244 language.startsWith('JAVA') ||245 language.startsWith('CPP') ||246 language === 'C' ||247 language.startsWith('PYTHON') ||248 language === 'JAVASCRIPT_NODE'249 ));250 251 if (shouldUseOptimized) {252 return await executeAllTestCasesOptimized(sourceCode, language, testCases, options);253 }254 255 const results = {256 summary: {257 total: testCases.length,258 passed: 0,259 failed: 0,260 errors: 0261 },262 cases: []263 };264 265 const concurrency = Math.min(options.concurrency || 5, 10);266 const queue = [];267 268 const pushTask = async (index) => {269 const testCase = testCases[index];270 const caseResult = {271 caseNumber: index + 1,272 input: testCase.input,273 expected: testCase.expected,274 expected_output: testCase.expected, // Add for consistency275 output: '',276 status: 'Error',277 error: null,278 executionTime: null,279 memoryUsed: null,280 isHidden: testCase.isHidden || false,281 pass: false // Add pass field282 };283 284 try {285 const submission = await submitCode(sourceCode, language, testCase.input, undefined, undefined, { topic: options.topic });286 const heId = submission.he_id;287 288 // Optimized polling - Java needs more attempts289 const isJava = language.startsWith('JAVA');290 const isCompiled = isJava || language.startsWith('CPP') || language.startsWith('C');291 const maxAttempts = language === 'JAVASCRIPT_NODE' ? 28 : isJava ? 35 : isCompiled ? 25 : 28;292 const interval = language === 'JAVASCRIPT_NODE' ? 250 : isCompiled ? 200 : 300;293 294 const finalResult = await pollForCompletion(heId, maxAttempts, interval);295 const runStatus = finalResult.result?.run_status;296 297 if (runStatus) {298 caseResult.executionTime = runStatus.time_used;299 caseResult.memoryUsed = runStatus.memory_used;300 301 if (runStatus.status === 'AC' || runStatus.status === 'CODE_COMPILED') {302 caseResult.output = await extractOutput(runStatus);303 if (typeof caseResult.output === 'string') {304 caseResult.output = caseResult.output.replace(/\[\s+/g, '[').replace(/\s+\]/g, ']').replace(/\s*,\s*/g, ',').trim();305 if (testCase.expected && testCase.expected.startsWith('"') && testCase.expected.endsWith('"')) {306 if (!caseResult.output.startsWith('"')) caseResult.output = `"${caseResult.output}"`;307 } else if (testCase.expected && !testCase.expected.startsWith('"') && caseResult.output.startsWith('"') && caseResult.output.endsWith('"')) {308 caseResult.output = caseResult.output.slice(1, -1);309 }310 }311 312 const normalizedOutput = normalizeOutputForCompare(caseResult.output);313 const normalizedExpected = normalizeOutputForCompare(testCase.expected);314 315 console.log(`๐ COMPARISON DEBUG - Case ${index + 1}:`);316 console.log(` Raw Output: "${caseResult.output}" (type: ${typeof caseResult.output})`);317 console.log(` Raw Expected: "${testCase.expected}" (type: ${typeof testCase.expected})`);318 console.log(` Normalized Output: "${normalizedOutput}"`);319 console.log(` Normalized Expected: "${normalizedExpected}"`);320 console.log(` Match: ${normalizedOutput === normalizedExpected}`);321 322 // Check if problem uses custom verifier323 let isValid = false;324 if (options.verifierType) {325 console.log(` ๐ง Using custom verifier: ${options.verifierType}`);326 const verifyResult = AnswerVerifier.verify(327 options.verifierType,328 testCase.input,329 caseResult.output,330 testCase.expected,331 options.verifierMetadata || {}332 );333 isValid = verifyResult.valid;334 console.log(` ๐ Verifier result: ${verifyResult.valid ? 'โ
VALID' : 'โ INVALID'} - ${verifyResult.reason}`);335 } else if (normalizedOutput === normalizedExpected) {336 isValid = true;337 } else {338 // Fallback comparisons - handle quotes and type mismatches339 const outputStr = String(caseResult.output || '');340 const expectedStr = String(testCase.expected || '');341 342 // Remove quotes for comparison343 const cleanOutput = outputStr.replace(/^["']|["']$/g, '');344 const cleanExpected = expectedStr.replace(/^["']|["']$/g, '');345 346 // Try multiple comparison strategies347 if (cleanOutput === cleanExpected) {348 isValid = true;349 console.log(` โ
Match after removing quotes`);350 } else if (outputStr === expectedStr) {351 isValid = true;352 console.log(` โ
Exact match`);353 } else {354 // Try numeric comparison355 const outputNum = Number(cleanOutput);356 const expectedNum = Number(cleanExpected);357 if (!isNaN(outputNum) && !isNaN(expectedNum) && outputNum === expectedNum) {358 isValid = true;359 console.log(` โ
Numeric match: ${outputNum} === ${expectedNum}`);360 } else {361 // Try array comparison362 const normalizeArrayStr = (str) => str.replace(/\s+/g, '').replace(/\[/g, '[').replace(/\]/g, ']');363 const normalizedArrayOutput = normalizeArrayStr(outputStr);364 const normalizedArrayExpected = normalizeArrayStr(expectedStr);365 if (normalizedArrayOutput === normalizedArrayExpected) {366 isValid = true;367 console.log(` โ
Array match`);368 }369 }370 }371 }372 373 if (isValid) {374 caseResult.status = 'Pass';375 caseResult.pass = true;376 console.log(` โ
PASSED!`);377 } else {378 caseResult.status = 'Fail';379 caseResult.pass = false;380 console.log(` โ FAILED!`);381 }382 } else {383 caseResult.status = STATUS_CODES[runStatus.status] || 'Error';384 caseResult.error = runStatus.stderr || runStatus.compile_status;385 caseResult.output = await extractOutput(runStatus);386 }387 } else {388 caseResult.status = 'Error';389 caseResult.error = 'No run status received';390 }391 } catch (error) {392 caseResult.status = 'Error';393 caseResult.error = error.message;394 }395 396 return caseResult;397 };398 399 const indices = Array.from({ length: testCases.length }, (_, i) => i);400 const active = new Set();401 const resultsArr = new Array(testCases.length);402 403 async function runNext() {404 if (indices.length === 0) return;405 const idx = indices.shift();406 const p = pushTask(idx).then((r) => { resultsArr[idx] = r; active.delete(p); });407 active.add(p);408 if (active.size >= concurrency) await Promise.race(Array.from(active));409 return runNext();410 }411 412 await runNext();413 await Promise.all(Array.from(active));414 415 for (const cr of resultsArr) {416 results.cases.push(cr);417 if (cr.status === 'Pass') results.summary.passed++;418 else if (cr.status === 'Fail') results.summary.failed++;419 else results.summary.errors++;420 }421 422 // โ DON'T filter hidden cases - we need them for submission results423 // results.cases = results.cases.filter(testCase => !testCase.isHidden);424 return results;425};426 427/**428 * OPTIMIZED: Execute all test cases in a single submission429 */430const executeAllTestCasesOptimized = async (sourceCode, language, testCases, options = {}, retryCount = 0) => {431 // Silent execution - logs only in routes432 433 const results = {434 summary: {435 total: testCases.length,436 passed: 0,437 failed: 0,438 errors: 0439 },440 cases: []441 };442 443 try {444 let testHarness = '';445 let testInput = '';446 447 // NEW: Use modular test harness factory448 if (supportsOptimizedExecution(language)) {449 testHarness = createTestHarness(sourceCode, language, testCases);450 testInput = 'all_test_cases';451 } else {452 console.log('โ ๏ธ Language not optimized, falling back to individual submissions');453 return await executeTestCases(sourceCode, language, testCases, { ...options, runAllTestCases: false });454 }455 456 // Test harness created silently457 458 // Smart batching: Check if test harness is too large459 const MAX_HARNESS_SIZE = 950 * 1024; // 950KB limit460 461 if (testHarness.length > MAX_HARNESS_SIZE) {462 console.log(`โ ๏ธ Large harness (${(testHarness.length / 1024).toFixed(2)}KB) - using smart batching...`);463 464 // Split into batches465 const batches = [];466 let currentBatch = [];467 468 for (const tc of testCases) {469 currentBatch.push(tc);470 // NEW: Use modular harness471 const batchHarness = createTestHarness(sourceCode, language, currentBatch);472 473 if (batchHarness.length > MAX_HARNESS_SIZE) {474 currentBatch.pop();475 if (currentBatch.length > 0) {476 batches.push([...currentBatch]);477 }478 currentBatch = [tc];479 }480 }481 482 if (currentBatch.length > 0) {483 batches.push(currentBatch);484 }485 486 console.log(`๐ฆ Split into ${batches.length} batches`);487 488 // Execute each batch489 const allResults = {490 summary: { total: testCases.length, passed: 0, failed: 0, errors: 0 },491 cases: []492 };493 494 for (let i = 0; i < batches.length; i++) {495 console.log(`๐ Batch ${i + 1}/${batches.length}: ${batches[i].length} cases`);496 const batchResults = await executeAllTestCasesOptimized(sourceCode, language, batches[i], options, 0);497 498 allResults.cases.push(...batchResults.cases);499 allResults.summary.passed += batchResults.summary.passed;500 allResults.summary.failed += batchResults.summary.failed;501 allResults.summary.errors += batchResults.summary.errors;502 }503 504 console.log(`โ
All batches complete: ${allResults.summary.passed}/${allResults.summary.total} passed`);505 return allResults;506 }507 508 const submission = await submitCode(testHarness, language, testInput, undefined, undefined, { topic: options.topic });509 const heId = submission.he_id;510 511 // Optimized polling - Java needs more attempts due to compilation512 const isJava = language.startsWith('JAVA');513 const isCompiledLanguage = isJava || language.startsWith('CPP') || language.startsWith('C') || language.startsWith('PYTHON');514 const isJavaScript = language === 'JAVASCRIPT_NODE';515 516 // Java gets more attempts, others optimized517 const baseAttempts = isJavaScript ? 35 : isJava ? 40 : isCompiledLanguage ? 30 : 25;518 const scaledAttempts = Math.min(baseAttempts + Math.ceil(testCases.length / 4), 50);519 const maxAttempts = retryCount > 0 ? Math.ceil(scaledAttempts * 1.2) : scaledAttempts;520 521 // Faster intervals - 200-300ms range522 const interval = isJavaScript ? 250 : isCompiledLanguage ? 200 : 300;523 524 const finalResult = await pollForCompletion(heId, maxAttempts, interval);525 526 const runStatus = finalResult.result?.run_status;527 if (runStatus && (runStatus.status === 'AC' || runStatus.status === 'CODE_COMPILED')) {528 const output = await extractOutput(runStatus);529 const parsedResults = parseOptimizedOutput(output, testCases, language, options);530 // โ DON'T filter hidden cases - we need them for submission results531 // results.cases = parsedResults.cases.filter(testCase => !testCase.isHidden);532 results.cases = parsedResults.cases;533 results.summary = parsedResults.summary;534 535 } else {536 console.log('โ ๏ธ Optimized execution status:', runStatus?.status);537 console.log('โ ๏ธ Attempting to parse any available output...');538 539 try {540 const output = await extractOutput(runStatus);541 if (output && output.trim()) {542 console.log('๐ PARTIAL OUTPUT:', output);543 const parsedResults = parseOptimizedOutput(output, testCases, language, options);544 console.log(`โ
PARTIAL: ${parsedResults.summary.passed}/${parsedResults.summary.total} test cases passed`);545 return parsedResults;546 }547 } catch (error) {548 console.log('โ Failed to parse partial output:', error.message);549 }550 551 if (retryCount < 1) {552 console.log(`๐ Retry ${retryCount + 1}/1: Optimized execution failed, retrying...`);553 return await executeAllTestCasesOptimized(sourceCode, language, testCases, options, retryCount + 1);554 } else {555 console.log('โ Optimized execution failed after 1 retry');556 console.log('โ ๏ธ Falling back to individual submissions...');557 return await executeTestCases(sourceCode, language, testCases, { ...options, runAllTestCases: false });558 }559 }560 561 } catch (error) {562 console.error('โ Optimized execution error:', error.message);563 564 // Don't retry if HackerEarth is stuck - waste of time565 if (error.message.includes('timed out') || error.message.includes('stuck')) {566 console.log('โ ๏ธ HackerEarth API is overloaded/stuck');567 console.log('โ ๏ธ Skipping retry, falling back immediately...');568 return await executeTestCases(sourceCode, language, testCases, { ...options, runAllTestCases: false });569 }570 571 console.log('โ Optimized execution failed completely');572 console.log('โ ๏ธ Falling back to individual test case execution...');573 return await executeTestCases(sourceCode, language, testCases, { ...options, runAllTestCases: false });574 }575 576 return results;577};578 579/**580 * Create JavaScript test harness for optimized execution581 */582const createJavaScriptTestHarness = (sourceCode, testCases) => {583 // Add helper function if not present and code uses it584 let harness = '';585 if (sourceCode.includes('isAlphaNumeric') && !sourceCode.includes('function isAlphaNumeric')) {586 harness += '// Helper function for alphanumeric check\n';587 harness += 'function isAlphaNumeric(char) { return /[a-z0-9]/i.test(char); }\n\n';588 }589 590 // Declare common global variables used in memoization591 harness += '// Global variables for memoization (reset between test cases)\n';592 harness += 'let memo, cache, dp;\n\n';593 594 harness += sourceCode + '\n\n';595 harness += '// Test harness for all test cases\n';596 harness += 'const testCases = [\n';597 598 testCases.forEach((testCase, index) => {599 const input = JSON.parse(testCase.input);600 const expected = testCase.expected;601 harness += ` { input: ${JSON.stringify(input)}, expected: ${JSON.stringify(expected)}, index: ${index} },\n`;602 });603 604 harness += `];605 606// Helper to check if string is palindrome607function isPalindromeStr(s) {608 return s === s.split('').reverse().join('');609}610 611// TreeNode helper for binary tree problems612function TreeNode(val, left, right) {613 this.val = (val===undefined ? 0 : val);614 this.left = (left===undefined ? null : left);615 this.right = (right===undefined ? null : right);616}617 618function buildBinaryTree(arr) {619 if (!arr || arr.length === 0) return null;620 const root = new TreeNode(arr[0]);621 const queue = [root];622 let i = 1;623 while (queue.length > 0 && i < arr.length) {624 const node = queue.shift();625 if (i < arr.length && arr[i] !== null) {626 node.left = new TreeNode(arr[i]);627 queue.push(node.left);628 }629 i++;630 if (i < arr.length && arr[i] !== null) {631 node.right = new TreeNode(arr[i]);632 queue.push(node.right);633 }634 i++;635 }636 return root;637}638 639console.log('TEST_RESULTS_START');640for (let i = 0; i < testCases.length; i++) {641 const testCase = testCases[i];642 try {643 // Reset global memoization variables between test cases644 // Initialize as objects/arrays to support different memoization patterns645 memo = {};646 cache = {};647 dp = [];648 649 // Convert tree inputs if needed650 const processedInput = {};651 for (const key in testCase.input) {652 if (key === 'root' && Array.isArray(testCase.input[key])) {653 processedInput[key] = buildBinaryTree(testCase.input[key]);654 } else {655 processedInput[key] = testCase.input[key];656 }657 }658 const result = ${extractFunctionName(sourceCode)}(...Object.values(processedInput));659 let output;660 let expected;661 662 // Format output based on type663 if (Array.isArray(result)) {664 output = '[' + result.join(',') + ']';665 } else if (typeof result === 'string') {666 output = JSON.stringify(result);667 } else if (typeof result === 'boolean') {668 output = String(result);669 } else {670 output = String(result);671 }672 673 // Format expected based on type - ensure it matches output format674 if (typeof testCase.expected === 'boolean') {675 expected = String(testCase.expected);676 } else if (typeof testCase.expected === 'number') {677 expected = String(testCase.expected);678 } else if (typeof testCase.expected === 'string') {679 // Check if it's already a quoted string or a plain string680 if (testCase.expected.startsWith('"') && testCase.expected.endsWith('"')) {681 expected = testCase.expected;682 } else {683 // It's a plain string value like "3" - keep it as is for comparison684 expected = testCase.expected;685 }686 } else {687 expected = String(testCase.expected);688 }689 690 // Special validation for palindrome problems - accept any valid palindrome of same length691 let passed = output === expected;692 if (!passed && typeof result === 'string' && typeof testCase.expected === 'string') {693 const expectedStr = testCase.expected.replace(/^"|"$/g, '');694 if (result.length === expectedStr.length && isPalindromeStr(result) && isPalindromeStr(expectedStr)) {695 passed = true;696 }697 }698 699 console.log(\`TEST_\${i+1}:\${passed ? 'PASS' : 'FAIL'}:\${output}:\${expected}\`);700 } catch (error) {701 console.log(\`TEST_\${i+1}:ERROR:\${error.message}\`);702 }703}704console.log('TEST_RESULTS_END');`;705 706 return harness;707};708 709/**710 * Create Python test harness for optimized execution711 */712const createPythonTestHarness = (sourceCode, testCases) => {713 // Add helper function if not present and code uses it714 let harness = '';715 if ((sourceCode.includes('is_alpha_numeric') || sourceCode.includes('isAlphaNumeric')) &&716 !sourceCode.includes('def is_alpha_numeric')) {717 harness += '# Helper function for alphanumeric check\n';718 harness += 'def is_alpha_numeric(char):\n';719 harness += ' return char.isalnum()\n\n';720 }721 722 harness += sourceCode + '\n\n';723 harness += '# Test harness for all test cases\n';724 harness += 'test_cases = [\n';725 726 testCases.forEach((testCase, index) => {727 const input = JSON.parse(testCase.input);728 const expected = testCase.expected;729 harness += ` {"input": ${JSON.stringify(input)}, "expected": ${JSON.stringify(expected)}, "index": ${index}},\n`;730 });731 732 harness += `]733 734import json735 736def is_palindrome_str(s):737 return s == s[::-1]738 739class TreeNode:740 def __init__(self, val=0, left=None, right=None):741 self.val = val742 self.left = left743 self.right = right744 745def build_binary_tree(arr):746 if not arr or len(arr) == 0:747 return None748 root = TreeNode(arr[0])749 queue = [root]750 i = 1751 while queue and i < len(arr):752 node = queue.pop(0)753 if i < len(arr) and arr[i] is not None:754 node.left = TreeNode(arr[i])755 queue.append(node.left)756 i += 1757 if i < len(arr) and arr[i] is not None:758 node.right = TreeNode(arr[i])759 queue.append(node.right)760 i += 1761 return root762 763print('TEST_RESULTS_START')764for i, test_case in enumerate(test_cases):765 try:766 # Convert tree inputs if needed767 processed_input = {}768 for key, value in test_case['input'].items():769 if key == 'root' and isinstance(value, list):770 processed_input[key] = build_binary_tree(value)771 else:772 processed_input[key] = value773 result = ${extractPythonFunctionName(sourceCode)}(**processed_input)774 if isinstance(result, list):775 output = '[' + ','.join(map(str, result)) + ']'776 elif isinstance(result, str):777 output = json.dumps(result)778 else:779 output = str(result)780 expected = test_case['expected'] if isinstance(test_case['expected'], str) and test_case['expected'].startswith('"') else json.dumps(test_case['expected'])781 782 # Special validation for palindrome problems783 passed = output == expected784 if not passed and isinstance(result, str) and isinstance(test_case['expected'], str):785 expected_str = test_case['expected'].strip('"')786 if len(result) == len(expected_str) and is_palindrome_str(result) and is_palindrome_str(expected_str):787 passed = True788 789 print(f'TEST_{i+1}:{"PASS" if passed else "FAIL"}:{output}:{expected}')790 except Exception as e:791 print(f'TEST_{i+1}:ERROR:{str(e)}')792print('TEST_RESULTS_END')`;793 794 return harness;795};796 797/**798 * Create Java test harness for optimized execution799 */800const createJavaTestHarness = (sourceCode, testCases) => {801 const functionName = extractJavaFunctionName(sourceCode);802 803 let cleanSourceCode = sourceCode804 .replace(/\bpublic\s+class\s+Main\s*\{[\s\S]*?\}/g, '')805 .replace(/\bpublic\s+static\s+void\s+main\s*\([^)]*\)\s*\{[\s\S]*?\}/g, '')806 .replace(/\bclass\s+Main\s*\{[\s\S]*?\}/g, '')807 .replace(/\bpublic\s+class\s+TestHarness\s*\{[\s\S]*?\}/g, '')808 .replace(/\bclass\s+TestHarness\s*\{[\s\S]*?\}/g, '');809 810 // Add helper method if needed811 if (cleanSourceCode.includes('isAlphaNumeric') && !cleanSourceCode.includes('isAlphaNumeric(')) {812 // Helper will be added inside Solution class813 }814 815 let harness = cleanSourceCode + '\n\n';816 harness += 'public class TestHarness {\n';817 harness += ' public static void main(String[] args) {\n';818 harness += ' Solution solution = new Solution();\n';819 harness += ' System.out.println("TEST_RESULTS_START");\n';820 821 testCases.forEach((testCase, index) => {822 const input = JSON.parse(testCase.input);823 const expected = testCase.expected;824 harness += ` // Test case ${index + 1}\n`;825 harness += ` try {\n`;826 827 let methodCall = `solution.${functionName}(`;828 const inputKeys = Object.keys(input);829 830 // Helper to safely convert value to Java literal831 const toJavaValue = (value) => {832 if (Array.isArray(value)) {833 return `new int[]{${value.join(',')}}`;834 } else if (typeof value === 'string') {835 return `"${value}"`;836 } else if (typeof value === 'number') {837 // Check if number is within int range838 if (value > 2147483647 || value < -2147483648) {839 return `(int)${value}L`; // Cast from long to int (will overflow as expected)840 }841 return value;842 } else {843 return value;844 }845 };846 847 if (inputKeys.length === 1) {848 const value = input[inputKeys[0]];849 methodCall += toJavaValue(value);850 } else {851 const args = inputKeys.map(key => toJavaValue(input[key])).join(', ');852 methodCall += args;853 }854 methodCall += ')';855 856 harness += ` Object result = ${methodCall};\n`;857 harness += ` String output;\n`;858 harness += ` if (result instanceof int[]) {\n`;859 harness += ` output = java.util.Arrays.toString((int[])result);\n`;860 harness += ` } else if (result instanceof String) {\n`;861 harness += ` output = "\\"" + result + "\\"";\n`;862 harness += ` } else {\n`;863 harness += ` output = String.valueOf(result);\n`;864 harness += ` }\n`;865 const expectedIsString = typeof expected === 'string' && expected.startsWith('"');866 const expectedStr = expectedIsString ? expected : `"${expected}"`;867 harness += ` String expectedStr = ${expectedStr};\n`;868 harness += ` boolean passed = output.equals(expectedStr);\n`;869 harness += ` // Special validation for palindrome problems\n`;870 harness += ` if (!passed && result instanceof String) {\n`;871 harness += ` String resultStr = (String)result;\n`;872 harness += ` String expStr = expectedStr.replace("\\"", "");\n`;873 harness += ` if (resultStr.length() == expStr.length()) {\n`;874 harness += ` String revResult = new StringBuilder(resultStr).reverse().toString();\n`;875 harness += ` String revExp = new StringBuilder(expStr).reverse().toString();\n`;876 harness += ` if (resultStr.equals(revResult) && expStr.equals(revExp)) {\n`;877 harness += ` passed = true;\n`;878 harness += ` }\n`;879 harness += ` }\n`;880 harness += ` }\n`;881 harness += ` System.out.println("TEST_${index + 1}:" + (passed ? "PASS" : "FAIL") + ":" + output + ":${expected}");\n`;882 harness += ` } catch (Exception e) {\n`;883 harness += ` System.out.println("TEST_${index + 1}:ERROR:" + e.getMessage());\n`;884 harness += ` }\n`;885 });886 887 harness += ' System.out.println("TEST_RESULTS_END");\n';888 harness += ' }\n';889 harness += '}';890 891 return harness;892};893 894/**895 * Create C++ test harness for optimized execution896 */897const createCppTestHarness = (sourceCode, testCases) => {898 const functionName = extractCppFunctionName(sourceCode);899 900 // Simple approach: Split by "int main" and take only the first part901 let cleanSourceCode = sourceCode;902 const mainIndex = cleanSourceCode.indexOf('int main');903 if (mainIndex !== -1) {904 cleanSourceCode = cleanSourceCode.substring(0, mainIndex).trim();905 }906 907 // Ensure all required includes are present908 let includes = '';909 if (!cleanSourceCode.includes('#include <climits>')) includes += '#include <climits>\n';910 if (!cleanSourceCode.includes('#include <cctype>')) includes += '#include <cctype>\n';911 if (!cleanSourceCode.includes('#include <string>')) includes += '#include <string>\n';912 if (!cleanSourceCode.includes('#include <iostream>')) includes += '#include <iostream>\n';913 if (!cleanSourceCode.includes('#include <vector>')) includes += '#include <vector>\n';914 if (!cleanSourceCode.includes('#include <algorithm>')) includes += '#include <algorithm>\n';915 if (!cleanSourceCode.includes('using namespace std')) includes += 'using namespace std;\n';916 917 // Add helper function if needed918 let helpers = '';919 if (cleanSourceCode.includes('isAlphaNumeric') && !cleanSourceCode.includes('bool isAlphaNumeric')) {920 helpers += '\n// Helper function for alphanumeric check\n';921 helpers += 'bool isAlphaNumeric(char c) { return isalnum(c); }\n\n';922 }923 924 let harness = includes + helpers + cleanSourceCode + '\n\n';925 harness += 'int main() {\n';926 harness += ' std::cout << "TEST_RESULTS_START" << std::endl;\n';927 928 testCases.forEach((testCase, index) => {929 const input = JSON.parse(testCase.input);930 const expected = testCase.expected;931 harness += ` // Test case ${index + 1}\n`;932 harness += ` try {\n`;933 934 // Helper to safely convert value to C++ literal935 const toCppValue = (value) => {936 if (Array.isArray(value)) {937 return `{${value.join(',')}}`;938 } else if (typeof value === 'string') {939 return `"${value}"`;940 } else if (typeof value === 'number') {941 // Check if number is within int range942 if (value > 2147483647 || value < -2147483648) {943 return `static_cast<int>(${value}LL)`; // Cast from long long to int944 }945 return value;946 } else {947 return value;948 }949 };950 951 let functionCall = `${functionName}(`;952 const inputKeys = Object.keys(input);953 if (inputKeys.length === 1) {954 const value = input[inputKeys[0]];955 functionCall += toCppValue(value);956 } else {957 const args = inputKeys.map(key => toCppValue(input[key])).join(', ');958 functionCall += args;959 }960 functionCall += ')';961 962 harness += ` auto result = ${functionCall};\n`;963 harness += ` std::string resultStr;\n`;964 harness += ` if constexpr (std::is_same_v<decltype(result), std::string>) {\n`;965 harness += ` resultStr = "\\"" + result + "\\"";\n`;966 harness += ` } else {\n`;967 harness += ` resultStr = std::to_string(result);\n`;968 harness += ` }\n`;969 const expectedIsString = typeof expected === 'string' && expected.startsWith('"');970 const expectedStr = expectedIsString ? expected : `"${expected}"`;971 harness += ` std::string expectedStr = ${expectedStr};\n`;972 harness += ` bool passed = resultStr == expectedStr;\n`;973 harness += ` // Special validation for palindrome problems\n`;974 harness += ` if (!passed && std::is_same_v<decltype(result), std::string>) {\n`;975 harness += ` std::string expStr = expectedStr;\n`;976 harness += ` expStr.erase(std::remove(expStr.begin(), expStr.end(), '"'), expStr.end());\n`;977 harness += ` if (result.length() == expStr.length()) {\n`;978 harness += ` std::string revResult = result;\n`;979 harness += ` std::reverse(revResult.begin(), revResult.end());\n`;980 harness += ` std::string revExp = expStr;\n`;981 harness += ` std::reverse(revExp.begin(), revExp.end());\n`;982 harness += ` if (result == revResult && expStr == revExp) {\n`;983 harness += ` passed = true;\n`;984 harness += ` }\n`;985 harness += ` }\n`;986 harness += ` }\n`;987 harness += ` std::cout << "TEST_${index + 1}:" << (passed ? "PASS" : "FAIL") << ":" << resultStr << ":" << expectedStr << std::endl;\n`;988 harness += ` } catch (...) {\n`;989 harness += ` std::cout << "TEST_${index + 1}:ERROR:Exception" << std::endl;\n`;990 harness += ` }\n`;991 });992 993 harness += ' std::cout << "TEST_RESULTS_END" << std::endl;\n';994 harness += ' return 0;\n';995 harness += '}';996 997 return harness;998};999 1000/**1001 * NOTE: validateMultipleOutputs is now imported from validators/outputValidator.js1002 * The old implementation has been removed to avoid duplication1003 */1004 1005/**1006 1007/**1008 * Parse optimized output to extract individual test results1009 * Extracts results between TEST_RESULTS_START and TEST_RESULTS_END1010 */1011const parseOptimizedOutput = (output, testCases, language, options = {}) => {1012 const results = {1013 summary: {1014 total: testCases.length,1015 passed: 0,1016 failed: 0,1017 errors: 01018 },1019 cases: []1020 };1021 1022 console.log(`๐ Parsing test results from output (${output.length} chars)`);1023 console.log(`๐ Raw output:\n${output}`);1024 1025 // Extract content between TEST_RESULTS_START and TEST_RESULTS_END1026 const startMarker = 'TEST_RESULTS_START';1027 const endMarker = 'TEST_RESULTS_END';1028 const startIdx = output.indexOf(startMarker);1029 const endIdx = output.indexOf(endMarker);1030 1031 if (startIdx === -1 || endIdx === -1) {1032 console.log('โ ๏ธ Could not find TEST_RESULTS markers in output');1033 // Fallback: try to parse all lines1034 const lines = output.split('\n');1035 const testResults = lines.filter(line => line.trim().startsWith('TEST_') && line.includes(':'));1036 console.log(` Found ${testResults.length} test result lines without markers`);1037 return parseTestResultLines(testResults, testCases, options);1038 }1039 1040 const testResultsSection = output.substring(startIdx + startMarker.length, endIdx).trim();1041 const testResultLines = testResultsSection.split('\n').filter(line => line.trim().startsWith('TEST_'));1042 1043 console.log(`๐ Found ${testResultLines.length} test results between markers`);1044 1045 return parseTestResultLines(testResultLines, testCases, options);1046};1047 1048/**1049 * Parse individual test result lines1050 * Format: TEST_1:PASS:321:321 or TEST_1:FAIL:123:321 or TEST_1:ERROR:message1051 */1052const parseTestResultLines = (testResultLines, testCases, options = {}) => {1053 const results = {1054 summary: {1055 total: testCases.length,1056 passed: 0,1057 failed: 0,1058 errors: 01059 },1060 cases: []1061 };1062 1063 testResultLines.forEach((resultLine, index) => {1064 const trimmedLine = resultLine.trim();1065 console.log(` Line ${index + 1}: "${trimmedLine}"`);1066 1067 // Parse format: TEST_N:STATUS:OUTPUT:EXPECTED1068 const match = trimmedLine.match(/^TEST_(\d+):([^:]+):(.*)$/);1069 1070 if (!match) {1071 console.log(` โ ๏ธ Could not parse line, skipping`);1072 return;1073 }1074 1075 const testNumber = parseInt(match[1], 10);1076 const status = match[2];1077 const remainder = match[3];1078 1079 // Split remainder by last colon to separate output and expected1080 const lastColonIdx = remainder.lastIndexOf(':');1081 let output = remainder;1082 let expected = '';1083 1084 if (lastColonIdx !== -1 && status !== 'ERROR') {1085 output = remainder.substring(0, lastColonIdx);1086 expected = remainder.substring(lastColonIdx + 1);1087 }1088 1089 console.log(` Parsed: testNumber=${testNumber}, status="${status}", output="${output}", expected="${expected}"`);1090 1091 const testCaseIndex = testNumber - 1;1092 if (testCaseIndex < 0 || testCaseIndex >= testCases.length) {1093 console.log(` โ ๏ธ Test number ${testNumber} out of range, skipping`);1094 return;1095 }1096 1097 const testCase = testCases[testCaseIndex];1098 let isPassed = status === 'PASS';1099 1100 // Re-validate if status is FAIL but output matches expected1101 if (!isPassed && status === 'FAIL' && output && expected) {1102 // Normalize both for comparison - remove quotes and compare values1103 const normalizedOutput = output.trim().replace(/^["']|["']$/g, '');1104 const normalizedExpected = expected.trim().replace(/^["']|["']$/g, '');1105 1106 // Try direct string comparison first1107 if (normalizedOutput === normalizedExpected) {1108 isPassed = true;1109 console.log(` ๐ Re-validated: Output matches expected, marking as PASS`);1110 }1111 // Try with original values (handles quote differences)1112 else if (output.trim() === expected.trim()) {1113 isPassed = true;1114 console.log(` ๐ Re-validated: Exact match with quotes, marking as PASS`);1115 }1116 // Try numeric comparison if both can be parsed as numbers1117 else if (!isNaN(normalizedOutput) && !isNaN(normalizedExpected)) {1118 if (Number(normalizedOutput) === Number(normalizedExpected)) {1119 isPassed = true;1120 console.log(` ๐ Re-validated: Numeric values match (${normalizedOutput} === ${normalizedExpected}), marking as PASS`);1121 }1122 }1123 // Check if output is array and expected is array length (count)1124 else if (normalizedOutput.startsWith('[') && !isNaN(normalizedExpected)) {1125 try {1126 const outputArray = JSON.parse(normalizedOutput);1127 if (Array.isArray(outputArray) && outputArray.length === Number(normalizedExpected)) {1128 isPassed = true;1129 console.log(` ๐ Re-validated: Array length matches expected count (${outputArray.length} === ${normalizedExpected}), marking as PASS`);1130 }1131 } catch (e) {}1132 }1133 // Check for in-place modification problems (expected = "merged")1134 else if (normalizedExpected === 'merged' && normalizedOutput.startsWith('[')) {1135 try {1136 const outputArr = JSON.parse(normalizedOutput);1137 const inputObj = JSON.parse(testCase.input);1138 1139 // For merge sorted array: check if output is sorted1140 if (inputObj.nums1 && inputObj.nums2 && inputObj.m !== undefined && inputObj.n !== undefined) {1141 const isSorted = outputArr.every((val, i, arr) => i === 0 || arr[i - 1] <= val);1142 const expectedLength = inputObj.m + inputObj.n;1143 1144 if (isSorted && outputArr.length === expectedLength) {1145 isPassed = true;1146 console.log(` ๐ In-place modification: Array is sorted with correct length (${outputArr.length})`);1147 }1148 }1149 } catch (e) {}1150 }1151 // Check for palindrome problems - any palindrome of same length is valid1152 else if (typeof normalizedOutput === 'string' && typeof normalizedExpected === 'string') {1153 const isPalindrome = (s) => s === s.split('').reverse().join('');1154 1155 if (normalizedOutput.length === normalizedExpected.length && 1156 isPalindrome(normalizedOutput) && 1157 isPalindrome(normalizedExpected)) {1158 isPassed = true;1159 console.log(` ๐ Palindrome validation: "${normalizedOutput}" is a valid palindrome (same length as "${normalizedExpected}")`);1160 }1161 }1162 // Dynamic validation for problems with multiple valid outputs1163 if (!isPassed && normalizedOutput.startsWith('[') && normalizedExpected.startsWith('[')) {1164 try {1165 const outputArr = JSON.parse(normalizedOutput);1166 const expectedArr = JSON.parse(normalizedExpected);1167 1168 if (Array.isArray(outputArr) && Array.isArray(expectedArr)) {1169 const inputObj = JSON.parse(testCase.input);1170 1171 // Validate using dynamic validator1172 const validationResult = validateMultipleOutputs(outputArr, expectedArr, inputObj);1173 if (validationResult.isValid) {1174 isPassed = true;1175 console.log(` ๐ Dynamic validation: ${validationResult.message}`);1176 }1177 }1178 } catch (e) {1179 // Not valid JSON arrays, skip this validation1180 }1181 }1182 // Finally, try custom verifier if available and nothing else matched1183 if (!isPassed && options.verifierType) {1184 console.log(` ๐ง Using custom verifier: ${options.verifierType}`);1185 const verifyResult = AnswerVerifier.verify(1186 options.verifierType,1187 testCase.input,1188 normalizedOutput,1189 normalizedExpected,1190 options.verifierMetadata || {}1191 );1192 isPassed = verifyResult.valid;1193 console.log(` ๐ Verifier result: ${verifyResult.valid ? 'โ
VALID' : 'โ INVALID'} - ${verifyResult.reason}`);1194 1195 // Store verification info for frontend1196 if (isPassed && verifyResult.allValidOutputs) {1197 testCase.verificationReason = verifyResult.reason;1198 testCase.validOutputs = verifyResult.allValidOutputs;1199 }1200 }