Skip to content

Commit 0597db8

Browse files
miraoJaromir Obrclaude
authored
fix(workers): keep results of each browser apart in multi-browser run-workers (#5759)
* fix(workers): keep results of each browser apart in multi-browser run-workers (#5411) In `run-workers <n> <run> --by suite` with several browsers, the same test had the same uid in every browser, so the main process merged their results, and every worker wrote its partial report.xml into the shared run folder. - Tag every message from a multi-run worker with the run's output folder and make the test uid unique per run - junitReporter: write reports only in the main process, one per run folder - --last-failed: ignore the run suffix when matching saved uids Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(workers): find the Testomatio HTML report wherever the reporter writes it Newer @testomatio/reporter versions write the report to <output>/report and ignore TESTOMATIO_HTML_REPORT_FOLDER. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Jaromir Obr <jobr@spork.tech> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent de64be0 commit 0597db8

12 files changed

Lines changed: 210 additions & 4 deletions

File tree

‎docs/plugins/junitReporter.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The produced file is consumable by Jenkins, GitLab CI, CircleCI, GitHub Actions
3333
Possible config options:
3434

3535
* `outputName`: file name for the report. Default: `report.xml`.
36-
* `output`: directory where the report is stored, relative to the project root. Default: the `output` directory.
36+
* `output`: directory where the report is stored, relative to the project root. Default: the `output` directory. With `run-workers` and `multiple` runs, each run writes its own report into its run folder (e.g. `output/dev_chromium1`) and this option is ignored.
3737
* `testGroupName`: value of the `name` attribute on the root `<testsuites>` element. Default: `CodeceptJS`.
3838
* `attachMeta`: add the test's meta information (`meta` keys, `tags`, `retries`) as `<properties>`. Default: true.
3939
* `attachSteps`: add the step/substep log as `<system-out>`. Default: true.

‎lib/codecept.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ class Codecept {
279279
*/
280280
filterLastFailedTests(mocha) {
281281
if (!this.opts.lastFailed) return
282-
const failedTestUids = new Set(Result.loadFailedTests().map(test => test.uid))
282+
const failedTestUids = new Set(Result.loadFailedTests().map(test => test.uid?.split('@')[0]))
283283
const filterSuite = suite => {
284284
suite.tests = suite.tests.filter(test => failedTestUids.has(test.uid))
285285
suite.suites.forEach(filterSuite)

‎lib/plugin/junitReporter.js‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs'
22
import path from 'path'
33
import os from 'os'
4+
import { isMainThread } from 'worker_threads'
45
import { mkdirp } from 'mkdirp'
56
import { DOMImplementation, XMLSerializer } from '@xmldom/xmldom'
67

@@ -45,7 +46,7 @@ const INVALID_XML_CHARS = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u0
4546
* Possible config options:
4647
*
4748
* * `outputName`: file name for the report. Default: `report.xml`.
48-
* * `output`: directory where the report is stored, relative to the project root. Default: the `output` directory.
49+
* * `output`: directory where the report is stored, relative to the project root. Default: the `output` directory. With `run-workers` and `multiple` runs, each run writes its own report into its run folder (e.g. `output/dev_chromium1`) and this option is ignored.
4950
* * `testGroupName`: value of the `name` attribute on the root `<testsuites>` element. Default: `CodeceptJS`.
5051
* * `attachMeta`: add the test's meta information (`meta` keys, `tags`, `retries`) as `<properties>`. Default: true.
5152
* * `attachSteps`: add the step/substep log as `<system-out>`. Default: true.
@@ -92,19 +93,35 @@ export default function (config = {}) {
9293
meta: {},
9394
steps: [],
9495
duration: (runnable && runnable.duration) || 0,
96+
outputDir: hook.outputDir,
9597
})
9698
})
9799

98100
const writeReport = result => {
99101
if (written) return
100102
if (!result || !Array.isArray(result.tests)) return
103+
if (!isMainThread) return
101104
written = true
102105

106+
const runDirs = [...new Set(result.tests.map(test => test.outputDir).filter(Boolean))]
107+
if (runDirs.length) {
108+
for (const dir of runDirs) {
109+
const runResult = { tests: result.tests.filter(test => test.outputDir === dir), stats: result.stats }
110+
const runHookFailures = hookFailures.filter(hf => hf.outputDir === dir)
111+
saveXml(dir, buildXml(runResult, config, runHookFailures))
112+
}
113+
return
114+
}
115+
103116
const dir = config.output ? path.resolve(store.codeceptDir || process.cwd(), config.output) : store.outputDir || process.cwd()
117+
saveXml(dir, buildXml(result, config, hookFailures))
118+
}
119+
120+
const saveXml = (dir, xml) => {
104121
mkdirp.sync(dir)
105122
const file = path.join(dir, config.outputName)
106123

107-
fs.writeFileSync(file, buildXml(result, config, hookFailures))
124+
fs.writeFileSync(file, xml)
108125
output.plugin('junitReporter', `JUnit report saved to ${file}`)
109126
}
110127

‎lib/workers.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,22 @@ const createWorker = (workerObject, isPoolMode = false, skipTests = []) => {
104104
})
105105

106106
WorkerStorage.addWorker(worker)
107+
worker.runOutputDir = workerObject.runOutputDir
107108
return worker
108109
}
109110

