@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
JavaScript
"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
};
}