Skip to content
Merged
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
9 changes: 7 additions & 2 deletions bin/mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,17 @@ async function cancelRun() {
abortRun = true
if (typeof pendingRunCleanup === 'function') { try { pendingRunCleanup() } catch {} }
if (pausedController) { try { pausedController.resolveContinue() } catch {} ; pausedController = null }

const mocha = typeof container.mocha === 'function' ? container.mocha() : container.mocha
try { mocha?.runner?.abort?.() } catch {}

if (pendingRunPromise) {
try { await Promise.race([pendingRunPromise.catch(() => {}), new Promise(r => setTimeout(r, 5000))]) } catch {}
try { await pendingRunPromise.catch(() => {}) } catch {}
}
pendingRunPromise = null
pendingRunResults = null
pendingTestFile = null
pendingStepInfo = null
abortRun = false
return true
}

Expand Down Expand Up @@ -1032,6 +1035,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
pendingRunCleanup = null
}

abortRun = false
let runError = null
const runPromise = (async () => {
try {
Expand Down Expand Up @@ -1126,6 +1130,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
pendingRunCleanup = null
}

abortRun = false
let runError = null
const runPromise = (async () => {
try {
Expand Down
2 changes: 1 addition & 1 deletion lib/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ class Codecept {

try {
event.emit(event.all.before, this)
mocha.run(async (failures) => await done(failures))
mocha.runner = mocha.run(async (failures) => await done(failures))
} catch (e) {
output.error(e.stack)
reject(e)
Expand Down
14 changes: 0 additions & 14 deletions lib/command/run-workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,5 @@ export default async function (workerCount, selectedRuns, options) {
process.exitCode = 1
} finally {
await workers.teardownAll()

// Force exit if event loop doesn't clear naturally
// This is needed because worker threads may leave handles open
// even after proper cleanup, preventing natural process termination
if (!options.noExit) {
// Use beforeExit to ensure we run after all other exit handlers
// have set the correct exit code
process.once('beforeExit', (code) => {
// Give cleanup a moment to complete, then force exit with the correct code
setTimeout(() => {
process.exit(code || process.exitCode || 0)
}, 100)
})
}
}
}
18 changes: 2 additions & 16 deletions lib/command/run.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { getConfig, printError, getTestRoot, createOutputDir } from './utils.js'
import { getConfig, printError, getTestRoot, createOutputDir, autoExit } from './utils.js'
import Config from '../config.js'
import store from '../store.js'
import Codecept from '../codecept.js'
import container from '../container.js'

export default async function (test, options) {
// registering options globally to use in config
Expand Down Expand Up @@ -43,19 +42,6 @@ export default async function (test, options) {
process.exitCode = 1
} finally {
await codecept.teardown()

// Schedule a delayed exit to prevent process hanging due to browser helper event loops
// Only needed for Playwright/Puppeteer which keep the event loop alive
// Wait 1 second to allow final cleanup and output to complete
if (!process.env.CODECEPT_DISABLE_AUTO_EXIT) {
const helpers = container.helpers()
const hasBrowserHelper = helpers && (helpers.Playwright || helpers.Puppeteer || helpers.WebDriver)

if (hasBrowserHelper) {
setTimeout(() => {
process.exit(process.exitCode || 0)
}, 1000).unref()
}
}
await autoExit()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kobenguyent I refactored the previously written code for it
I hope having a single point would make this it clearer and easy to control

}
}
14 changes: 14 additions & 0 deletions lib/command/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ export const createOutputDir = (config, testRoot) => {
}
}

export async function autoExit() {
const timeout = parseInt(process.env.CODECEPT_AUTO_EXIT_TIMEOUT, 10)
if (timeout === 0) return
const exitTimeout = timeout || 2000

const { default: container } = await import('../container.js')
const helpers = container.helpers()
if (!helpers || !Object.values(helpers).some(h => typeof h._cleanup === 'function')) return

const { default: recorder } = await import('../recorder.js')
await Promise.race([recorder.promise(), new Promise(resolve => setTimeout(resolve, exitTimeout))])
process.exit(process.exitCode || 0)
}

export const findConfigFile = testsPath => {
const extensions = ['js', 'ts']
for (const ext of extensions) {
Expand Down
7 changes: 4 additions & 3 deletions lib/plugin/retryFailedStep.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import debugModule from 'debug'
import event from '../event.js'
import recorder from '../recorder.js'
import store from '../store.js'

const debug = debugModule('codeceptjs:retryFailedStep')

const defaultConfig = {
retries: 3,
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
Expand Down Expand Up @@ -147,9 +150,7 @@ export default function (config) {
test.opts.conditionalRetries = config.retries
test.opts.stepRetryPriority = stepRetryPriority

if (process.env.DEBUG_RETRY_PLUGIN) {
console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
}
debug('applying retries = %d for test %s', config.retries, test.title)
recorder.retry(config)
})

Expand Down
14 changes: 11 additions & 3 deletions lib/workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,10 +547,11 @@ class Workers extends EventEmitter {
if (this.isPoolMode) {
this.activeWorkers.set(worker, { available: true, workerIndex: null })
}

// Track last activity time to detect hanging workers
let lastActivity = Date.now()
let currentTest = null
let autoTerminated = false
const workerTimeout = process.env.CODECEPT_WORKER_TIMEOUT ? ms(process.env.CODECEPT_WORKER_TIMEOUT) : ms('5m')

const timeoutChecker = setInterval(() => {
Expand Down Expand Up @@ -611,6 +612,13 @@ class Workers extends EventEmitter {
})
}

const exitTimeout = parseInt(process.env.CODECEPT_AUTO_EXIT_TIMEOUT, 10)
if (exitTimeout === 0) break
setTimeout(() => {
autoTerminated = true
worker.terminate()
}, exitTimeout || 2000)

break
case event.suite.before:
{
Expand Down Expand Up @@ -741,8 +749,8 @@ class Workers extends EventEmitter {
worker.on('exit', (code) => {
clearInterval(timeoutChecker)
this.closedWorkers += 1
if (code !== 0) {

if (code !== 0 && !autoTerminated) {
console.error(`[Main] Worker exited with code ${code}`)
if (currentTest) {
console.error(`[Main] Last test running: ${currentTest}`)
Expand Down