import v8 from 'node:v8'; import Emittery from 'emittery'; const copyStats = stats => v8.deserialize(v8.serialize(stats)); export default class RunStatus extends Emittery { constructor(files, parallelRuns, selectionInsights) { super(); this.pendingTests = new Map(); this.pendingTestsLogs = new Map(); this.emptyParallelRun = parallelRuns && parallelRuns.currentFileCount === 0 && parallelRuns.totalRuns > 1 && files > 0; this.selectionInsights = selectionInsights; this.stats = { byFile: new Map(), declaredTests: 0, failedHooks: 0, failedTests: 0, failedWorkers: 0, files, parallelRuns, finishedWorkers: 0, internalErrors: 0, remainingTests: 0, passedKnownFailingTests: 0, passedTests: 0, selectedTests: 0, sharedWorkerErrors: 0, skippedTests: 0, timedOutTests: 0, timeouts: 0, todoTests: 0, uncaughtExceptions: 0, unexpectedProcessExits: 0, unhandledRejections: 0, }; } observeWorker(worker, testFile, stats) { this.stats.byFile.set(testFile, { declaredTests: 0, failedHooks: 0, failedTests: 0, internalErrors: 0, remainingTests: 0, passedKnownFailingTests: 0, passedTests: 0, selectedTests: 0, selectingLines: false, skippedTests: 0, todoTests: 0, uncaughtExceptions: 0, unexpectedProcessExits: 0, unhandledRejections: 0, ...stats, }); this.pendingTests.set(testFile, new Set()); this.pendingTestsLogs.set(testFile, new Map()); worker.onStateChange(data => this.emitStateChange(data)); } // eslint-disable-next-line complexity emitStateChange(event) { const {stats} = this; const fileStats = stats.byFile.get(event.testFile); let changedStats = true; switch (event.type) { case 'declared-test': { stats.declaredTests++; fileStats.declaredTests++; break; } case 'hook-failed': { stats.failedHooks++; fileStats.failedHooks++; break; } case 'internal-error': { stats.internalErrors++; if (event.testFile) { fileStats.internalErrors++; } break; } case 'selected-test': { stats.selectedTests++; fileStats.selectedTests++; if (event.skip) { stats.skippedTests++; fileStats.skippedTests++; } else if (event.todo) { stats.todoTests++; fileStats.todoTests++; } else { stats.remainingTests++; fileStats.remainingTests++; this.addPendingTest(event); } break; } case 'shared-worker-error': { stats.sharedWorkerErrors++; break; } case 'test-failed': { stats.failedTests++; fileStats.failedTests++; stats.remainingTests--; fileStats.remainingTests--; this.removePendingTest(event); break; } case 'test-passed': { if (event.knownFailing) { stats.passedKnownFailingTests++; fileStats.passedKnownFailingTests++; } else { stats.passedTests++; fileStats.passedTests++; } stats.remainingTests--; fileStats.remainingTests--; this.removePendingTest(event); break; } case 'test-register-log-reference': { this.addPendingTestLogs(event); break; } case 'timeout': { stats.timeouts++; event.pendingTests = this.pendingTests; event.pendingTestsLogs = this.pendingTestsLogs; this.pendingTests = new Map(); this.pendingTestsLogs = new Map(); for (const testsInFile of event.pendingTests.values()) { stats.timedOutTests += testsInFile.size; } break; } case 'interrupt': { event.pendingTests = this.pendingTests; event.pendingTestsLogs = this.pendingTestsLogs; this.pendingTests = new Map(); this.pendingTestsLogs = new Map(); break; } case 'process-exit': { stats.unexpectedProcessExits++; fileStats.unexpectedProcessExits++; event.pendingTests = this.pendingTests; event.pendingTestsLogs = this.pendingTestsLogs; this.pendingTests = new Map(); this.pendingTestsLogs = new Map(); break; } case 'uncaught-exception': { stats.uncaughtExceptions++; fileStats.uncaughtExceptions++; break; } case 'unhandled-rejection': { stats.unhandledRejections++; fileStats.unhandledRejections++; break; } case 'worker-failed': { stats.failedWorkers++; break; } case 'worker-finished': { stats.finishedWorkers++; if (this.pendingTests.get(event.testFile)?.size === 0) { this.pendingTests.delete(event.testFile); } break; } default: { changedStats = false; break; } } if (changedStats) { this.emit('stateChange', {type: 'stats', stats: copyStats(stats)}); } this.emit('stateChange', event); } end() { this.emitStateChange({type: 'end'}); return this; } suggestExitCode(circumstances) { if (this.emptyParallelRun) { return 0; } if (circumstances.matching && this.stats.selectedTests === 0) { return 1; } if ( this.stats.declaredTests === 0 || this.stats.internalErrors > 0 || this.stats.failedHooks > 0 || this.stats.failedTests > 0 || this.stats.failedWorkers > 0 || this.stats.remainingTests > 0 || this.stats.sharedWorkerErrors > 0 || this.stats.timeouts > 0 || this.stats.uncaughtExceptions > 0 || this.stats.unexpectedProcessExits > 0 || this.stats.unhandledRejections > 0 ) { return 1; } if ([...this.stats.byFile.values()].some(stats => stats.selectingLines && stats.selectedTests === 0)) { return 1; } return 0; } addPendingTestLogs(event) { this.pendingTestsLogs.get(event.testFile)?.set(event.title, event.logs); } addPendingTest(event) { if (this.pendingTests.has(event.testFile)) { this.pendingTests.get(event.testFile).add(event.title); } } removePendingTest(event) { if (this.pendingTests.has(event.testFile)) { this.pendingTests.get(event.testFile).delete(event.title); } } getFailedTestFiles() { return [...this.stats.byFile].filter(statByFile => statByFile[1].failedTests).map(statByFile => statByFile[0]); } }