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