UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

372 lines (311 loc) 13.9 kB
const tty = require('tty') if (!tty.getWindowSize) { // this is really old method, long removed from Node, but Mocha // reporters fall back on it if they cannot use `process.stdout.getWindowSize` // we need to polyfill it. tty.getWindowSize = () => [40, 80] } const { parentPort, workerData } = require('worker_threads') const event = require('../../event') const container = require('../../container') const { getConfig } = require('../utils') const { tryOrDefault, deepMerge } = require('../../utils') let stdout = '' const stderr = '' // Requiring of Codecept need to be after tty.getWindowSize is available. const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept') const { options, tests, testRoot, workerIndex, poolMode } = workerData // hide worker output // In pool mode, only suppress output if debug is NOT enabled // In regular mode, hide result output but allow step output in verbose/debug if (poolMode && !options.debug) { // In pool mode without debug, suppress only result summaries and failures, but allow Scenario Steps const originalWrite = process.stdout.write process.stdout.write = string => { // Always allow Scenario Steps output (including the circle symbol) if (string.includes('Scenario Steps:') || string.includes('◯ Scenario Steps:')) { return originalWrite.call(process.stdout, string) } if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:')) { return true } return originalWrite.call(process.stdout, string) } } else if (!poolMode && !options.debug && !options.verbose) { process.stdout.write = string => { stdout += string return true } } else { // In verbose/debug mode for test/suite modes, show step details // but suppress individual worker result summaries to avoid duplicate output const originalWrite = process.stdout.write const originalConsoleLog = console.log process.stdout.write = string => { // Suppress individual worker result summaries and failure reports if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:') || string.includes('◯ Scenario Steps:')) { return true } return originalWrite.call(process.stdout, string) } // Override console.log to catch result summaries console.log = (...args) => { const fullMessage = args.join(' ') if (fullMessage.includes(' FAIL |') || fullMessage.includes(' OK |') || fullMessage.includes('-- FAILURES:')) { return } return originalConsoleLog.apply(console, args) } } const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {}) // important deep merge so dynamic things e.g. functions on config are not overridden const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs) // Load test and run const codecept = new Codecept(config, options) codecept.init(testRoot) codecept.loadTests() const mocha = container.mocha() if (poolMode) { // In pool mode, don't filter tests upfront - wait for assignments // We'll reload test files fresh for each test request } else { // Legacy mode - filter tests upfront filterTests() } // run tests ;(async function () { if (poolMode) { await runPoolTests() } else if (mocha.suite.total()) { await runTests() } })() let globalStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 } async function runTests() { try { await codecept.bootstrap() } catch (err) { throw new Error(`Error while running bootstrap file :${err}`) } listenToParentThread() initializeListeners() disablePause() try { await codecept.run() } finally { await codecept.teardown() } } async function runPoolTests() { try { await codecept.bootstrap() } catch (err) { throw new Error(`Error while running bootstrap file :${err}`) } initializeListeners() disablePause() // Accumulate results across all tests in pool mode let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 } let allTests = [] let allFailures = [] let previousStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 } // Keep requesting tests until no more available while (true) { // Request a test assignment sendToParentThread({ type: 'REQUEST_TEST', workerIndex }) const testResult = await new Promise((resolve, reject) => { // Set up pool mode message handler const messageHandler = async eventData => { if (eventData.type === 'TEST_ASSIGNED') { const testUid = eventData.test try { // In pool mode, we need to create a fresh Mocha instance for each test // because Mocha instances become disposed after running tests container.createMocha() // Create fresh Mocha instance filterTestById(testUid) const mocha = container.mocha() if (mocha.suite.total() > 0) { // Run the test and complete await codecept.run() // Get the results from this specific test run const result = container.result() const currentStats = result.stats || {} // Calculate the difference from previous accumulated stats const newPasses = Math.max(0, (currentStats.passes || 0) - previousStats.passes) const newFailures = Math.max(0, (currentStats.failures || 0) - previousStats.failures) const newTests = Math.max(0, (currentStats.tests || 0) - previousStats.tests) const newPending = Math.max(0, (currentStats.pending || 0) - previousStats.pending) const newFailedHooks = Math.max(0, (currentStats.failedHooks || 0) - previousStats.failedHooks) // Add only the new results consolidatedStats.passes += newPasses consolidatedStats.failures += newFailures consolidatedStats.tests += newTests consolidatedStats.pending += newPending consolidatedStats.failedHooks += newFailedHooks // Update previous stats for next comparison previousStats = { ...currentStats } // Add new failures to consolidated collections if (result.failures && result.failures.length > allFailures.length) { const newFailures = result.failures.slice(allFailures.length) allFailures.push(...newFailures) } } // Signal test completed and request next parentPort?.off('message', messageHandler) resolve('TEST_COMPLETED') } catch (err) { parentPort?.off('message', messageHandler) reject(err) } } else if (eventData.type === 'NO_MORE_TESTS') { // No tests available, exit worker parentPort?.off('message', messageHandler) resolve('NO_MORE_TESTS') } else { // Handle other message types (support messages, etc.) container.append({ support: eventData.data }) } } parentPort?.on('message', messageHandler) }) // Exit if no more tests if (testResult === 'NO_MORE_TESTS') { break } } try { await codecept.teardown() } catch (err) { // Log teardown errors but don't fail console.error('Teardown error:', err) } // Send final consolidated results for the entire worker const finalResult = { hasFailed: consolidatedStats.failures > 0, stats: consolidatedStats, duration: 0, // Pool mode doesn't track duration per worker tests: [], // Keep tests empty to avoid serialization issues - stats are sufficient failures: allFailures, // Include all failures for error reporting } sendToParentThread({ event: event.all.after, workerIndex, data: finalResult }) sendToParentThread({ event: event.all.result, workerIndex, data: finalResult }) // Add longer delay to ensure messages are delivered before closing await new Promise(resolve => setTimeout(resolve, 100)) // Close worker thread when pool mode is complete parentPort?.close() } function filterTestById(testUid) { // Reload test files fresh for each test in pool mode const files = codecept.testFiles // Get the existing mocha instance const mocha = container.mocha() // Clear suites and tests but preserve other mocha settings mocha.suite.suites = [] mocha.suite.tests = [] // Clear require cache for test files to ensure fresh loading files.forEach(file => { delete require.cache[require.resolve(file)] }) // Set files and load them mocha.files = files mocha.loadFiles() // Now filter to only the target test - use a more robust approach let foundTest = false for (const suite of mocha.suite.suites) { const originalTests = [...suite.tests] suite.tests = [] for (const test of originalTests) { if (test.uid === testUid) { suite.tests.push(test) foundTest = true break // Only add one matching test } } // If no tests found in this suite, remove it if (suite.tests.length === 0) { suite.parent.suites = suite.parent.suites.filter(s => s !== suite) } } // Filter out empty suites from the root mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0) if (!foundTest) { // If testUid doesn't match, maybe it's a simple test name - try fallback mocha.suite.suites = [] mocha.suite.tests = [] mocha.loadFiles() // Try matching by title for (const suite of mocha.suite.suites) { const originalTests = [...suite.tests] suite.tests = [] for (const test of originalTests) { if (test.title === testUid || test.fullTitle() === testUid || test.uid === testUid) { suite.tests.push(test) foundTest = true break } } } // Clean up empty suites again mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0) } } function filterTests() { const files = codecept.testFiles mocha.files = files mocha.loadFiles() for (const suite of mocha.suite.suites) { suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0) } } function initializeListeners() { // suite event.dispatcher.on(event.suite.before, suite => sendToParentThread({ event: event.suite.before, workerIndex, data: suite.simplify() })) event.dispatcher.on(event.suite.after, suite => sendToParentThread({ event: event.suite.after, workerIndex, data: suite.simplify() })) // calculate duration event.dispatcher.on(event.test.started, test => (test.start = new Date())) // tests event.dispatcher.on(event.test.before, test => sendToParentThread({ event: event.test.before, workerIndex, data: test.simplify() })) event.dispatcher.on(event.test.after, test => sendToParentThread({ event: event.test.after, workerIndex, data: test.simplify() })) // we should force-send correct errors to prevent race condition event.dispatcher.on(event.test.finished, (test, err) => sendToParentThread({ event: event.test.finished, workerIndex, data: { ...test.simplify(), err } })) event.dispatcher.on(event.test.failed, (test, err) => sendToParentThread({ event: event.test.failed, workerIndex, data: { ...test.simplify(), err } })) event.dispatcher.on(event.test.passed, (test, err) => sendToParentThread({ event: event.test.passed, workerIndex, data: { ...test.simplify(), err } })) event.dispatcher.on(event.test.started, test => sendToParentThread({ event: event.test.started, workerIndex, data: test.simplify() })) event.dispatcher.on(event.test.skipped, test => sendToParentThread({ event: event.test.skipped, workerIndex, data: test.simplify() })) // steps event.dispatcher.on(event.step.finished, step => sendToParentThread({ event: event.step.finished, workerIndex, data: step.simplify() })) event.dispatcher.on(event.step.started, step => sendToParentThread({ event: event.step.started, workerIndex, data: step.simplify() })) event.dispatcher.on(event.step.passed, step => sendToParentThread({ event: event.step.passed, workerIndex, data: step.simplify() })) event.dispatcher.on(event.step.failed, step => sendToParentThread({ event: event.step.failed, workerIndex, data: step.simplify() })) event.dispatcher.on(event.hook.failed, (hook, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err } })) event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() })) event.dispatcher.on(event.hook.finished, hook => sendToParentThread({ event: event.hook.finished, workerIndex, data: hook.simplify() })) if (!poolMode) { // In regular mode, close worker after all tests are complete event.dispatcher.once(event.all.after, () => { sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() }) }) // all event.dispatcher.once(event.all.result, () => { sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() }) parentPort?.close() }) } else { // In pool mode, don't send result events for individual tests // Results will be sent once when the worker completes all tests } } function disablePause() { global.pause = () => {} } function sendToParentThread(data) { parentPort?.postMessage(data) } function listenToParentThread() { if (!poolMode) { parentPort?.on('message', eventData => { container.append({ support: eventData.data }) }) } // In pool mode, message handling is done in runPoolTests() }