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