sanket3280/code-execution
0
1/**2 * Unified Execution Service3 * Orchestrates code execution using Docker or HackerEarth strategy4 */5 6const DataStructureSerializer = require('../serializers/DataStructureSerializer');7const CodeWrapperFactory = require('../wrappers/CodeWrapperFactory');8const DockerExecutionStrategy = require('../strategies/DockerExecutionStrategy');9const { executionConfig } = require('../../../config/execution.config');10const executionLogger = require('../../../utils/executionLogger.unified');11const crypto = require('crypto');12 13class UnifiedExecutionService {14 constructor() {15 this.serializer = new DataStructureSerializer();16 this.wrapperFactory = new CodeWrapperFactory();17 this.dockerStrategy = new DockerExecutionStrategy(this.wrapperFactory, this.serializer);18 // HackerEarth strategy will be added later19 this.heStrategy = null;20 }21 22 /**23 * Execute code against test cases using optimal strategy24 * @param {Object} options - Execution options25 * @param {string} options.sourceCode - User's source code26 * @param {string} options.language - Language code (JAVASCRIPT_NODE, PYTHON3, etc.)27 * @param {Array} options.testCases - Array of {input, expected}28 * @param {Object} options.metadata - Problem metadata29 * @param {boolean} options.runAllTestCases - Execute all at once (true) or batch (false)30 * @returns {Promise<ExecutionResult>}31 */32 async executeTestCases(options) {33 const executionId = crypto.randomBytes(8).toString('hex');34 const startTime = Date.now();35 36 // Validate input37 this.validateInput(options);38 39 const { sourceCode, language, testCases, metadata = {}, runAllTestCases = true } = options;40 41 // Log execution start42 executionLogger.logExecutionStart(executionId, {43 language,44 testCaseCount: testCases.length,45 strategy: executionConfig.strategy46 });47 48 try {49 // Select execution strategy50 const strategy = await this.selectStrategy();51 52 // Execute using selected strategy53 let results;54 try {55 results = await strategy.execute(sourceCode, language, testCases, metadata);56 } catch (error) {57 // Try fallback if enabled58 if (executionConfig.enableFallback) {59 const fallbackStrategy = await this.getFallbackStrategy(strategy);60 if (fallbackStrategy) {61 executionLogger.logStrategyFallback(62 executionId,63 strategy.getName(),64 fallbackStrategy.getName(),65 error.message66 );67 results = await fallbackStrategy.execute(sourceCode, language, testCases, metadata);68 } else {69 throw error;70 }71 } else {72 throw error;73 }74 }75 76 // Aggregate results77 const aggregated = this.aggregateResults(results, testCases);78 79 // Add execution metadata80 const executionTime = Date.now() - startTime;81 aggregated.executionTime = executionTime;82 aggregated.strategy = strategy.getName();83 aggregated.executionId = executionId;84 85 // Log completion86 executionLogger.logExecutionComplete(executionId, aggregated);87 88 // Log performance metrics89 if (aggregated.avgTimeMs !== undefined) {90 executionLogger.logPerformanceMetrics(executionId, aggregated);91 }92 93 return aggregated;94 } catch (error) {95 executionLogger.logExecutionError(executionId, error, executionConfig.strategy);96 throw error;97 }98 }99 100 /**101 * Validate input parameters102 */103 validateInput(options) {104 const { sourceCode, language, testCases } = options;105 106 if (!sourceCode || typeof sourceCode !== 'string' || sourceCode.trim().length === 0) {107 throw new Error('Source code is required and must be a non-empty string');108 }109 110 if (sourceCode.length > executionConfig.limits.maxSourceCodeSize) {111 throw new Error(`Source code exceeds maximum size of ${executionConfig.limits.maxSourceCodeSize} bytes`);112 }113 114 if (!language || typeof language !== 'string') {115 throw new Error('Language is required and must be a string');116 }117 118 if (!this.wrapperFactory.isLanguageSupported(language)) {119 throw new Error(`Language ${language} is not supported`);120 }121 122 if (!Array.isArray(testCases) || testCases.length === 0) {123 throw new Error('Test cases must be a non-empty array');124 }125 126 if (testCases.length > executionConfig.limits.maxTestCases) {127 throw new Error(`Test case count exceeds maximum of ${executionConfig.limits.maxTestCases}`);128 }129 130 // Validate each test case131 testCases.forEach((tc, index) => {132 if (!tc || typeof tc !== 'object') {133 throw new Error(`Test case ${index} must be an object`);134 }135 if (!('input' in tc)) {136 throw new Error(`Test case ${index} must have an 'input' property`);137 }138 });139 }140 141 /**142 * Select execution strategy based on configuration143 */144 async selectStrategy() {145 const strategy = executionConfig.strategy;146 147 if (strategy === 'docker') {148 if (await this.dockerStrategy.isAvailable()) {149 return this.dockerStrategy;150 }151 throw new Error('Docker strategy selected but Docker is not available');152 }153 154 if (strategy === 'hackerearth') {155 if (this.heStrategy && await this.heStrategy.isAvailable()) {156 return this.heStrategy;157 }158 throw new Error('HackerEarth strategy selected but not configured');159 }160 161 // Auto mode: prefer Docker, fallback to HackerEarth162 if (strategy === 'auto') {163 if (await this.dockerStrategy.isAvailable()) {164 return this.dockerStrategy;165 }166 if (this.heStrategy && await this.heStrategy.isAvailable()) {167 return this.heStrategy;168 }169 throw new Error('No execution strategy available');170 }171 172 throw new Error(`Unknown execution strategy: ${strategy}`);173 }174 175 /**176 * Get fallback strategy177 */178 async getFallbackStrategy(currentStrategy) {179 if (!executionConfig.enableFallback) {180 return null;181 }182 183 // If current is Docker, try HackerEarth184 if (currentStrategy === this.dockerStrategy) {185 if (this.heStrategy && await this.heStrategy.isAvailable()) {186 return this.heStrategy;187 }188 }189 190 // If current is HackerEarth, try Docker191 if (currentStrategy === this.heStrategy) {192 if (await this.dockerStrategy.isAvailable()) {193 return this.dockerStrategy;194 }195 }196 197 return null;198 }199 200 /**201 * Aggregate individual test case results into summary202 */203 aggregateResults(results, testCases) {204 const total = results.length;205 let passed = 0;206 let failed = 0;207 let errors = 0;208 209 let totalTime = 0;210 let maxTime = 0;211 let totalMemory = 0;212 let maxMemory = 0;213 214 const cases = [];215 216 // Check for global error (CE, TLE, RE affecting all tests)217 const hasGlobalError = results.length > 0 && results.every(r =>218 r.status === 'CE' || r.status === 'TLE' || r.status === 'RE' || r.status === 'Error'219 );220 221 let globalError = null;222 if (hasGlobalError && results.length > 0) {223 const firstResult = results[0];224 globalError = {225 type: firstResult.status,226 message: firstResult.error || 'Execution failed',227 details: firstResult.error || ''228 };229 }230 231 results.forEach((result, index) => {232 const testCase = testCases[index] || {};233 234 // Determine pass/fail235 const pass = result.status === 'AC' || result.pass === true;236 237 if (pass) {238 passed++;239 } else if (result.status === 'RE' || result.status === 'CE' || result.status === 'TLE') {240 errors++;241 } else {242 failed++;243 }244 245 // Aggregate metrics246 const timeMs = result.executionTime || 0;247 const memoryKB = result.memoryUsed || 0;248 249 totalTime += timeMs;250 maxTime = Math.max(maxTime, timeMs);251 totalMemory += memoryKB;252 maxMemory = Math.max(maxMemory, memoryKB);253 254 // Build case result255 cases.push({256 caseId: index + 1,257 status: result.status || (pass ? 'AC' : 'WA'),258 pass,259 input: testCase.input,260 expected: testCase.expected,261 output: result.output,262 timeMs,263 memoryKB,264 error: result.error || null265 });266 });267 268 // Calculate averages269 const avgTimeMs = total > 0 ? Math.round(totalTime / total) : 0;270 const avgMemoryKB = total > 0 ? Math.round(totalMemory / total) : 0;271 272 // Estimate complexity273 const timeComplexity = this.estimateTimeComplexity(results);274 const spaceComplexity = this.estimateSpaceComplexity(results);275 276 return {277 summary: {278 total,279 passed,280 failed,281 errors282 },283 globalError,284 timeComplexity,285 spaceComplexity,286 avgTimeMs,287 maxTimeMs: maxTime,288 avgMemoryKB,289 maxMemoryKB: maxMemory,290 performanceLog: this.generatePerformanceLog(avgTimeMs, maxTime, avgMemoryKB, maxMemory),291 cases: hasGlobalError ? [] : cases292 };293 }294 295 /**296 * Estimate time complexity based on execution times297 */298 estimateTimeComplexity(results) {299 // Simple heuristic based on execution time variance300 if (results.length < 2) return 'O(1)';301 302 const times = results.map(r => r.executionTime || 0);303 const avgTime = times.reduce((a, b) => a + b, 0) / times.length;304 const variance = times.reduce((sum, t) => sum + Math.pow(t - avgTime, 2), 0) / times.length;305 306 // High variance suggests O(n) or worse307 if (variance > avgTime * 0.5) {308 return 'O(n)';309 }310 311 return 'O(1)';312 }313 314 /**315 * Estimate space complexity based on memory usage316 */317 estimateSpaceComplexity(results) {318 // Simple heuristic based on memory usage319 if (results.length < 2) return 'O(1)';320 321 const memories = results.map(r => r.memoryUsed || 0);322 const avgMemory = memories.reduce((a, b) => a + b, 0) / memories.length;323 324 // High memory usage suggests O(n)325 if (avgMemory > 1024) { // > 1MB326 return 'O(n)';327 }328 329 return 'O(1)';330 }331 332 /**333 * Generate performance log text334 */335 generatePerformanceLog(avgTimeMs, maxTimeMs, avgMemoryKB, maxMemoryKB) {336 return `Average execution time: ${avgTimeMs}ms, Max: ${maxTimeMs}ms. Average memory: ${avgMemoryKB}KB, Max: ${maxMemoryKB}KB.`;337 }338}339 340module.exports = UnifiedExecutionService;341 