js-tests-results-collector
Version:
Universal test results collector for Jest, Jasmine, Mocha, Cypress, and Playwright that sends results to Buddy Works API
251 lines (216 loc) • 7.98 kB
JavaScript
const ApiClient = require('./api-client');
const Config = require('./config');
const Logger = require('../utils/logger');
const fs = require('fs');
const path = require('path');
const os = require('os');
class SessionManager {
constructor() {
this.logger = new Logger('SessionManager');
this.sessionId = null;
this.config = null;
this.apiClient = null;
this.initialized = false;
this.sessionCreationPromise = null; // Add mutex for session creation
this.hasFrameworkErrors = false; // Track if actual framework errors occurred
this.hasErrorTests = false; // Track if any tests had ERROR status
this.hasFailedTests = false; // Track if any tests had FAILED status
}
async initialize() {
if (this.initialized) {
return;
}
try {
this.config = new Config();
this.apiClient = new ApiClient(this.config);
this.initialized = true;
this.logger.debug('SessionManager initialized');
} catch (error) {
this.logger.error('Failed to initialize SessionManager', error);
throw error;
}
}
async createSession() {
if (!this.initialized) {
await this.initialize();
}
// If we already have a session, return it
if (this.sessionId) {
this.logger.debug('Session already exists, returning existing session ID');
return this.sessionId;
}
// Use the same session creation logic as getOrCreateSession
return await this.getOrCreateSession();
}
async getOrCreateSession() {
if (!this.initialized) {
await this.initialize();
}
// If we already have a session, return it
if (this.sessionId) {
return this.sessionId;
}
// If session creation is already in progress, wait for it
if (this.sessionCreationPromise) {
this.logger.debug('Session creation already in progress, waiting...');
await this.sessionCreationPromise;
return this.sessionId;
}
// Create the session creation promise and store it
this.sessionCreationPromise = this.createSessionInternal();
try {
await this.sessionCreationPromise;
return this.sessionId;
} finally {
// Clear the promise after completion
this.sessionCreationPromise = null;
}
}
async createSessionInternal() {
// Double-check that we still need to create a session
if (this.sessionId) {
return this.sessionId;
}
try {
// Check if we have an existing session ID to reopen
if (this.config.hasExistingSession()) {
this.logger.info('Reopening existing session');
this.sessionId = await this.apiClient.reopenSession(this.config.getExistingSessionId());
} else {
this.logger.info('Starting new session');
this.sessionId = await this.apiClient.createSession();
}
// Store session ID in environment variable for other processes to access
process.env.BUDDY_SESSION_ID = this.sessionId;
this.logger.debug(`Session ID stored in environment: ${this.sessionId}`);
// Also write to file for cross-process communication (e.g., Cypress after:run hook)
this.writeSessionToFile(this.sessionId);
} catch (error) {
this.logger.error('Failed to create/reopen session', error);
throw error;
}
return this.sessionId;
}
async submitTestCase(testCase) {
if (!this.initialized) {
await this.initialize();
}
try {
const sessionId = await this.getOrCreateSession();
// Track test result statuses to determine final session status
if (testCase.status === 'ERROR') {
this.hasErrorTests = true;
this.logger.debug('Tracked ERROR test result');
} else if (testCase.status === 'FAILED') {
this.hasFailedTests = true;
this.logger.debug('Tracked FAILED test result');
}
await this.apiClient.submitTestCase(sessionId, testCase);
} catch (error) {
this.logger.error('Failed to submit test case', error);
// Mark that we had a framework error (API/network issue, not just a failed test)
this.hasFrameworkErrors = true;
// Don't throw - we don't want to break the test runner
}
}
async closeSession() {
if (!this.initialized) {
await this.initialize();
}
// If we don't have a session ID in memory, try to get it from environment
if (!this.sessionId && process.env.BUDDY_SESSION_ID) {
this.sessionId = process.env.BUDDY_SESSION_ID;
this.logger.debug(`Retrieved session ID from environment: ${this.sessionId}`);
}
// If still no session ID, try to read from file
if (!this.sessionId) {
this.sessionId = this.readSessionFromFile();
if (this.sessionId) {
this.logger.debug(`Retrieved session ID from file: ${this.sessionId}`);
}
}
if (this.sessionId) {
try {
// Determine the session status based on test results and framework errors
// Priority: ERROR > FAILED > SUCCESS
let sessionStatus = 'SUCCESS';
if (this.hasFrameworkErrors || this.hasErrorTests) {
sessionStatus = 'ERROR';
} else if (this.hasFailedTests) {
sessionStatus = 'FAILED';
}
this.logger.debug(`Closing session with status: ${sessionStatus}`, {
frameworkErrors: this.hasFrameworkErrors,
errorTests: this.hasErrorTests,
failedTests: this.hasFailedTests
});
await this.apiClient.closeSession(this.sessionId, sessionStatus);
this.logger.info(`Session ${this.sessionId} closed successfully with status: ${sessionStatus}`);
} catch (error) {
this.logger.error('Failed to close session', error);
// Don't throw - we don't want to break the test runner
} finally {
// Reset session ID and tracking flags regardless of success/failure
this.sessionId = null;
this.sessionCreationPromise = null;
this.hasFrameworkErrors = false;
this.hasErrorTests = false;
this.hasFailedTests = false;
// Clear the environment variable
delete process.env.BUDDY_SESSION_ID;
this.logger.debug('Session ID and tracking flags cleared');
// Clear the session file
this.clearSessionFile();
}
} else {
this.logger.debug('No active session to close');
}
}
// Method to mark that a framework error occurred (not just a failed test)
markFrameworkError() {
this.hasFrameworkErrors = true;
}
// Method to get the current session ID (useful for debugging)
getCurrentSessionId() {
return this.sessionId;
}
// Helper methods for file-based session persistence
getSessionFilePath() {
return path.join(os.tmpdir(), 'buddy-session-id.txt');
}
writeSessionToFile(sessionId) {
try {
fs.writeFileSync(this.getSessionFilePath(), sessionId, 'utf8');
this.logger.debug(`Session ID written to file: ${sessionId}`);
} catch (error) {
this.logger.error('Failed to write session ID to file', error);
}
}
readSessionFromFile() {
try {
const filePath = this.getSessionFilePath();
if (fs.existsSync(filePath)) {
const sessionId = fs.readFileSync(filePath, 'utf8').trim();
this.logger.debug(`Session ID read from file: ${sessionId}`);
return sessionId;
}
} catch (error) {
this.logger.error('Failed to read session ID from file', error);
}
return null;
}
clearSessionFile() {
try {
const filePath = this.getSessionFilePath();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
this.logger.debug('Session file cleared');
}
} catch (error) {
this.logger.error('Failed to clear session file', error);
}
}
}
// Create a single instance to be shared across all test reporters
const sessionManager = new SessionManager();
module.exports = sessionManager;