UNPKG

@mmisty/cypress-allure-adapter

Version:

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

273 lines (272 loc) 11.3 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.TaskManager = void 0; const debug_1 = __importDefault(require("debug")); const common_1 = require("../common"); const debug = (0, debug_1.default)('cypress-allure:task-manager'); class Semaphore { constructor(max, acquireTimeoutMs = 5000) { this.max = max; this.waiters = []; this.count = max; this.acquireTimeout = acquireTimeoutMs; } acquire() { return __awaiter(this, void 0, void 0, function* () { if (this.count > 0) { this.count--; return true; } // Wait with timeout to prevent indefinite blocking return new Promise(resolve => { const timeoutId = setTimeout(() => { // Remove this waiter from the queue const index = this.waiters.findIndex(w => w.timeoutId === timeoutId); if (index !== -1) { this.waiters.splice(index, 1); } debug('Semaphore acquire timed out, continuing without blocking'); // Don't log warning - just proceed (reduced timeout makes this expected) resolve(false); // Return false to indicate timeout }, this.acquireTimeout); // Unref the timeout so it doesn't keep Node alive if (timeoutId.unref) { timeoutId.unref(); } this.waiters.push({ resolve: () => { clearTimeout(timeoutId); resolve(true); }, timeoutId, }); }); }); } release() { this.count++; const next = this.waiters.shift(); if (next) { this.count--; clearTimeout(next.timeoutId); next.resolve(); } } } /** * Task Manager * * Queues and executes tasks. Supports both: * - Legacy function-based tasks (executed locally) * - Serializable operations (sent to remote server via client) */ class TaskManager { constructor(options) { var _a; this.entityQueues = new Map(); this.client = null; this.options = options !== null && options !== void 0 ? options : {}; // Increased from 5 to 10 - tasks are I/O bound so more parallelism is beneficial const maxParallel = (_a = this.options.maxParallel) !== null && _a !== void 0 ? _a : 10; this.semaphore = new Semaphore(maxParallel); } /** * Set the client for remote task execution */ setClient(client) { this.client = client; } /** * Schedule queue processing on next tick (deferred to not block current execution) */ scheduleProcessQueue(entityId) { const queue = this.entityQueues.get(entityId); if (queue && !queue.isFlushing) { // Use setImmediate to defer processing to next event loop tick // This allows other events (like browser connection) to be processed first setImmediate(() => { this.processQueue(entityId).catch(err => { (0, common_1.logWithPackage)('error', `Entity worker crashed ${entityId}: ${err.message}`); }); }); } } /** * Add a legacy function-based task * @deprecated Use addOperation for new code */ addTask(entityId, task) { if (!entityId) { (0, common_1.logWithPackage)('error', 'Cannot start task without entityId set'); return; } let queue = this.entityQueues.get(entityId); if (!queue) { queue = { tasks: [], isFlushing: false }; this.entityQueues.set(entityId, queue); } queue.tasks.push(task); debug(`Task added for entity "${entityId}", queue length: ${queue.tasks.length}`); this.scheduleProcessQueue(entityId); } /** * Add a serializable operation to be executed on the server */ addOperation(entityId, operation) { if (!entityId) { (0, common_1.logWithPackage)('error', 'Cannot add operation without entityId set'); return; } let queue = this.entityQueues.get(entityId); if (!queue) { queue = { tasks: [], isFlushing: false }; this.entityQueues.set(entityId, queue); } queue.tasks.push(operation); debug(`Operation added for entity "${entityId}", queue length: ${queue.tasks.length}`); this.scheduleProcessQueue(entityId); } processQueue(entityId) { return __awaiter(this, void 0, void 0, function* () { const queue = this.entityQueues.get(entityId); if (!queue) { (0, common_1.logWithPackage)('warn', `Tasks for ${entityId} not found`); return; } if (queue.isFlushing) return; queue.isFlushing = true; try { while (queue.tasks.length > 0) { const task = queue.tasks.shift(); if (!task) continue; yield this.semaphore.acquire(); try { yield this.runWithTimeout(task, entityId); } finally { this.semaphore.release(); } // Yield to event loop after each task to allow other events to be processed // This prevents blocking Cypress browser connection events yield new Promise(resolve => setImmediate(resolve)); } } catch (err) { (0, common_1.logWithPackage)('error', `Queue error for ${entityId}: ${err.message}`); } finally { queue.isFlushing = false; } }); } runWithTimeout(task, entityId) { return __awaiter(this, void 0, void 0, function* () { var _a; const TASK_TIMEOUT = (_a = this.options.taskTimeout) !== null && _a !== void 0 ? _a : 30000; let timeoutId = undefined; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { (0, common_1.logWithPackage)('error', `Task for ${entityId} timed out`); reject(new Error('task timeout')); }, TASK_TIMEOUT); }); try { if (typeof task === 'function') { // Legacy function-based task yield Promise.race([task(), timeoutPromise]); } else { // Serializable operation - execute via client if (this.client) { yield Promise.race([this.client.execute(task), timeoutPromise]); } else { (0, common_1.logWithPackage)('error', 'No client set for operation execution'); } } } catch (err) { (0, common_1.logWithPackage)('error', `Task failed for ${entityId}: ${err.message}`); } finally { clearTimeout(timeoutId); } }); } flushAllTasks() { return __awaiter(this, void 0, void 0, function* () { var _a; debug('Flushing all entity queues'); const start = Date.now(); const timeout = (_a = this.options.overallTimeout) !== null && _a !== void 0 ? _a : 120000; const getRunningInfo = () => { let totalTasks = 0; let flushingQueues = 0; for (const q of this.entityQueues.values()) { totalTasks += q.tasks.length; if (q.isFlushing) flushingQueues++; } return { totalTasks, flushingQueues, hasRunning: totalTasks > 0 || flushingQueues > 0 }; }; let info = getRunningInfo(); // Log initial state if (info.hasRunning) { debug(`Waiting for ${info.totalTasks} tasks in ${info.flushingQueues} queues`); } // Use longer polling interval to reduce event loop churn (was 50ms) const POLL_INTERVAL = 200; while (info.hasRunning) { yield new Promise(r => setTimeout(r, POLL_INTERVAL)); info = getRunningInfo(); if (Date.now() - start > timeout) { (0, common_1.logWithPackage)('error', `flushAllTasks exceeded ${timeout / 1000}s, exiting (${info.totalTasks} tasks remaining)`); break; } } debug(`All tasks flushed in ${Date.now() - start}ms`); }); } flushAllTasksForQueue(entityId) { return __awaiter(this, void 0, void 0, function* () { var _a; debug(`Flushing all tasks for queue ${entityId}`); const start = Date.now(); const queue = this.entityQueues.get(entityId); const timeout = (_a = this.options.overallTimeout) !== null && _a !== void 0 ? _a : 120000; if (!queue) { debug(`Tasks for ${entityId} not found (queue may not exist yet)`); return; } const initialTasks = queue.tasks.length; if (initialTasks > 0) { debug(`Waiting for ${initialTasks} tasks in queue ${entityId}`); } // Use longer polling interval to reduce event loop churn (was 50ms) const POLL_INTERVAL = 200; while (queue.tasks.length > 0 || queue.isFlushing) { yield new Promise(r => setTimeout(r, POLL_INTERVAL)); if (Date.now() - start > timeout) { (0, common_1.logWithPackage)('error', `flushAllTasksForQueue exceeded ${timeout / 1000}s, exiting (${queue.tasks.length} tasks remaining)`); break; } } debug(`Queue ${entityId} flushed in ${Date.now() - start}ms`); }); } } exports.TaskManager = TaskManager;