@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
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.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;
});