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

587 lines (586 loc) 21.8 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.IntegrationTestFramework = void 0; exports.createTestScenario = createTestScenario; exports.createCommandStep = createCommandStep; exports.createFileExpectation = createFileExpectation; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const os = __importStar(require("os")); const crypto = __importStar(require("crypto")); class IntegrationTestFramework extends events_1.EventEmitter { constructor(config) { super(); this.environments = new Map(); this.results = []; this.config = { parallel: false, maxConcurrency: 4, timeout: 300000, // 5 minutes retries: 0, cleanupAfterEach: true, captureOutput: true, recordVideo: false, generateReport: true, ...config }; } async run() { this.emit('test:start', { scenarios: this.config.scenarios.length }); const startTime = Date.now(); const scenarios = this.filterScenarios(); try { if (this.config.parallel) { await this.runParallel(scenarios); } else { await this.runSequential(scenarios); } const report = this.generateReport(Date.now() - startTime); this.emit('test:complete', report); return report; } catch (error) { this.emit('test:error', error); throw error; } finally { await this.cleanup(); } } async runScenario(scenario) { this.emit('scenario:start', scenario); const result = { scenario: scenario.name, success: false, duration: 0, steps: [], expectations: [], artifacts: [] }; const startTime = Date.now(); const env = await this.createEnvironment(scenario); try { // Run setup steps if (scenario.setup) { await this.runSetup(scenario.setup, env); } // Run test steps for (const step of scenario.steps) { const stepResult = await this.runStep(step, env); result.steps.push(stepResult); if (!stepResult.success && !step.continueOnFailure) { throw new Error(`Step failed: ${step.description || step.action}`); } } // Check expectations for (const expectation of scenario.expectations) { const expectationResult = await this.checkExpectation(expectation, env); result.expectations.push(expectationResult); } result.success = result.expectations.every(e => e.success); result.duration = Date.now() - startTime; result.artifacts = env.artifacts; this.emit('scenario:complete', result); return result; } catch (error) { result.error = error; result.duration = Date.now() - startTime; this.emit('scenario:error', { scenario, error }); return result; } finally { // Run teardown steps if (scenario.teardown) { await this.runTeardown(scenario.teardown, env); } if (this.config.cleanupAfterEach) { await this.cleanupEnvironment(env); } } } async createEnvironment(scenario) { const id = crypto.randomBytes(8).toString('hex'); const workDir = path.join(os.tmpdir(), 're-shell-test', id); await fs.ensureDir(workDir); const env = { id, workDir, env: { ...process.env }, processes: new Map(), artifacts: [] }; this.environments.set(id, env); return env; } async runSetup(steps, env) { for (const step of steps) { this.emit('setup:step', step); switch (step.type) { case 'directory': await fs.ensureDir(path.join(env.workDir, step.action)); break; case 'file': const filePath = path.join(env.workDir, step.action); await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, step.config?.content || ''); break; case 'env': Object.assign(env.env, step.config || {}); break; case 'service': await this.startService(step.action, step.config, env); break; case 'custom': await this.runCustomSetup(step, env); break; } } } async runStep(step, env) { this.emit('step:start', step); const result = { step: step.description || step.action, success: false, duration: 0 }; const startTime = Date.now(); let retries = 0; while (retries <= (step.retries || 0)) { try { const output = await this.executeStep(step, env); result.output = output; result.success = true; break; } catch (error) { result.error = error.message; retries++; if (retries > (step.retries || 0)) { break; } await this.wait(1000 * retries); // Exponential backoff } } result.duration = Date.now() - startTime; result.retries = retries; this.emit('step:complete', result); return result; } async executeStep(step, env) { switch (step.type) { case 'command': return this.executeCommand(step, env); case 'file': return this.executeFileOperation(step, env); case 'http': return this.executeHttpRequest(step, env); case 'process': return this.executeProcessOperation(step, env); case 'wait': await this.wait(step.args?.duration || 1000); return 'Wait completed'; case 'custom': return this.executeCustomStep(step, env); default: throw new Error(`Unknown step type: ${step.type}`); } } async executeCommand(step, env) { const command = step.action; const args = step.args || {}; const timeout = step.timeout || 30000; try { const result = (0, child_process_1.execSync)(command, { cwd: args.cwd || env.workDir, env: { ...env.env, ...args.env }, timeout, encoding: 'utf-8' }); if (step.expectedOutput) { if (typeof step.expectedOutput === 'string') { if (!result.includes(step.expectedOutput)) { throw new Error(`Output does not contain expected: ${step.expectedOutput}`); } } else if (step.expectedOutput instanceof RegExp) { if (!step.expectedOutput.test(result)) { throw new Error(`Output does not match pattern: ${step.expectedOutput}`); } } } return result; } catch (error) { if (error.status !== undefined && step.expectedExitCode !== undefined) { if (error.status !== step.expectedExitCode) { throw new Error(`Exit code ${error.status} does not match expected ${step.expectedExitCode}`); } } else if (error.status !== 0 && step.expectedExitCode === undefined) { throw error; } return error.stdout || ''; } } async executeFileOperation(step, env) { const filePath = path.join(env.workDir, step.args?.path || step.action); switch (step.args?.operation || 'create') { case 'create': await fs.writeFile(filePath, step.args?.content || ''); return `File created: ${filePath}`; case 'append': await fs.appendFile(filePath, step.args?.content || ''); return `Content appended to: ${filePath}`; case 'delete': await fs.remove(filePath); return `File deleted: ${filePath}`; case 'copy': const destPath = path.join(env.workDir, step.args?.destination); await fs.copy(filePath, destPath); return `File copied to: ${destPath}`; case 'move': const newPath = path.join(env.workDir, step.args?.destination); await fs.move(filePath, newPath); return `File moved to: ${newPath}`; default: throw new Error(`Unknown file operation: ${step.args?.operation}`); } } async executeHttpRequest(step, env) { // Simplified HTTP request implementation // In production, use a proper HTTP client library const url = step.action; const method = step.args?.method || 'GET'; const headers = step.args?.headers || {}; const body = step.args?.body; // Mock implementation return `HTTP ${method} ${url}`; } async executeProcessOperation(step, env) { const operation = step.args?.operation || 'start'; const processName = step.action; switch (operation) { case 'start': const process = (0, child_process_1.spawn)(step.args?.command || processName, step.args?.args || [], { cwd: env.workDir, env: env.env, detached: false }); env.processes.set(processName, process); return `Process started: ${processName}`; case 'stop': const proc = env.processes.get(processName); if (proc) { proc.kill(step.args?.signal || 'SIGTERM'); env.processes.delete(processName); } return `Process stopped: ${processName}`; case 'restart': await this.executeProcessOperation({ ...step, args: { ...step.args, operation: 'stop' } }, env); await this.wait(1000); return this.executeProcessOperation({ ...step, args: { ...step.args, operation: 'start' } }, env); default: throw new Error(`Unknown process operation: ${operation}`); } } async executeCustomStep(step, env) { // Custom step execution would be implemented by extending this class return `Custom step: ${step.action}`; } async checkExpectation(expectation, env) { this.emit('expectation:check', expectation); const result = { expectation: `${expectation.type} ${expectation.condition} ${expectation.target}`, success: false }; try { switch (expectation.type) { case 'file': await this.checkFileExpectation(expectation, env, result); break; case 'directory': await this.checkDirectoryExpectation(expectation, env, result); break; case 'output': await this.checkOutputExpectation(expectation, env, result); break; case 'process': await this.checkProcessExpectation(expectation, env, result); break; case 'http': await this.checkHttpExpectation(expectation, env, result); break; case 'custom': await this.checkCustomExpectation(expectation, env, result); break; } } catch (error) { result.error = error.message; } this.emit('expectation:result', result); return result; } async checkFileExpectation(expectation, env, result) { const filePath = path.join(env.workDir, expectation.target); switch (expectation.condition) { case 'exists': result.success = await fs.pathExists(filePath); result.actual = result.success ? 'exists' : 'does not exist'; result.expected = 'exists'; break; case 'contains': if (await fs.pathExists(filePath)) { const content = await fs.readFile(filePath, 'utf-8'); result.success = content.includes(expectation.value); result.actual = content.substring(0, 100) + '...'; result.expected = `contains "${expectation.value}"`; } break; case 'matches': if (await fs.pathExists(filePath)) { const content = await fs.readFile(filePath, 'utf-8'); const regex = new RegExp(expectation.value); result.success = regex.test(content); result.actual = content.substring(0, 100) + '...'; result.expected = `matches ${regex}`; } break; case 'equals': if (await fs.pathExists(filePath)) { const content = await fs.readFile(filePath, 'utf-8'); result.success = content === expectation.value; result.actual = content; result.expected = expectation.value; } break; } } async checkDirectoryExpectation(expectation, env, result) { const dirPath = path.join(env.workDir, expectation.target); switch (expectation.condition) { case 'exists': result.success = await fs.pathExists(dirPath); result.actual = result.success ? 'exists' : 'does not exist'; result.expected = 'exists'; break; case 'contains': if (await fs.pathExists(dirPath)) { const files = await fs.readdir(dirPath); result.success = files.includes(expectation.value); result.actual = files; result.expected = `contains "${expectation.value}"`; } break; } } async checkOutputExpectation(expectation, env, result) { // This would check captured output from steps result.success = true; } async checkProcessExpectation(expectation, env, result) { const process = env.processes.get(expectation.target); switch (expectation.condition) { case 'exists': result.success = process !== undefined && !process.killed; result.actual = process ? 'running' : 'not running'; result.expected = 'running'; break; } } async checkHttpExpectation(expectation, env, result) { // Mock HTTP expectation check result.success = true; } async checkCustomExpectation(expectation, env, result) { // Custom expectation check result.success = true; } async runTeardown(steps, env) { for (const step of steps) { this.emit('teardown:step', step); try { switch (step.type) { case 'cleanup': await fs.remove(path.join(env.workDir, step.action)); break; case 'service': case 'process': const process = env.processes.get(step.action); if (process) { process.kill(step.force ? 'SIGKILL' : 'SIGTERM'); env.processes.delete(step.action); } break; case 'custom': await this.runCustomTeardown(step, env); break; } } catch (error) { this.emit('teardown:error', { step, error }); } } } async startService(name, config, env) { // Service startup implementation } async runCustomSetup(step, env) { // Custom setup implementation } async runCustomTeardown(step, env) { // Custom teardown implementation } async cleanupEnvironment(env) { // Kill all processes for (const [name, process] of env.processes) { try { process.kill('SIGKILL'); } catch { // Process might already be dead } } env.processes.clear(); // Remove work directory try { await fs.remove(env.workDir); } catch { // Directory might already be removed } this.environments.delete(env.id); } async cleanup() { for (const env of this.environments.values()) { await this.cleanupEnvironment(env); } } filterScenarios() { const hasOnly = this.config.scenarios.some(s => s.only); if (hasOnly) { return this.config.scenarios.filter(s => s.only && !s.skip); } return this.config.scenarios.filter(s => !s.skip); } async runSequential(scenarios) { for (const scenario of scenarios) { const result = await this.runScenario(scenario); this.results.push(result); } } async runParallel(scenarios) { const chunks = this.chunkArray(scenarios, this.config.maxConcurrency); for (const chunk of chunks) { const promises = chunk.map(scenario => this.runScenario(scenario)); const results = await Promise.all(promises); this.results.push(...results); } } chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } generateReport(duration) { const summary = { total: this.results.length, passed: this.results.filter(r => r.success).length, failed: this.results.filter(r => !r.success).length, skipped: this.config.scenarios.filter(s => s.skip).length, duration }; const allArtifacts = []; this.results.forEach(r => { if (r.artifacts) { allArtifacts.push(...r.artifacts); } }); return { summary, results: this.results, artifacts: allArtifacts, duration, timestamp: new Date() }; } wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Public utility methods addScenario(scenario) { this.config.scenarios.push(scenario); } getResults() { return this.results; } getReport() { if (this.results.length === 0) return null; return this.generateReport(0); } } exports.IntegrationTestFramework = IntegrationTestFramework; // Export utility functions function createTestScenario(config) { return { name: 'Test Scenario', steps: [], expectations: [], ...config }; } function createCommandStep(command, options) { return { type: 'command', action: command, ...options }; } function createFileExpectation(path, condition, value) { return { type: 'file', target: path, condition, value }; }