Skip to content

Commit 50829bb

Browse files
Jaromir Obrclaude
andcommitted
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>
1 parent 9f371d4 commit 50829bb

12 files changed

Lines changed: 208 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.
@@ -100,19 +101,35 @@ export default function (config = {}) {
100101
meta: {},
101102
steps: [],
102103
duration: (runnable && runnable.duration) || 0,
104+
outputDir: hook.outputDir,
103105
})
104106
})
105107

106108
const writeReport = result => {
107109
if (written) return
108110
if (!result || !Array.isArray(result.tests)) return
111+
if (!isMainThread) return
109112
written = true
110113

114+
const runDirs = [...new Set(result.tests.map(test => test.outputDir).filter(Boolean))]
115+
if (runDirs.length) {
116+
for (const dir of runDirs) {
117+
const runResult = { tests: result.tests.filter(test => test.outputDir === dir), stats: result.stats }
118+
const runHookFailures = hookFailures.filter(hf => hf.outputDir === dir)
119+
saveXml(dir, buildXml(runResult, config, runHookFailures))
120+
}
121+
return
122+
}
123+
111124
const dir = config.output ? path.resolve(store.codeceptDir || process.cwd(), config.output) : store.outputDir || process.cwd()
125+
saveXml(dir, buildXml(result, config, hookFailures))
126+
}
127+
128+
const saveXml = (dir, xml) => {
112129
mkdirp.sync(dir)
113130
const file = path.join(dir, config.outputName)
114131

115-
fs.writeFileSync(file, buildXml(result, config, hookFailures))
132+
fs.writeFileSync(file, xml)
116133
output.plugin('junitReporter', `JUnit report saved to ${file}`)
117134
}
118135

‎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)