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
34 changes: 31 additions & 3 deletions lib/command/workers/runTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,36 @@ initPromise = (async function () {
await runPoolTests()
} else if (mocha.suite.total()) {
await runTests()
await new Promise(resolve => setTimeout(resolve, 100))
// Remove message listener before closing to prevent event loop hanging
if (global.parentMessageHandler) {
try {
parentPort?.off('message', global.parentMessageHandler)
} catch (err) {
// Ignore errors when removing listener
}
}
try {
parentPort?.close()
} catch (err) {
// Ignore errors when closing port
}
} else {
// No tests to run, close the worker
console.error(`[Worker ${workerIndex}] ERROR: No tests found after filtering! Assigned ${tests.length} UIDs but none matched.`)
parentPort?.close()
// Remove message listener before closing to prevent event loop hanging
if (global.parentMessageHandler) {
try {
parentPort?.off('message', global.parentMessageHandler)
} catch (err) {
// Ignore errors when removing listener
}
}
try {
parentPort?.close()
} catch (err) {
// Ignore errors when closing port
}
}
} catch (err) {
if (global.container?.tsFileMapping && fixErrorStack) {
Expand Down Expand Up @@ -558,9 +584,11 @@ function sendToParentThread(data) {

function listenToParentThread() {
if (!poolMode) {
parentPort?.on('message', eventData => {
const messageHandler = eventData => {
container.append({ support: eventData.data })
})
}
parentPort?.on('message', messageHandler)
global.parentMessageHandler = messageHandler
}
// In pool mode, message handling is done in runPoolTests()
}
8 changes: 8 additions & 0 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,14 @@ async function createPlugins(config, options = {}) {

// Use async loading for all plugins (ESM and CJS)
plugins[pluginName] = await loadPluginAsync(module, config[pluginName])

// Skip loading plugin in parent process if runInParent is false
if (config[pluginName].runInParent === false && process.env.RUNS_WITH_WORKERS) {
delete plugins[pluginName]
debug(`plugin ${pluginName} skipped in parent process (runInParent: false)`)
continue
}

debug(`plugin ${pluginName} loaded via async import`)
} catch (err) {
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
Expand Down
11 changes: 6 additions & 5 deletions lib/listener/globalTimeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import debugModule from 'debug'
const debug = debugModule('codeceptjs:timeout')
import { TIMEOUT_ORDER, TimeoutError, TestTimeoutError, StepTimeoutError } from '../timeout.js'
import { BeforeSuiteHook, AfterSuiteHook } from '../mocha/hooks.js'
import { extractStepCode } from '../step.js'

export default function () {
let timeout
Expand Down Expand Up @@ -122,15 +123,15 @@ export default function () {
if (typeof timeout !== 'number') return

if (!store.timeouts) {
debug('step', step.toCode().trim(), 'timeout disabled')
debug('step', extractStepCode(step), 'timeout disabled')
return
}

if (timeout < 0) {
debug('Previous steps timed out, setting timeout to 0.01s')
step.setTimeout(0.01, TIMEOUT_ORDER.testOrSuite)
} else {
debug(`Setting timeout ${timeout}ms for step ${step.toCode().trim()}`)
debug(`Setting timeout ${timeout}ms for step ${extractStepCode(step)}`)
step.setTimeout(timeout, TIMEOUT_ORDER.testOrSuite)
}
})
Expand Down Expand Up @@ -158,17 +159,17 @@ export default function () {

event.dispatcher.on(event.step.finished, step => {
if (!store.timeouts) {
debug('step', step.toCode().trim(), 'timeout disabled')
debug('step', extractStepCode(step), 'timeout disabled')
return
}

if (typeof timeout === 'number') debug('Timeout', timeout)

debug(`step ${step.toCode().trim()}:${step.status} duration`, step.duration)
debug(`step ${extractStepCode(step)}:${step.status} duration`, step.duration)
if (typeof timeout === 'number' && !Number.isNaN(timeout)) timeout -= step.duration

if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
debug(`step ${step.toCode().trim()} timed out`)
debug(`step ${extractStepCode(step)} timed out`)
recorder.throw(new TestTimeoutError(currentTimeout))
}
})
Expand Down
1 change: 1 addition & 0 deletions lib/plugin/autoDelay.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const defaultConfig = {
methods: methodsToDelay,
delayBefore: 100,
delayAfter: 200,
runInParent: false,
}

/**
Expand Down
6 changes: 5 additions & 1 deletion lib/plugin/pauseOnFail.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import pause from '../pause.js'
* ```
*
*/
export default function() {
const defaultConfig = {
runInParent: false,
}

export default function(config) {
let failed = false

event.dispatcher.on(event.test.started, () => {
Expand Down
1 change: 1 addition & 0 deletions lib/plugin/retryFailedStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const defaultConfig = {
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
factor: 1.5,
ignoredSteps: [],
runInParent: false,
deferToScenarioRetries: true,
}

Expand Down
1 change: 1 addition & 0 deletions lib/plugin/screenshotOnFail.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const defaultConfig = {
uniqueScreenshotNames: false,
disableScreenshots: false,
fullPageScreenshots: false,
runInParent: false,
}

const supportedHelpers = Container.STANDARD_ACTING_HELPERS
Expand Down
1 change: 1 addition & 0 deletions lib/plugin/stepTimeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const defaultConfig = {
overrideStepLimits: false,
noTimeoutSteps: ['amOnPage', 'wait*'],
customTimeoutSteps: [],
runInParent: false,
}

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/step.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
import BaseStep from './step/base.js'
import StepConfig from './step/config.js'
import Step from './step/helper.js'
import Step, { extractStepCode } from './step/helper.js'

/**
* MetaStep is a step that is used to wrap other steps.
Expand All @@ -20,4 +20,4 @@ import MetaStep from './step/meta.js'
import FuncStep from './step/func.js'

export default Step
export { MetaStep, BaseStep, StepConfig, FuncStep }
export { MetaStep, BaseStep, StepConfig, FuncStep, extractStepCode }
1 change: 1 addition & 0 deletions lib/step/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ class Step {
startTime: step.startTime,
endTime: step.endTime,
parent,
code: step.toCode(),
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/step/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class HelperStep extends Step {

export default HelperStep

export function extractStepCode(step) {
return (step.code || (typeof step.toCode === 'function' ? step.toCode() : step.name)).trim()
}

function dryRunResolver() {
return {
get(target, prop) {
Expand Down
15 changes: 15 additions & 0 deletions lib/workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,18 +626,22 @@ class Workers extends EventEmitter {
break
case event.suite.before:
this.emit(event.suite.before, deserializeSuite(message.data))
event.dispatcher.emit(event.suite.before, deserializeSuite(message.data))
break
case event.test.before:
this.emit(event.test.before, deserializeTest(message.data))
event.dispatcher.emit(event.test.before, deserializeTest(message.data))
break
case event.test.started:
this.emit(event.test.started, deserializeTest(message.data))
event.dispatcher.emit(event.test.started, deserializeTest(message.data))
break
case event.test.failed:
// For hook failures, emit immediately as there won't be a test.finished event
// Regular test failures are handled via test.finished to support retries
if (message.data?.hookName) {
this.emit(event.test.failed, deserializeTest(message.data))
event.dispatcher.emit(event.test.failed, deserializeTest(message.data))
}
// Otherwise skip - we'll emit based on finished state
break
Expand All @@ -646,6 +650,7 @@ class Workers extends EventEmitter {
break
case event.test.skipped:
this.emit(event.test.skipped, deserializeTest(message.data))
event.dispatcher.emit(event.test.skipped, deserializeTest(message.data))
break
case event.test.finished:
// Handle different types of test completion properly
Expand All @@ -669,34 +674,44 @@ class Workers extends EventEmitter {
// For tests without UID, emit immediately
if (isFailed) {
this.emit(event.test.failed, deserializeTest(data))
event.dispatcher.emit(event.test.failed, deserializeTest(data))
} else {
this.emit(event.test.passed, deserializeTest(data))
event.dispatcher.emit(event.test.passed, deserializeTest(data))
}
}

this.emit(event.test.finished, deserializeTest(data))
event.dispatcher.emit(event.test.finished, deserializeTest(data))
}
break
case event.test.after:
this.emit(event.test.after, deserializeTest(message.data))
event.dispatcher.emit(event.test.after, deserializeTest(message.data))
break
case event.step.finished:
this.emit(event.step.finished, message.data)
event.dispatcher.emit(event.step.finished, message.data)
break
case event.step.started:
this.emit(event.step.started, message.data)
event.dispatcher.emit(event.step.started, message.data)
break
case event.step.passed:
this.emit(event.step.passed, message.data)
event.dispatcher.emit(event.step.passed, message.data)
break
case event.step.failed:
this.emit(event.step.failed, message.data, message.data.error)
event.dispatcher.emit(event.step.failed, message.data, message.data.error)
break
case event.hook.failed:
// Hook failures are already reported as test failures by the worker
// Just emit the hook.failed event for listeners
this.emit(event.hook.failed, message.data)
event.dispatcher.emit(event.hook.failed, message.data)
break

}
})

Expand Down
3 changes: 2 additions & 1 deletion test/helper/webapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -1580,7 +1580,8 @@ export function tests() {
},
)
} catch (e) {
expect(e.message).to.include('expected all elements ({css: a[href="/codeceptjs/CodeceptJS"]}) to have attributes {"disable":true} "0" to equal "3"')
expect(e.message).to.include('expected all elements ({css: a[href="/codeceptjs/CodeceptJS"]}) to have attributes {"disable":true}')
expect(e.message).to.match(/"0" to equal "\d+"/)
}
})

Expand Down
Loading