UNPKG

@mmisty/cypress-allure-adapter

Version:

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

358 lines (357 loc) 14.1 kB
"use strict"; 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.startReporterServer = exports.testMessages = void 0; const ws_1 = require("ws"); const net_1 = __importDefault(require("net")); const common_1 = require("../common"); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)('cypress-allure:server'); const logMessage = (0, debug_1.default)('cypress-allure:server:message'); const log = (...args) => { debug(`${args}`); }; // for testing exports.testMessages = []; const messageGot = (...args) => { logMessage(`${args}`); }; /** * Check if a port is available asynchronously */ const checkPort = (port) => { return new Promise(resolve => { const server = net_1.default.createServer(); let resolved = false; const cleanup = (result) => { if (resolved) return; resolved = true; try { server.close(); } catch (_a) { // Ignore } resolve(result); }; // Set timeout to prevent hanging const timeout = setTimeout(() => cleanup(false), 500); server.once('error', () => { clearTimeout(timeout); cleanup(false); }); server.once('listening', () => { clearTimeout(timeout); cleanup(true); }); try { server.listen(port, '127.0.0.1'); } catch (_a) { clearTimeout(timeout); cleanup(false); } }); }; function retrieveRandomPortNumber() { return __awaiter(this, void 0, void 0, function* () { const getRandomPort = () => 40000 + Math.round(Math.random() * 25000); let port = getRandomPort(); for (let i = 0; i < 30; i++) { const result = yield checkPort(port); if (result) { return port; } port = getRandomPort(); } (0, common_1.logWithPackage)('error', 'could not find free port, will not report'); return port; }); } const debugQueue = (0, debug_1.default)('cypress-allure:server:queue'); // Verbose logging that can be enabled via ALLURE_DEBUG_QUEUE env var const isVerboseQueue = () => process.env.ALLURE_DEBUG_QUEUE === 'true'; const logQueue = (...args) => { debugQueue(`${args}`); if (isVerboseQueue()) { (0, common_1.logWithPackage)('log', `[queue] ${args}`); } }; // Default timeout for queue completion after run ends (5 minutes) const DEFAULT_QUEUE_TIMEOUT_MS = 5 * 60 * 1000; /** * Get queue timeout from env variable ALLURE_QUEUE_TIMEOUT_MS (in milliseconds) * or ALLURE_QUEUE_TIMEOUT (in seconds) or use default (5 minutes) */ const getQueueTimeoutMs = () => { // Check for milliseconds env var first const timeoutMs = process.env.ALLURE_QUEUE_TIMEOUT_MS; if (timeoutMs) { const parsed = parseInt(timeoutMs, 10); if (!isNaN(parsed) && parsed > 0) { return parsed; } } // Check for seconds env var const timeoutSec = process.env.ALLURE_QUEUE_TIMEOUT; if (timeoutSec) { const parsed = parseInt(timeoutSec, 10); if (!isNaN(parsed) && parsed > 0) { return parsed * 1000; } } return DEFAULT_QUEUE_TIMEOUT_MS; }; /** * Message queue to ensure messages are processed in order */ class MessageProcessingQueue { constructor(tasks, sockserver) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.queue = []; this.isProcessing = false; this.runEnded = false; this.timeoutId = null; this.tasks = tasks; this.sockserver = sockserver; } enqueue(data) { return new Promise(resolve => { this.queue.push({ data, resolve }); logQueue(`Enqueued message, queue size: ${this.queue.length}`); this.processNext(); }); } /** * Called when the run ends (client disconnects or run end event) * Starts a timeout to force completion if tasks don't finish in time */ onRunEnded() { if (this.runEnded) { return; } this.runEnded = true; const timeoutMs = getQueueTimeoutMs(); logQueue(`Run ended, starting ${timeoutMs / 1000}s timeout for remaining tasks`); if (this.queue.length === 0 && !this.isProcessing) { logQueue('No pending tasks, queue completed'); return; } logQueue(`Pending tasks: ${this.queue.length}, processing: ${this.isProcessing}`); this.timeoutId = setTimeout(() => { if (this.queue.length > 0 || this.isProcessing) { (0, common_1.logWithPackage)('warn', `Queue timeout reached after ${timeoutMs / 1000}s. ` + `Forcing completion with ${this.queue.length} pending tasks. ` + 'Some allure results may be incomplete.'); this.forceComplete(); } }, timeoutMs); } /** * Force complete all pending tasks without processing */ forceComplete() { logQueue(`Force completing ${this.queue.length} pending tasks`); // Resolve all pending promises while (this.queue.length > 0) { const item = this.queue.shift(); if (item) { item.resolve(); } } this.isProcessing = false; this.clearTimeout(); } clearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } } processNext() { return __awaiter(this, void 0, void 0, function* () { if (this.isProcessing || this.queue.length === 0) { return; } this.isProcessing = true; const item = this.queue.shift(); if (!item) { this.isProcessing = false; return; } logQueue(`Processing message, remaining in queue: ${this.queue.length}`); try { yield this.processMessage(item.data); logQueue('Message processed successfully'); } finally { item.resolve(); this.isProcessing = false; // Check if we're done after run ended if (this.runEnded && this.queue.length === 0) { logQueue('All tasks completed after run ended'); this.clearTimeout(); } // Process next item in queue using setImmediate to avoid stack overflow // and ensure proper event loop behavior if (this.queue.length > 0) { logQueue('Scheduling next message processing'); setImmediate(() => this.processNext()); } } }); } processMessage(data) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; messageGot('message received'); messageGot(data); exports.testMessages.push(`${data}`); const parseData = (rawData) => { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any return JSON.parse(rawData.toString()); } catch (e) { return {}; } }; const requestData = parseData(data); const payload = requestData.data; logQueue(`Task: ${payload === null || payload === void 0 ? void 0 : payload.task}, arg: ${JSON.stringify(((_a = payload === null || payload === void 0 ? void 0 : payload.arg) === null || _a === void 0 ? void 0 : _a.title) || ((_b = payload === null || payload === void 0 ? void 0 : payload.arg) === null || _b === void 0 ? void 0 : _b.name) || ((_c = payload === null || payload === void 0 ? void 0 : payload.arg) === null || _c === void 0 ? void 0 : _c.message) || '')}`); if (requestData.id) { const result = yield executeTask(this.tasks, payload); this.sockserver.clients.forEach(client => { log(`sending back: ${JSON.stringify(requestData)}`); client.send(JSON.stringify({ payload, status: result ? 'done' : 'failed' })); }); } else { yield executeTask(this.tasks, payload); this.sockserver.clients.forEach(client => { log(`sending back: ${JSON.stringify(requestData)}`); client.send(JSON.stringify({ payload, status: 'done' })); }); } }); } } const socketLogic = (sockserver, tasks) => { if (!sockserver) { log('Could not start reporting server'); return; } sockserver.on('connection', ws => { log('New client connected!'); ws.send('connection established'); const messageQueue = new MessageProcessingQueue(tasks, sockserver); ws.on('close', () => { log('Client has disconnected!'); // Start timeout for remaining tasks when client disconnects messageQueue.onRunEnded(); }); ws.on('message', data => { // Enqueue message for sequential processing messageQueue.enqueue(data); }); ws.onerror = function () { (0, common_1.logWithPackage)('error', 'websocket error'); }; }); }; const startReporterServer = (configOptions_1, tasks_1, ...args_1) => __awaiter(void 0, [configOptions_1, tasks_1, ...args_1], void 0, function* (configOptions, tasks, attempt = 0) { // Guard against too many retries const MAX_ATTEMPTS = 30; if (attempt >= MAX_ATTEMPTS) { (0, common_1.logWithPackage)('error', `Could not find free port after ${MAX_ATTEMPTS} attempts, allure reporting disabled`); return undefined; } const wsPort = yield retrieveRandomPortNumber(); let sockserver; let serverStarted = false; let startupTimeout; try { sockserver = new ws_1.WebSocketServer({ port: wsPort, path: common_1.wsPath }, () => { serverStarted = true; // Clear the startup timeout since server started successfully if (startupTimeout) { clearTimeout(startupTimeout); startupTimeout = undefined; } configOptions.env[common_1.ENV_WS] = wsPort; const attemptMessage = attempt > 0 ? ` from ${attempt} attempt` : ''; (0, common_1.logWithPackage)('log', `running on ${wsPort} port${attemptMessage}`); socketLogic(sockserver, tasks); }); sockserver.on('error', err => { // Clear the startup timeout on error if (startupTimeout) { clearTimeout(startupTimeout); startupTimeout = undefined; } if (err.message.indexOf('address already in use') !== -1) { if (attempt < MAX_ATTEMPTS) { // Use setImmediate instead of process.nextTick to prevent stack overflow setImmediate(() => { (0, exports.startReporterServer)(configOptions, tasks, attempt + 1); }); } else { (0, common_1.logWithPackage)('error', `Could not find free port after ${MAX_ATTEMPTS} attempts: ${err.message}`); } return; } (0, common_1.logWithPackage)('error', `Error on ws server: ${err.message}`); }); // Set a startup timeout - if server doesn't start in 15s, log warning startupTimeout = setTimeout(() => { if (!serverStarted && sockserver) { (0, common_1.logWithPackage)('warn', 'WebSocket server startup timed out, allure reporting may be incomplete'); } }, 15000); // Unref the timeout so it doesn't keep the process alive startupTimeout.unref(); } catch (err) { (0, common_1.logWithPackage)('error', `Failed to create WebSocket server: ${err.message}`); return undefined; } return sockserver; }); exports.startReporterServer = startReporterServer; // eslint-disable-next-line @typescript-eslint/no-explicit-any const executeTask = (tasks, data) => __awaiter(void 0, void 0, void 0, function* () { if (!data || !data.task) { log(`Will not run task - not data or task field:${JSON.stringify(data)}`); return false; } try { if (Object.keys(tasks).indexOf(data.task) !== -1) { const task = data.task; // todo check log(task); yield tasks[task](data.arg); return true; } else { log(`No such task: ${data.task}`); } } catch (err) { (0, common_1.logWithPackage)('error', `Error running task: '${data.task}': ${err.message}`); // eslint-disable-next-line no-console console.log(err.stack); } return false; });