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
10 changes: 10 additions & 0 deletions lib/mocha/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ class Hook {
}

simplify() {
// this.runnable (context.ctx.test) is the hook's own runnable; its
// .parent is the real Mocha Suite that owns this hook. Included here so
// run-workers can forward suite identity to the main process, where
// listeners (e.g. junitReporter) have no other way to recover it — the
// worker's live Suite/ctx objects aren't serializable across the thread
// boundary, only these plain fields are.
const suite = this.runnable?.parent
return {
hookName: this.hookName,
title: this.title,
// test: this.test ? serializeTest(this.test) : null,
// suite: this.suite ? serializeSuite(this.suite) : null,
error: this.err ? serializeError(this.err) : null,
suiteTitle: suite?.title || null,
suiteFile: suite?.file || null,
suiteTags: suite?.tags || [],
}
}

Expand Down
18 changes: 17 additions & 1 deletion lib/plugin/junitReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,29 @@ export default function (config = {}) {

let written = false
const hookFailures = []
// groupBySuite() (below) groups by object identity, not by value — reused
// across BeforeSuite/AfterSuite failures from the same worker-forwarded
// suite so they land in one <testsuite>, not one per failure.
const workerSuiteByKey = new Map()

event.dispatcher.on(event.hook.failed, hook => {
if (!hook || !['BeforeSuite', 'AfterSuite'].includes(hook.hookName)) return
const err = hook.err || hook.error
if (!err) return
const runnable = hook.ctx && hook.ctx.test
const suite = runnable && runnable.parent
// Under run-workers, hook.ctx is absent — the failure arrives as the
// plain object from Hook.simplify() in the main process instead of a
// live Hook instance. Fall back to the suiteTitle/suiteFile/suiteTags
// simplify() carries across the worker boundary so the failure still
// groups under its real suite instead of the "Tests" fallback.
let suite = runnable && runnable.parent
if (!suite && hook.suiteTitle) {
const key = `${hook.suiteTitle} ${hook.suiteFile || ''}`
if (!workerSuiteByKey.has(key)) {
workerSuiteByKey.set(key, { title: hook.suiteTitle, file: hook.suiteFile, tags: hook.suiteTags })
}
suite = workerSuiteByKey.get(key)
}
hookFailures.push({
title: hook.title || `${hook.hookName} hook failed`,
state: 'failed',
Expand Down
20 changes: 20 additions & 0 deletions test/data/sandbox/codecept.workers-junit.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const config = {
tests: './workers-junit-suite-hooks/*.js',
timeout: 10000,
output: './output',
helpers: {
FileSystem: {},
Workers: {
require: './workers_helper',
},
},
include: {},
async bootstrap() {},
mocha: {},
plugins: {
junitReporter: {
enabled: true,
},
},
name: 'sandbox',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Feature('JunitWorkerSuiteHooks')

BeforeSuite(async () => {
throw new Error('BeforeSuite worker failure')
})

Scenario('should not be executed either', ({ I }) => {
I.say('unreachable')
})

AfterSuite(async () => {
throw new Error('AfterSuite worker failure')
})
29 changes: 29 additions & 0 deletions test/runner/run_workers_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs from 'fs'
import semver from 'semver'
import { exec } from 'child_process'
import { fileURLToPath } from 'url'
import xml2js from 'xml2js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

Expand Down Expand Up @@ -512,6 +513,34 @@ describe('CodeceptJS Workers Runner', function () {
})
})

it('should preserve suite identity for BeforeSuite/AfterSuite hook failures in the JUnit report', function (done) {
if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version')
const reportFile = path.join(codecept_dir, 'output', 'report.xml')
if (fs.existsSync(reportFile)) fs.rmSync(reportFile)

exec(`${codecept_run_glob('codecept.workers-junit.conf.js')} 1`, (err, stdout) => {
;(async () => {
expect(stdout).toContain('BeforeSuite worker failure')
expect(stdout).toContain('AfterSuite worker failure')
expect(err.code).toEqual(1)

expect(fs.existsSync(reportFile)).toEqual(true)
const parsed = await new xml2js.Parser().parseStringPromise(fs.readFileSync(reportFile, 'utf8'))

// Both hook failures must land under the real suite name, grouped into a
// single <testsuite>, not split across two <testsuite name="Tests"> elements.
expect(parsed.testsuites.testsuite).toHaveLength(1)
const suiteEl = parsed.testsuites.testsuite[0]
expect(suiteEl.$.name).toEqual('JunitWorkerSuiteHooks')
expect(suiteEl.testcase).toHaveLength(2)

const names = suiteEl.testcase.map(tc => tc.$.name)
expect(names.some(n => n.includes('BeforeSuite'))).toEqual(true)
expect(names.some(n => n.includes('AfterSuite'))).toEqual(true)
})().then(done, done)
})
})

it('should handle large worker count without inflating statistics', function (done) {
if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version')
// Test with more workers than tests to ensure no inflation
Expand Down
Loading