UNPKG

@mmisty/cypress-allure-adapter

Version:

cypress allure adapter to generate allure results during tests execution (Allure TestOps compatible)

783 lines (782 loc) 33.9 kB
"use strict"; /** * Allure Task Client * * Client for communicating with the AllureTaskServer. * Supports both remote (separate process) and local (in-process) modes. */ 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; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.stopAllureTaskServer = exports.startAllureTaskServer = exports.getAllureTaskClient = exports.AllureTaskClient = void 0; const http_1 = __importDefault(require("http")); const child_process_1 = require("child_process"); const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const debug_1 = __importDefault(require("debug")); const common_1 = require("../common"); const allure_operations_1 = require("./allure-operations"); const allure_task_server_1 = require("./allure-task-server"); const debug = (0, debug_1.default)('cypress-allure:task-client'); /** * Resolve the path to the server script */ const resolveServerScriptPath = () => { const jsPath = path_1.default.resolve(__dirname, 'allure-task-server.js'); if ((0, fs_1.existsSync)(jsPath)) { return jsPath; } const tsPath = path_1.default.resolve(__dirname, 'allure-task-server.ts'); if ((0, fs_1.existsSync)(tsPath)) { return tsPath; } return jsPath; }; const needsTsNode = (scriptPath) => { return scriptPath.endsWith('.ts'); }; /** * Allure Task Client */ class AllureTaskClient { constructor(mode = 'remote') { this.port = null; this.serverProcess = null; this.isConnected = false; this.startPromise = null; this.maxRetries = 50; this.retryDelay = 100; this.mode = mode; debug(`AllureTaskClient created in ${mode} mode`); } getMode() { return this.mode; } /** * Start the server process (remote mode only) */ start() { return __awaiter(this, void 0, void 0, function* () { if (this.mode === 'local') { this.isConnected = true; return 0; } if (this.isConnected && this.port) { return this.port; } if (this.startPromise) { return this.startPromise; } debug('Starting task server process'); this.startPromise = this.doStart(); return this.startPromise; }); } doStart() { return __awaiter(this, void 0, void 0, function* () { const requestedPort = yield (0, allure_task_server_1.findAvailablePort)(); return new Promise((resolve, reject) => { var _a, _b; const serverScript = resolveServerScriptPath(); const useTsNode = needsTsNode(serverScript); debug(`Starting server from: ${serverScript} (ts-node: ${useTsNode})`); const command = useTsNode ? 'npx' : 'node'; const args = useTsNode ? ['ts-node', '--transpile-only', serverScript, '--port', String(requestedPort)] : [serverScript, '--port', String(requestedPort)]; this.serverProcess = (0, child_process_1.spawn)(command, args, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], detached: false, shell: process.platform === 'win32', env: process.env, // Pass all environment variables to child process (DEBUG, etc.) }); let portResolved = false; let outputBuffer = ''; // Timeout for server startup - cleared when server starts or fails const startupTimeout = setTimeout(() => { if (!portResolved) { this.killServer(); reject(new Error('Timeout waiting for task server to start')); } }, 15000); (_a = this.serverProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => { const output = data.toString(); outputBuffer += output; debug(`Server stdout: ${output.trim()}`); const portMatch = outputBuffer.match(/ALLURE_SERVER_PORT:(\d+)/); if (portMatch && !portResolved) { portResolved = true; clearTimeout(startupTimeout); this.port = parseInt(portMatch[1], 10); debug(`Server started on port ${this.port}`); this.isConnected = true; process.env[allure_operations_1.SERVER_PORT_ENV] = String(this.port); (0, common_1.logWithPackage)('log', `Allure task server running on port ${this.port} (separate process)`); resolve(this.port); } }); (_b = this.serverProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => { var _a; const output = data.toString(); // Forward server debug output to parent's stderr so it's visible // Debug package writes to stderr, so we forward it directly if ((_a = process.env.DEBUG) === null || _a === void 0 ? void 0 : _a.includes('cypress-allure')) { process.stderr.write(output); } else { debug(`Server stderr: ${output.trim()}`); } }); this.serverProcess.on('error', err => { debug(`Server process error: ${err.message}`); if (!portResolved) { clearTimeout(startupTimeout); reject(new Error(`Failed to start task server: ${err.message}`)); } }); this.serverProcess.on('exit', (code, signal) => { debug(`Server process exited: code=${code}, signal=${signal}`); this.isConnected = false; this.serverProcess = null; if (!portResolved) { clearTimeout(startupTimeout); reject(new Error(`Task server exited unexpectedly: code=${code}, signal=${signal}`)); } }); }); }); } /** * Wait for the server to be ready */ waitForReady() { return __awaiter(this, void 0, void 0, function* () { if (this.mode === 'local' || this.isConnected) { return; } if (this.startPromise) { yield this.startPromise; return; } // Wait for connection for (let i = 0; i < this.maxRetries; i++) { if (this.isConnected) { return; } yield new Promise(r => setTimeout(r, this.retryDelay)); } }); } killServer() { var _a, _b, _c, _d, _e, _f; if (this.serverProcess) { debug('Killing server process'); const proc = this.serverProcess; this.serverProcess = null; this.isConnected = false; // Remove all event listeners to prevent memory leaks and open handles (_a = proc.stdout) === null || _a === void 0 ? void 0 : _a.removeAllListeners(); (_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.removeAllListeners(); (_c = proc.stdin) === null || _c === void 0 ? void 0 : _c.removeAllListeners(); proc.removeAllListeners(); // Destroy streams (_d = proc.stdout) === null || _d === void 0 ? void 0 : _d.destroy(); (_e = proc.stderr) === null || _e === void 0 ? void 0 : _e.destroy(); (_f = proc.stdin) === null || _f === void 0 ? void 0 : _f.destroy(); // Disconnect IPC channel if connected if (proc.connected) { try { proc.disconnect(); } catch (_g) { // Ignore - might already be disconnected } } // Kill process - try SIGTERM first, then SIGKILL try { proc.kill('SIGTERM'); } catch (_h) { // Ignore } // Force kill after a short delay if still running const killTimer = setTimeout(() => { try { proc.kill('SIGKILL'); } catch (_a) { // Ignore - process already dead } }, 100); killTimer.unref(); // Don't keep Node alive for this timer } } stop() { return __awaiter(this, void 0, void 0, function* () { if (this.mode === 'local') { return; } debug('Stopping task client'); const proc = this.serverProcess; if (this.isConnected && this.port) { try { yield this.execute({ type: 'shutdown' }); } catch (_a) { // Server might already be down } } // Wait for server process to exit before killing if (proc && !proc.killed) { yield new Promise(resolve => { const timeout = setTimeout(() => { debug('Timeout waiting for server exit, force killing'); this.killServer(); resolve(); }, 500); const onExit = () => { clearTimeout(timeout); this.killServer(); resolve(); }; // Check if process is still valid before adding listener if (proc.exitCode !== null) { // Process already exited clearTimeout(timeout); this.killServer(); resolve(); } else { proc.once('exit', onExit); } }); } else { this.killServer(); } this.startPromise = null; this.port = null; }); } getPort() { return this.port; } /** * Check if the server is healthy and restart if needed */ ensureServerHealthy() { return __awaiter(this, void 0, void 0, function* () { if (this.mode === 'local') return true; // If no server process or it has exited, try to restart if (!this.serverProcess || this.serverProcess.exitCode !== null) { debug('Server process not running, attempting restart'); (0, common_1.logWithPackage)('warn', 'Task server process not running, attempting restart...'); this.isConnected = false; this.serverProcess = null; this.startPromise = null; try { yield this.start(); (0, common_1.logWithPackage)('log', 'Task server restarted successfully'); return true; } catch (err) { debug(`Failed to restart server: ${err.message}`); (0, common_1.logWithPackage)('error', `Failed to restart task server: ${err.message}`); return false; } } return this.isConnected; }); } /** * Execute an operation with retry logic */ execute(operation_1) { return __awaiter(this, arguments, void 0, function* (operation, retries = 3) { if (this.mode === 'local') { return this.executeLocal(operation); } for (let attempt = 0; attempt < retries; attempt++) { yield this.waitForReady(); // Check server health and restart if needed const healthy = yield this.ensureServerHealthy(); if (!healthy || !this.port) { if (attempt < retries - 1) { debug(`Server not healthy, retrying (attempt ${attempt + 1}/${retries})`); (0, common_1.logWithPackage)('warn', `Task server not healthy, retrying (attempt ${attempt + 1}/${retries})...`); yield new Promise(r => setTimeout(r, 100 * (attempt + 1))); continue; } (0, common_1.logWithPackage)('error', 'Task server connection failed after all retries'); return { success: false, error: 'Not connected to task server' }; } const result = yield this.executeRemote(operation); // If connection error, retry if (!result.success && this.isConnectionError(result.error)) { if (attempt < retries - 1) { debug(`Connection error, retrying (attempt ${attempt + 1}/${retries}): ${result.error}`); (0, common_1.logWithPackage)('warn', `Task server connection error: ${result.error}, retrying (attempt ${attempt + 1}/${retries})...`); this.isConnected = false; yield new Promise(r => setTimeout(r, 100 * (attempt + 1))); continue; } (0, common_1.logWithPackage)('error', `Task server connection failed: ${result.error}`); } return result; } (0, common_1.logWithPackage)('error', 'Task server: max retries exceeded'); return { success: false, error: 'Max retries exceeded' }; }); } /** * Check if an error is a connection error that should trigger retry */ isConnectionError(error) { if (!error) return false; const connectionErrors = ['ECONNREFUSED', 'ECONNRESET', 'socket hang up', 'EPIPE', 'ETIMEDOUT']; return connectionErrors.some(e => error.includes(e)); } executeRemote(operation) { return __awaiter(this, void 0, void 0, function* () { return new Promise(resolve => { const body = JSON.stringify(operation); const req = http_1.default.request({ hostname: 'localhost', port: this.port, path: allure_operations_1.SERVER_PATH, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }, timeout: 60000, }, res => { let responseBody = ''; res.on('data', chunk => { responseBody += chunk; }); res.on('end', () => { try { resolve(JSON.parse(responseBody)); } catch (_a) { resolve({ success: false, error: 'Invalid response from server' }); } }); }); req.on('error', err => { debug(`Request error: ${err.message}`); resolve({ success: false, error: err.message }); }); req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Request timeout' }); }); req.write(body); req.end(); }); }); } /** * Execute operation locally (for local mode) * This imports the server module and executes directly */ executeLocal(operation) { return __awaiter(this, void 0, void 0, function* () { // Dynamic import to avoid loading server dependencies in remote mode // const { AllureTaskServer } = await import('./allure-task-server'); // Create a temporary server instance for local execution // Note: This is not ideal but maintains compatibility // In production, remote mode should be used // const localServer = new AllureTaskServer(); // For local mode, we execute operations directly without starting HTTP server // We need to access the internal execute function // Since we can't easily do that, we'll implement basic local execution here return this.executeLocalOperation(operation); }); } executeLocalOperation(operation) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; // Import fs operations const fs = yield Promise.resolve().then(() => __importStar(require('fs/promises'))); const fsSync = yield Promise.resolve().then(() => __importStar(require('fs'))); if (operation.type === 'shutdown' || operation.type === 'health') { return { success: true }; } if (operation.type === 'batch') { const results = []; for (const op of operation.operations) { results.push(yield this.executeLocalOperation(op)); } const allSuccess = results.every(r => r.success); if (allSuccess) { return { success: true, data: results }; } return { success: false, error: 'Some batch operations failed' }; } // FS operations switch (operation.type) { case 'fs:mkdir': try { yield fs.mkdir(operation.path, { recursive: (_b = (_a = operation.options) === null || _a === void 0 ? void 0 : _a.recursive) !== null && _b !== void 0 ? _b : true }); return { success: true }; } catch (err) { if (err.code === 'EEXIST') return { success: true }; return { success: false, error: err.message }; } case 'fs:mkdirSync': try { fsSync.mkdirSync(operation.path, operation.options); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:writeFile': try { const content = operation.encoding === 'base64' ? Buffer.from(operation.content, 'base64') : operation.content; yield fs.writeFile(operation.path, content); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:appendFile': try { yield fs.appendFile(operation.path, operation.content); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:readFile': try { const data = yield fs.readFile(operation.path); return { success: true, data: data.toString('base64') }; } catch (err) { return { success: false, error: err.message }; } case 'fs:copyFile': try { yield fs.copyFile(operation.from, operation.to); if (operation.removeSource && operation.from !== operation.to) { try { yield fs.rm(operation.from); } catch (_c) { /* ignore */ } } return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:removeFile': try { yield fs.rm(operation.path, { recursive: true, force: true }); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:removeFileSync': try { fsSync.rmSync(operation.path, { recursive: true, force: true }); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:exists': try { yield fs.stat(operation.path); return { success: true, data: true }; } catch (_d) { return { success: true, data: false }; } case 'fs:existsSync': return { success: true, data: fsSync.existsSync(operation.path) }; default: // For Allure operations in local mode, we'd need to import and call the implementations // For now, fall through to error return { success: false, error: `Local execution not supported for: ${operation.type}` }; } }); } /** * Execute sync operation locally (no network) */ executeSyncLocal(operation) { const fsSync = require('fs'); switch (operation.type) { case 'fs:mkdirSync': try { fsSync.mkdirSync(operation.path, operation.options); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:removeFileSync': try { fsSync.rmSync(operation.path, { recursive: true, force: true }); return { success: true }; } catch (err) { return { success: false, error: err.message }; } case 'fs:existsSync': return { success: true, data: fsSync.existsSync(operation.path) }; default: return { success: false, error: `Sync operation not supported: ${operation.type}` }; } } /** * Execute sync operation (uses HTTP for remote, direct for local) */ executeSync(operation, retries = 2) { // Local mode - always execute locally if (this.mode === 'local') { return this.executeSyncLocal(operation); } // Remote mode but not connected - fallback to local if (!this.port || !this.isConnected) { return this.executeSyncLocal(operation); } // Try remote execution via curl with retries for (let attempt = 0; attempt < retries; attempt++) { try { const body = JSON.stringify(operation); const result = (0, child_process_1.execSync)(`curl -s --connect-timeout 2 --max-time 5 -X POST -H "Content-Type: application/json" -d '${body.replace(/'/g, "\\'")}' http://localhost:${this.port}${allure_operations_1.SERVER_PATH}`, { encoding: 'utf-8', timeout: 5000 }); const parsed = JSON.parse(result); if (parsed.success !== undefined) { return parsed; } } catch (err) { debug(`Sync request failed (attempt ${attempt + 1}/${retries}): ${err.message}`); if (attempt < retries - 1) { (0, common_1.logWithPackage)('warn', `Task server sync request failed, retrying (attempt ${attempt + 1}/${retries})...`); // Small delay before retry (sync sleep using execSync) try { (0, child_process_1.execSync)('sleep 0.1', { timeout: 200 }); } catch (_a) { // Ignore } } } } // Fallback to local on all retries failed debug('Sync retries exhausted, falling back to local execution'); (0, common_1.logWithPackage)('warn', 'Task server sync retries exhausted, using local fallback'); return this.executeSyncLocal(operation); } // ============================================================================ // Convenience Methods (matching old ReportingServer interface) // ============================================================================ mkdir(filePath, options) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'fs:mkdir', path: filePath, options }); if (!result.success) throw new Error(result.error); }); } mkdirSync(filePath, options) { const result = this.executeSync({ type: 'fs:mkdirSync', path: filePath, options }); if (!result.success) throw new Error(result.error); } writeFile(filePath, content) { return __awaiter(this, void 0, void 0, function* () { const contentStr = Buffer.isBuffer(content) ? content.toString('base64') : content; const encoding = Buffer.isBuffer(content) ? 'base64' : undefined; const result = yield this.execute({ type: 'fs:writeFile', path: filePath, content: contentStr, encoding }); if (!result.success) throw new Error(result.error); }); } appendFile(filePath, content) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'fs:appendFile', path: filePath, content }); if (!result.success) throw new Error(result.error); }); } readFile(filePath) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'fs:readFile', path: filePath }); if (!result.success) throw new Error(result.error); return Buffer.from(result.data, 'base64'); }); } copyFile(from_1, to_1) { return __awaiter(this, arguments, void 0, function* (from, to, removeSource = false) { const result = yield this.execute({ type: 'fs:copyFile', from, to, removeSource }); if (!result.success) throw new Error(result.error); }); } removeFile(filePath) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'fs:removeFile', path: filePath }); if (!result.success) throw new Error(result.error); }); } removeFileSync(filePath) { const result = this.executeSync({ type: 'fs:removeFileSync', path: filePath }); if (!result.success) throw new Error(result.error); } exists(filePath) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'fs:exists', path: filePath }); if (!result.success) throw new Error(result.error); return result.data; }); } existsSync(filePath) { const result = this.executeSync({ type: 'fs:existsSync', path: filePath }); if (!result.success) throw new Error(result.error); return result.data; } // ============================================================================ // High-level Allure Operations // ============================================================================ attachVideo(allureResults, videoPath, allureAddVideoOnPass) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'allure:attachVideo', allureResults, videoPath, allureAddVideoOnPass, }); if (!result.success) throw new Error(result.error); }); } moveToWatch(allureResults, allureResultsWatch) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'allure:moveToWatch', allureResults, allureResultsWatch, }); if (!result.success) throw new Error(result.error); }); } attachScreenshots(allureResults, screenshots, allTests) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'allure:attachScreenshots', allureResults, screenshots, allTests, }); if (!result.success) throw new Error(result.error); }); } copyScreenshot(allureResults, screenshotPath, targetName) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'allure:copyScreenshot', allureResults, screenshotPath, targetName, }); if (!result.success) throw new Error(result.error); }); } writeTestMessage(filePath, message) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.execute({ type: 'allure:writeTestMessage', path: filePath, message, }); if (!result.success) throw new Error(result.error); }); } } exports.AllureTaskClient = AllureTaskClient; // Singleton instance let clientInstance = null; const getAllureTaskClient = (mode) => { if (!clientInstance) { clientInstance = new AllureTaskClient(mode !== null && mode !== void 0 ? mode : 'remote'); } return clientInstance; }; exports.getAllureTaskClient = getAllureTaskClient; const startAllureTaskServer = (mode) => __awaiter(void 0, void 0, void 0, function* () { const client = (0, exports.getAllureTaskClient)(mode); yield client.start(); return client; }); exports.startAllureTaskServer = startAllureTaskServer; const stopAllureTaskServer = () => __awaiter(void 0, void 0, void 0, function* () { if (clientInstance) { yield clientInstance.stop(); clientInstance = null; } }); exports.stopAllureTaskServer = stopAllureTaskServer;