UNPKG

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