111+
const tagWithRun = (message, runOutputDir) => {
112+
if (!message.event || !message.data || typeof message.data !== 'object') return
113+
const runName = path.basename(runOutputDir)
114+
const tag = obj => {
115+
if (!obj || typeof obj !== 'object' || obj.outputDir) return
116+
obj.outputDir = runOutputDir
117+
if (obj.uid) obj.uid = `${obj.uid}@${runName}`
118+
}
119+
tag(message.data)
120+
if (Array.isArray(message.data.tests)) message.data.tests.forEach(tag)
121+
}
122+
110123
const simplifyObject = object => {
111124
return Object.keys(object)
112125
.filter(k => k.indexOf('_') !== 0)
@@ -169,6 +182,7 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns
169182
workerObj.addTests(tests)
170183
workerObj.setTestRoot(testRoot)
171184
workerObj.addOptions(options)
185+
workerObj.runOutputDir = path.resolve(testRoot, finalConfig.output)
172186
testWorkerArray.push(workerObj)
173187
})
174188
workers.push(...testWorkerArray)
@@ -659,6 +673,7 @@ class Workers extends EventEmitter {
659673

660674
worker.on('message', message => {
661675
lastActivity = Date.now() // Update activity timestamp
676+
if (worker.runOutputDir) tagWithRun(message, worker.runOutputDir)
662677

663678
// Track current test
664679
if (message.event === event.test.started && message.data) {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const config = {
2+
tests: './workers-multiple-browsers/*.js',
3+
timeout: 10000,
4+
output: './output/junit-multiple',
5+
helpers: {
6+
FakeDriver: {
7+
require: '../fake_driver',
8+
browser: 'dummy',
9+
},
10+
},
11+
include: {},
12+
mocha: {},
13+
multiple: {
14+
dev: {
15+
browsers: ['chromium', 'webkit'],
16+
},
17+
},
18+
plugins: {
19+
junitReporter: {
20+
enabled: true,
21+
},
22+
},
23+
name: 'sandbox',
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const config = {
2+
tests: './workers-multiple-browsers/*.js',
3+
timeout: 10000,
4+
output: './output/testomatio-multiple',
5+
helpers: {
6+
FakeDriver: {
7+
require: '../fake_driver',
8+
browser: 'dummy',
9+
},
10+
},
11+
include: {},
12+
mocha: {},
13+
multiple: {
14+
dev: {
15+
browsers: ['chromium', 'webkit'],
16+
},
17+
},
18+
plugins: {
19+
testomatio: {
20+
enabled: true,
21+
require: '@testomatio/reporter/codecept',
22+
},
23+
},
24+
name: 'sandbox',
25+
}

‎test/data/sandbox/configs/last-failed/codecept.conf.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ export const config = {
55
include: {},
66
bootstrap: null,
77
mocha: {},
8+
multiple: {
9+
dev: {
10+
browsers: ['chromium', 'webkit'],
11+
},
12+
},
813
name: 'last-failed',
914
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Feature('MultipleBrowsersSuiteA')
2+
3+
Scenario('scenario 1', ({ I }) => {
4+
I.printBrowser()
5+
})
6+
7+
Scenario('scenario 2', ({ I }) => {
8+
I.printBrowser()
9+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Feature('MultipleBrowsersSuiteB')
2+
3+
Scenario('scenario 1', ({ I }) => {
4+
I.printBrowser()
5+
})
6+
7+
Scenario('scenario 2', ({ I }) => {
8+
I.printBrowser()
9+
})

‎test/runner/last_failed_test.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ describe('--last-failed', function () {
119119
expect(stdout).not.toContain('multiplies numbers')
120120
})
121121

122+
it('should rerun only failed tests in workers with multiple browsers', async () => {
123+
const fullRun = await run('run-workers 2 dev')
124+
expect(fullRun.stdout).toContain('4 passed, 6 failed')
125+
126+
const { code, stdout } = await run('run-workers 2 dev --last-failed')
127+
expect(code).toEqual(1)
128+
expect(stdout).toContain('0 passed, 6 failed')
129+
expect(stdout).not.toContain('adds numbers')
130+
expect(stdout).not.toContain('concats strings')
131+
})
132+
122133
it('should rerun only failed tests in workers by pool', async () => {
123134
await run('run')
124135
const { stdout } = await run('run-workers 2 --by pool --last-failed')

0 commit comments

Comments
 (0)