From 7967530a2136113c07e3312de07c2f6928b1f717 Mon Sep 17 00:00:00 2001 From: kapilvus Date: Sun, 19 Jul 2026 01:11:46 +0530 Subject: [PATCH] fix(junit): preserve suite identity for hook failures reported from workers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under run-workers, event.hook.failed for a BeforeSuite/AfterSuite failure arrives in the main process via Hook.simplify(), which only carries {hookName, title, error} — no ctx, so the reporter has no way to recover the owning suite. groupBySuite() then falls back to key = the failure entry itself, and buildXml's suiteName defaults to "Tests", splitting each hook failure into its own instead of the suite that actually failed. - Hook.simplify() now also serializes suiteTitle/suiteFile/suiteTags from this.runnable.parent (the real Suite, already used successfully on the main-thread path — see the "adds suite hook failures as failed testcases" unit test). - junitReporter's event.hook.failed listener falls back to these fields when hook.ctx is absent, reusing one synthetic suite object per title+file pair so multiple failures from the same worker-reported suite still group into a single . Reproduced with the exact scenario from #5645 (BeforeSuite + AfterSuite both throwing) run via `codecept run-workers 2`: before this change both failures land under two separate entries; after, both are under one , matching the main-thread output shape. --- lib/mocha/hooks.js | 10 ++++++++++ lib/plugin/junitReporter.js | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index 3393e3369..ded7f7b42 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -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 || [], } } diff --git a/lib/plugin/junitReporter.js b/lib/plugin/junitReporter.js index 0fac0a8a8..a3f104e5d 100644 --- a/lib/plugin/junitReporter.js +++ b/lib/plugin/junitReporter.js @@ -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 , 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',