UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

924 lines (923 loc) 34.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrorScenarioTesting = void 0; exports.createErrorScenario = createErrorScenario; exports.createErrorTrigger = createErrorTrigger; exports.runErrorScenarios = runErrorScenarios; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const os = __importStar(require("os")); const child_process_1 = require("child_process"); class ErrorScenarioTesting extends events_1.EventEmitter { constructor(config) { super(); this.results = []; this.logs = []; this.config = { retryAttempts: 3, timeout: 30000, parallel: false, generateReport: true, captureStdout: true, captureStderr: true, validateRecovery: true, cleanupAfterEach: true, ...config }; this.workDir = path.join(os.tmpdir(), 're-shell-error-test'); } async run() { this.emit('errortest:start', { scenarios: this.config.scenarios.length }); const startTime = Date.now(); try { await this.setup(); const scenarios = this.config.scenarios.filter(s => !s.skip); if (this.config.parallel) { await this.runParallel(scenarios); } else { await this.runSequential(scenarios); } const report = this.generateReport(Date.now() - startTime); if (this.config.generateReport) { await this.saveReport(report); } this.emit('errortest:complete', report); return report; } catch (error) { this.emit('errortest:error', error); throw error; } finally { await this.cleanup(); } } async setup() { await fs.ensureDir(this.workDir); this.log('info', 'Test environment prepared', { workDir: this.workDir }); } async runSequential(scenarios) { for (const scenario of scenarios) { const result = await this.runScenario(scenario); this.results.push(result); if (this.config.cleanupAfterEach) { await this.cleanupScenario(scenario); } } } async runParallel(scenarios) { const promises = scenarios.map(scenario => this.runScenario(scenario)); const results = await Promise.all(promises); this.results.push(...results); } async runScenario(scenario) { this.emit('scenario:start', scenario); this.log('info', `Running error scenario: ${scenario.name}`); const result = { scenario: scenario.name, success: false, duration: 0, attempts: 0, logs: [], artifacts: [], timestamp: new Date() }; const startTime = Date.now(); try { // Setup prerequisites if (scenario.prerequisites) { await this.runPrerequisites(scenario.prerequisites); } // Trigger error condition result.error = await this.triggerError(scenario); result.attempts++; // Validate expected error if (scenario.expectedError && result.error) { result.error.matched = this.validateExpectedError(result.error, scenario.expectedError); } // Attempt recovery if configured if (scenario.recovery && this.config.validateRecovery) { result.recovery = await this.attemptRecovery(scenario.recovery, result.error); result.success = result.recovery.successful; } else { result.success = result.error?.matched ?? false; } // Run validation checks if (scenario.validation) { result.validation = await this.runValidation(scenario.validation); result.success = result.success && result.validation.every(v => v.success); } result.duration = Date.now() - startTime; this.log('info', `Scenario completed: ${scenario.name}`, { success: result.success, duration: result.duration }); this.emit('scenario:complete', result); return result; } catch (error) { result.duration = Date.now() - startTime; this.log('error', `Scenario failed: ${scenario.name}`, { error: error.message }); this.emit('scenario:error', { scenario, error }); return result; } } async runPrerequisites(prerequisites) { for (const prereq of prerequisites) { try { await this.executeCommand(prereq); this.log('debug', `Prerequisite completed: ${prereq}`); } catch (error) { this.log('warn', `Prerequisite failed: ${prereq}`, { error: error.message }); } } } async triggerError(scenario) { this.log('debug', `Triggering error: ${scenario.trigger.type}`); const trigger = scenario.trigger; let capturedError; try { switch (trigger.type) { case 'command': capturedError = await this.triggerCommandError(trigger); break; case 'file': capturedError = await this.triggerFileError(trigger); break; case 'network': capturedError = await this.triggerNetworkError(trigger); break; case 'process': capturedError = await this.triggerProcessError(trigger); break; case 'memory': capturedError = await this.triggerMemoryError(trigger); break; case 'signal': capturedError = await this.triggerSignalError(trigger); break; case 'custom': capturedError = await this.triggerCustomError(trigger); break; default: throw new Error(`Unknown trigger type: ${trigger.type}`); } } catch (error) { capturedError = { type: 'exception', message: error.message, code: error.code, signal: error.signal, stack: error.stack, stdout: error.stdout, stderr: error.stderr, matched: false }; } this.log('debug', 'Error triggered', { error: capturedError }); return capturedError; } async triggerCommandError(trigger) { const command = trigger.action; const args = trigger.args || {}; try { const result = (0, child_process_1.execSync)(command, { cwd: this.workDir, timeout: this.config.timeout, encoding: 'utf-8', stdio: 'pipe', env: { ...process.env, ...args.env } }); // Command succeeded when we expected failure return { type: 'unexpected_success', message: 'Command succeeded unexpectedly', stdout: result, matched: false }; } catch (error) { return { type: 'command_error', message: error.message, code: error.status, signal: error.signal, stdout: error.stdout, stderr: error.stderr, matched: false }; } } async triggerFileError(trigger) { const action = trigger.action; const args = trigger.args || {}; try { switch (action) { case 'delete_required': const requiredFile = path.join(this.workDir, args.file || 'required.txt'); await fs.remove(requiredFile); break; case 'corrupt_file': const corruptFile = path.join(this.workDir, args.file || 'data.json'); await fs.writeFile(corruptFile, '{"invalid": json}'); break; case 'permission_denied': const restrictedFile = path.join(this.workDir, args.file || 'restricted.txt'); await fs.writeFile(restrictedFile, 'restricted content'); await fs.chmod(restrictedFile, 0o000); break; case 'disk_full': // Simulate disk full by creating a large file const largeContent = 'x'.repeat(1024 * 1024 * 100); // 100MB await fs.writeFile(path.join(this.workDir, 'large.tmp'), largeContent); break; } // Now try to use the file to trigger the error const testCommand = args.testCommand || `cat ${args.file || 'required.txt'}`; (0, child_process_1.execSync)(testCommand, { cwd: this.workDir, encoding: 'utf-8' }); return { type: 'file_error_not_triggered', message: 'File error was not triggered as expected', matched: false }; } catch (error) { return { type: 'file_error', message: error.message, code: error.code, stdout: error.stdout, stderr: error.stderr, matched: false }; } } async triggerNetworkError(trigger) { const action = trigger.action; const args = trigger.args || {}; try { switch (action) { case 'connection_refused': const command = `curl -f ${args.url || 'http://localhost:99999'} --max-time 5`; (0, child_process_1.execSync)(command, { encoding: 'utf-8' }); break; case 'timeout': const timeoutCommand = `curl -f ${args.url || 'http://httpbin.org/delay/30'} --max-time 5`; (0, child_process_1.execSync)(timeoutCommand, { encoding: 'utf-8' }); break; case 'dns_failure': const dnsCommand = `curl -f http://nonexistent.invalid --max-time 5`; (0, child_process_1.execSync)(dnsCommand, { encoding: 'utf-8' }); break; } return { type: 'network_error_not_triggered', message: 'Network error was not triggered as expected', matched: false }; } catch (error) { return { type: 'network_error', message: error.message, code: error.status, stdout: error.stdout, stderr: error.stderr, matched: false }; } } async triggerProcessError(trigger) { const action = trigger.action; const args = trigger.args || {}; try { switch (action) { case 'spawn_failure': (0, child_process_1.spawn)('/nonexistent/command', [], { stdio: 'pipe' }); break; case 'child_exit': const child = (0, child_process_1.spawn)('node', ['-e', 'process.exit(1)'], { stdio: 'pipe' }); await new Promise((resolve, reject) => { child.on('exit', resolve); child.on('error', reject); }); break; case 'resource_exhaustion': // Try to spawn too many processes const processes = []; for (let i = 0; i < 1000; i++) { try { const proc = (0, child_process_1.spawn)('sleep', ['1'], { stdio: 'ignore' }); processes.push(proc); } catch (error) { // Clean up spawned processes processes.forEach(p => p.kill()); throw error; } } // Clean up processes.forEach(p => p.kill()); break; } return { type: 'process_error_not_triggered', message: 'Process error was not triggered as expected', matched: false }; } catch (error) { return { type: 'process_error', message: error.message, code: error.code, matched: false }; } } async triggerMemoryError(trigger) { const action = trigger.action; const args = trigger.args || {}; try { switch (action) { case 'out_of_memory': // Allocate large amounts of memory const arrays = []; for (let i = 0; i < 100; i++) { arrays.push(new Array(1024 * 1024).fill(0)); } break; case 'memory_leak': // Simulate memory leak const leak = []; const interval = setInterval(() => { leak.push(new Array(1024).fill(Math.random())); }, 1); setTimeout(() => clearInterval(interval), 5000); break; } return { type: 'memory_error_not_triggered', message: 'Memory error was not triggered as expected', matched: false }; } catch (error) { return { type: 'memory_error', message: error.message, matched: false }; } } async triggerSignalError(trigger) { const action = trigger.action; const args = trigger.args || {}; try { const child = (0, child_process_1.spawn)('node', ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'pipe' }); // Send signal after delay setTimeout(() => { child.kill(args.signal || 'SIGTERM'); }, args.delay || 100); await new Promise((resolve, reject) => { child.on('exit', (code, signal) => { resolve({ code, signal }); }); child.on('error', reject); }); return { type: 'signal_error', message: `Process killed with signal: ${args.signal || 'SIGTERM'}`, signal: args.signal || 'SIGTERM', matched: false }; } catch (error) { return { type: 'signal_error', message: error.message, signal: args.signal, matched: false }; } } async triggerCustomError(trigger) { // Custom error triggers would be implemented by extending this class return { type: 'custom_error', message: `Custom error: ${trigger.action}`, matched: false }; } validateExpectedError(actual, expected) { switch (expected.type) { case 'exception': if (expected.pattern) { const regex = typeof expected.pattern === 'string' ? new RegExp(expected.pattern) : expected.pattern; return regex.test(actual.message); } return true; case 'exit_code': return actual.code === expected.code; case 'signal': return actual.signal === expected.signal; case 'output': if (expected.pattern && actual.stdout) { const regex = typeof expected.pattern === 'string' ? new RegExp(expected.pattern) : expected.pattern; return regex.test(actual.stdout); } return false; default: return false; } } async attemptRecovery(recoveryActions, error) { this.log('info', 'Attempting recovery'); const result = { attempted: true, successful: false, actions: [], duration: 0, finalState: 'failed' }; const startTime = Date.now(); for (const action of recoveryActions) { const actionResult = await this.executeRecoveryAction(action, error); result.actions.push(actionResult); if (!actionResult.success) { this.log('warn', `Recovery action failed: ${action.action}`); break; } } result.duration = Date.now() - startTime; result.successful = result.actions.every(a => a.success); result.finalState = result.successful ? 'recovered' : result.actions.some(a => a.success) ? 'partial' : 'failed'; this.log('info', 'Recovery attempt completed', { successful: result.successful, state: result.finalState }); return result; } async executeRecoveryAction(action, error) { this.log('debug', `Executing recovery action: ${action.type}`); const result = { action: action.action, success: false, duration: 0 }; const startTime = Date.now(); try { switch (action.type) { case 'retry': await this.retryAction(action); break; case 'fallback': await this.fallbackAction(action); break; case 'cleanup': await this.cleanupAction(action); break; case 'restart': await this.restartAction(action); break; case 'custom': await this.customRecoveryAction(action); break; } result.success = true; result.duration = Date.now() - startTime; } catch (recoveryError) { result.error = recoveryError.message; result.duration = Date.now() - startTime; } return result; } async retryAction(action) { const maxAttempts = action.maxAttempts || 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { await this.executeCommand(action.action); return; } catch (error) { if (attempt === maxAttempts) { throw error; } await this.wait(1000 * attempt); // Exponential backoff } } } async fallbackAction(action) { await this.executeCommand(action.action); } async cleanupAction(action) { if (action.action.startsWith('rm ') || action.action.startsWith('delete ')) { const target = action.action.split(' ')[1]; await fs.remove(path.join(this.workDir, target)); } else { await this.executeCommand(action.action); } } async restartAction(action) { // Simulate service restart await this.executeCommand(action.action); } async customRecoveryAction(action) { // Custom recovery actions would be implemented by extending this class this.log('debug', `Custom recovery: ${action.action}`); } async runValidation(checks) { const results = []; for (const check of checks) { const result = await this.executeValidationCheck(check); results.push(result); } return results; } async executeValidationCheck(check) { const result = { check: `${check.type}:${check.target}`, success: false }; try { switch (check.type) { case 'file': await this.validateFile(check, result); break; case 'process': await this.validateProcess(check, result); break; case 'state': await this.validateState(check, result); break; case 'output': await this.validateOutput(check, result); break; case 'custom': await this.validateCustom(check, result); break; } } catch (error) { result.error = error.message; } return result; } async validateFile(check, result) { const filePath = path.join(this.workDir, check.target); switch (check.condition) { case 'exists': const exists = await fs.pathExists(filePath); result.success = exists; result.actual = exists; result.expected = true; break; case 'contains': if (await fs.pathExists(filePath)) { const content = await fs.readFile(filePath, 'utf-8'); result.success = content.includes(check.value); result.actual = content.substring(0, 100); result.expected = `contains "${check.value}"`; } break; } } async validateProcess(check, result) { switch (check.condition) { case 'running': try { (0, child_process_1.execSync)(`pgrep -f "${check.target}"`, { encoding: 'utf-8' }); result.success = true; result.actual = 'running'; result.expected = 'running'; } catch { result.success = false; result.actual = 'not running'; result.expected = 'running'; } break; } } async validateState(check, result) { // State validation implementation result.success = true; } async validateOutput(check, result) { // Output validation implementation result.success = true; } async validateCustom(check, result) { // Custom validation implementation result.success = true; } async executeCommand(command) { return (0, child_process_1.execSync)(command, { cwd: this.workDir, encoding: 'utf-8', timeout: this.config.timeout }); } async cleanupScenario(scenario) { if (scenario.cleanup) { for (const cleanup of scenario.cleanup) { try { await this.executeCommand(cleanup); } catch { // Ignore cleanup errors } } } } async cleanup() { try { await fs.remove(this.workDir); } catch { // Ignore cleanup errors } } generateReport(duration) { const summary = this.generateSummary(duration); const analysis = this.analyzeResults(); const recommendations = this.generateRecommendations(analysis); const patterns = this.identifyPatterns(); return { summary, results: this.results, analysis, recommendations, patterns, timestamp: new Date() }; } generateSummary(duration) { const categories = new Map(); const severity = new Map(); for (const result of this.results) { const scenario = this.config.scenarios.find(s => s.name === result.scenario); if (scenario) { categories.set(scenario.category, (categories.get(scenario.category) || 0) + 1); const sev = scenario.expectedError?.severity || 'medium'; severity.set(sev, (severity.get(sev) || 0) + 1); } } return { totalScenarios: this.results.length, passed: this.results.filter(r => r.success).length, failed: this.results.filter(r => !r.success).length, recovered: this.results.filter(r => r.recovery?.successful).length, unrecovered: this.results.filter(r => r.recovery && !r.recovery.successful).length, categories, severity, duration: duration / 1000 }; } analyzeResults() { const resilience = this.calculateResilience(); const hotspots = this.identifyHotspots(); const trends = this.analyzeTrends(); const gaps = this.identifyGaps(); return { resilience, hotspots, trends, gaps }; } calculateResilience() { const total = this.results.length; const errors = this.results.filter(r => r.error).length; const recovered = this.results.filter(r => r.recovery?.successful).length; const partialRecoveries = this.results.filter(r => r.recovery?.finalState === 'partial').length; const recoveryTimes = this.results .filter(r => r.recovery) .map(r => r.recovery.duration); const criticalFailures = this.results.filter(r => { const scenario = this.config.scenarios.find(s => s.name === r.scenario); return scenario?.expectedError?.severity === 'critical' && !r.success; }).length; return { errorRate: total > 0 ? (errors / total) * 100 : 0, recoveryRate: errors > 0 ? (recovered / errors) * 100 : 0, averageRecoveryTime: recoveryTimes.length > 0 ? recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length : 0, criticalFailures, partialRecoveries }; } identifyHotspots() { const hotspots = []; const categoryStats = new Map(); for (const result of this.results) { const scenario = this.config.scenarios.find(s => s.name === result.scenario); if (scenario) { const stats = categoryStats.get(scenario.category) || { count: 0, failures: 0 }; stats.count++; if (!result.success) stats.failures++; categoryStats.set(scenario.category, stats); } } for (const [category, stats] of categoryStats) { const failureRate = (stats.failures / stats.count) * 100; if (failureRate > 50) { hotspots.push({ category, count: stats.failures, severity: failureRate > 80 ? 'critical' : 'high', description: `High failure rate in ${category} operations (${failureRate.toFixed(1)}%)`, impact: `${stats.failures} out of ${stats.count} scenarios failed`, suggestion: `Review and strengthen ${category} error handling` }); } } return hotspots; } analyzeTrends() { // Simple trend analysis return []; } identifyGaps() { const gaps = []; for (const result of this.results) { if (result.recovery && !result.recovery.successful) { gaps.push({ scenario: result.scenario, issue: 'Recovery failed', impact: 'high', recommendation: 'Implement additional recovery mechanisms' }); } if (!result.recovery && result.error) { gaps.push({ scenario: result.scenario, issue: 'No recovery mechanism defined', impact: 'medium', recommendation: 'Add recovery actions for this error scenario' }); } } return gaps; } generateRecommendations(analysis) { const recommendations = []; if (analysis.resilience.recoveryRate < 80) { recommendations.push(`Improve recovery mechanisms - current rate: ${analysis.resilience.recoveryRate.toFixed(1)}%`); } if (analysis.resilience.criticalFailures > 0) { recommendations.push(`Address ${analysis.resilience.criticalFailures} critical failure(s) immediately`); } for (const hotspot of analysis.hotspots) { if (hotspot.severity === 'critical') { recommendations.push(hotspot.suggestion); } } if (analysis.gaps.length > 0) { recommendations.push(`Implement recovery mechanisms for ${analysis.gaps.length} identified gap(s)`); } return recommendations; } identifyPatterns() { const patterns = []; const errorMessages = this.results .filter(r => r.error) .map(r => r.error.message); // Common error patterns const commonPatterns = [ { name: 'Permission Denied', pattern: /permission denied|access denied/i }, { name: 'File Not Found', pattern: /no such file|file not found/i }, { name: 'Network Error', pattern: /connection refused|timeout|network/i }, { name: 'Out of Memory', pattern: /out of memory|cannot allocate/i }, { name: 'Process Error', pattern: /child process|spawn|exit code/i } ]; for (const patternDef of commonPatterns) { const matches = errorMessages.filter(msg => patternDef.pattern.test(msg)); if (matches.length > 0) { patterns.push({ name: patternDef.name, pattern: patternDef.pattern, category: 'common', frequency: matches.length, examples: matches.slice(0, 3) }); } } return patterns; } async saveReport(report) { const reportPath = path.join(process.cwd(), 'error-test-report.json'); await fs.writeJson(reportPath, report, { spaces: 2 }); const summaryPath = reportPath.replace('.json', '-summary.txt'); await fs.writeFile(summaryPath, this.formatSummary(report)); this.emit('report:saved', { json: reportPath, summary: summaryPath }); } formatSummary(report) { const lines = [ 'Error Scenario Testing Report', '============================', '', `Date: ${report.timestamp.toISOString()}`, '', 'Summary:', ` Total Scenarios: ${report.summary.totalScenarios}`, ` Passed: ${report.summary.passed}`, ` Failed: ${report.summary.failed}`, ` Recovered: ${report.summary.recovered}`, ` Unrecovered: ${report.summary.unrecovered}`, '', 'Resilience Metrics:', ` Error Rate: ${report.analysis.resilience.errorRate.toFixed(1)}%`, ` Recovery Rate: ${report.analysis.resilience.recoveryRate.toFixed(1)}%`, ` Avg Recovery Time: ${report.analysis.resilience.averageRecoveryTime.toFixed(0)}ms`, ` Critical Failures: ${report.analysis.resilience.criticalFailures}`, '', 'Error Patterns:' ]; report.patterns.forEach(pattern => { lines.push(` ${pattern.name}: ${pattern.frequency} occurrences`); }); lines.push('', 'Recommendations:'); report.recommendations.forEach(rec => { lines.push(` - ${rec}`); }); return lines.join('\n'); } log(level, message, context) { const entry = { timestamp: new Date(), level, message, context }; this.logs.push(entry); this.emit('log', entry); } wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } exports.ErrorScenarioTesting = ErrorScenarioTesting; // Export utility functions function createErrorScenario(name, category, trigger, options) { return { name, category, trigger, ...options }; } function createErrorTrigger(type, action, args) { return { type, action, args }; } async function runErrorScenarios(scenarios, config) { const tester = new ErrorScenarioTesting({ scenarios, ...config }); return tester.run(); }