Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions lib/command/workers/runTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads'

// Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
// These will be imported dynamically when needed
let event, container, Codecept, getConfig, tryOrDefault, deepMerge
let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack

let stdout = ''

Expand All @@ -21,10 +21,17 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData

// Global error handlers to catch critical errors but not test failures
process.on('uncaughtException', (err) => {
if (global.container?.tsFileMapping && fixErrorStack) {
const fileMapping = global.container.tsFileMapping()
if (fileMapping) {
fixErrorStack(err, fileMapping)
}
}

// Log to stderr to bypass stdout suppression
process.stderr.write(`[Worker ${workerIndex}] UNCAUGHT EXCEPTION: ${err.message}\n`)
process.stderr.write(`${err.stack}\n`)

// Don't exit on test assertion errors - those are handled by mocha
if (err.name === 'AssertionError' || err.message?.includes('expected')) {
return
Expand All @@ -33,13 +40,20 @@ process.on('uncaughtException', (err) => {
})

process.on('unhandledRejection', (reason, promise) => {
if (reason && typeof reason === 'object' && reason.stack && global.container?.tsFileMapping && fixErrorStack) {
const fileMapping = global.container.tsFileMapping()
if (fileMapping) {
fixErrorStack(reason, fileMapping)
}
}

// Log to stderr to bypass stdout suppression
const msg = reason?.message || String(reason)
process.stderr.write(`[Worker ${workerIndex}] UNHANDLED REJECTION: ${msg}\n`)
if (reason?.stack) {
process.stderr.write(`${reason.stack}\n`)
}

// Don't exit on test-related rejections
if (msg.includes('expected') || msg.includes('AssertionError')) {
return
Expand Down Expand Up @@ -132,13 +146,15 @@ initPromise = (async function () {
const utilsModule = await import('../utils.js')
const coreUtilsModule = await import('../../utils.js')
const CodeceptModule = await import('../../codecept.js')

const typescriptModule = await import('../../utils/typescript.js')

event = eventModule.default
container = containerModule.default
getConfig = utilsModule.getConfig
tryOrDefault = coreUtilsModule.tryOrDefault
deepMerge = coreUtilsModule.deepMerge
Codecept = CodeceptModule.default
fixErrorStack = typescriptModule.fixErrorStack

const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})

Expand All @@ -147,6 +163,12 @@ initPromise = (async function () {
// IMPORTANT: await is required here since getConfig is async
baseConfig = await getConfig(options.config || testRoot)
} catch (configErr) {
if (global.container?.tsFileMapping && fixErrorStack) {
const fileMapping = global.container.tsFileMapping()
if (fileMapping) {
fixErrorStack(configErr, fileMapping)
}
}
process.stderr.write(`[Worker ${workerIndex}] FAILED loading config: ${configErr.message}\n`)
process.stderr.write(`${configErr.stack}\n`)
await new Promise(resolve => setTimeout(resolve, 100))
Expand All @@ -163,6 +185,12 @@ initPromise = (async function () {
try {
await codecept.init(testRoot)
} catch (initErr) {
if (global.container?.tsFileMapping && fixErrorStack) {
const fileMapping = global.container.tsFileMapping()
if (fileMapping) {
fixErrorStack(initErr, fileMapping)
}
}
process.stderr.write(`[Worker ${workerIndex}] FAILED during codecept.init(): ${initErr.message}\n`)
process.stderr.write(`${initErr.stack}\n`)
process.exit(1)
Expand Down Expand Up @@ -190,6 +218,12 @@ initPromise = (async function () {
parentPort?.close()
}
} catch (err) {
if (global.container?.tsFileMapping && fixErrorStack) {
const fileMapping = global.container.tsFileMapping()
if (fileMapping) {
fixErrorStack(err, fileMapping)
}
}
process.stderr.write(`[Worker ${workerIndex}] FATAL ERROR: ${err.message}\n`)
process.stderr.write(`${err.stack}\n`)
process.exit(1)
Expand Down
6 changes: 6 additions & 0 deletions lib/mocha/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import event from '../event.js'
import AssertionFailedError from '../assert/error.js'
import output from '../output.js'
import test, { cloneTest } from './test.js'
import { fixErrorStack } from '../utils/typescript.js'

// Get version from package.json to avoid circular dependency
const __filename = fileURLToPath(import.meta.url)
Expand Down Expand Up @@ -264,6 +265,11 @@ class Cli extends Base {
}

try {
const fileMapping = global.container?.tsFileMapping?.()
if (fileMapping) {
fixErrorStack(err, fileMapping)
}

let stack = err.stack
stack = (stack || '').replace(originalMessage, '')
stack = stack ? stack.split('\n') : []
Expand Down
5 changes: 5 additions & 0 deletions lib/mocha/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import gherkinParser, { loadTranslations } from './gherkin.js'
import output from '../output.js'
import scenarioUiFunction from './ui.js'
import { initMochaGlobals } from '../globals.js'
import { fixErrorStack } from '../utils/typescript.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = fsPath.dirname(__filename)
Expand Down Expand Up @@ -34,6 +35,10 @@ class MochaFactory {
// Handle ECONNREFUSED without dynamic import for now
err = new Error('Connection refused: ' + err.toString())
}
const fileMapping = global.container?.tsFileMapping?.()
if (fileMapping) {
fixErrorStack(err, fileMapping)
}
output.error(err)
output.print(err.stack)
process.exit(1)
Expand Down