codeceptjs 4.0.2-beta.1 → 4.0.2-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/command/run-workers.js +12 -2
- package/lib/command/workers/runTests.js +81 -2
- package/lib/workers.js +81 -3
- package/package.json +1 -1
|
@@ -40,11 +40,22 @@ export default async function (workerCount, selectedRuns, options) {
|
|
|
40
40
|
|
|
41
41
|
output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
|
|
42
42
|
output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
|
|
43
|
-
output.print()
|
|
44
43
|
store.hasWorkers = true
|
|
45
44
|
|
|
46
45
|
const workers = new Workers(numberOfWorkers, config)
|
|
47
46
|
workers.overrideConfig(overrideConfigs)
|
|
47
|
+
|
|
48
|
+
// Show test distribution after workers are initialized
|
|
49
|
+
await workers.bootstrapAll()
|
|
50
|
+
|
|
51
|
+
const workerObjects = workers.getWorkers()
|
|
52
|
+
output.print()
|
|
53
|
+
output.print('Test distribution:')
|
|
54
|
+
workerObjects.forEach((worker, index) => {
|
|
55
|
+
const testCount = worker.tests.length
|
|
56
|
+
output.print(` Worker ${index + 1}: ${testCount} test${testCount !== 1 ? 's' : ''}`)
|
|
57
|
+
})
|
|
58
|
+
output.print()
|
|
48
59
|
|
|
49
60
|
workers.on(event.test.failed, test => {
|
|
50
61
|
output.test.failed(test)
|
|
@@ -68,7 +79,6 @@ export default async function (workerCount, selectedRuns, options) {
|
|
|
68
79
|
if (options.verbose) {
|
|
69
80
|
await getMachineInfo()
|
|
70
81
|
}
|
|
71
|
-
await workers.bootstrapAll()
|
|
72
82
|
await workers.run()
|
|
73
83
|
} catch (err) {
|
|
74
84
|
output.error(err)
|
|
@@ -19,6 +19,29 @@ const stderr = ''
|
|
|
19
19
|
|
|
20
20
|
const { options, tests, testRoot, workerIndex, poolMode } = workerData
|
|
21
21
|
|
|
22
|
+
// Global error handlers to catch critical errors but not test failures
|
|
23
|
+
process.on('uncaughtException', (err) => {
|
|
24
|
+
// Don't exit on test assertion errors - those are handled by mocha
|
|
25
|
+
if (err.name === 'AssertionError' || err.message?.includes('expected')) {
|
|
26
|
+
console.error(`[Worker ${workerIndex}] Test assertion error (handled by mocha):`, err.message)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
console.error(`[Worker ${workerIndex}] Uncaught exception:`, err.message)
|
|
30
|
+
console.error(err.stack)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
35
|
+
// Don't exit on test-related rejections
|
|
36
|
+
const msg = reason?.message || String(reason)
|
|
37
|
+
if (msg.includes('expected') || msg.includes('AssertionError')) {
|
|
38
|
+
console.error(`[Worker ${workerIndex}] Test rejection (handled by mocha):`, msg)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
console.error(`[Worker ${workerIndex}] Unhandled rejection:`, reason)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
})
|
|
44
|
+
|
|
22
45
|
// hide worker output
|
|
23
46
|
// In pool mode, only suppress output if debug is NOT enabled
|
|
24
47
|
// In regular mode, hide result output but allow step output in verbose/debug
|
|
@@ -26,6 +49,10 @@ if (poolMode && !options.debug) {
|
|
|
26
49
|
// In pool mode without debug, allow test names and important output but suppress verbose details
|
|
27
50
|
const originalWrite = process.stdout.write
|
|
28
51
|
process.stdout.write = string => {
|
|
52
|
+
// Always allow Worker logs
|
|
53
|
+
if (string.includes('[Worker')) {
|
|
54
|
+
return originalWrite.call(process.stdout, string)
|
|
55
|
+
}
|
|
29
56
|
// Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
|
|
30
57
|
if (
|
|
31
58
|
string.includes('✔') ||
|
|
@@ -45,7 +72,12 @@ if (poolMode && !options.debug) {
|
|
|
45
72
|
return originalWrite.call(process.stdout, string)
|
|
46
73
|
}
|
|
47
74
|
} else if (!poolMode && !options.debug && !options.verbose) {
|
|
75
|
+
const originalWrite = process.stdout.write
|
|
48
76
|
process.stdout.write = string => {
|
|
77
|
+
// Always allow Worker logs
|
|
78
|
+
if (string.includes('[Worker')) {
|
|
79
|
+
return originalWrite.call(process.stdout, string)
|
|
80
|
+
}
|
|
49
81
|
stdout += string
|
|
50
82
|
return true
|
|
51
83
|
}
|
|
@@ -82,6 +114,8 @@ let config
|
|
|
82
114
|
// Load test and run
|
|
83
115
|
initPromise = (async function () {
|
|
84
116
|
try {
|
|
117
|
+
console.log(`[Worker ${workerIndex}] Starting initialization...`)
|
|
118
|
+
|
|
85
119
|
// Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
|
|
86
120
|
const eventModule = await import('../../event.js')
|
|
87
121
|
const containerModule = await import('../../container.js')
|
|
@@ -89,6 +123,8 @@ initPromise = (async function () {
|
|
|
89
123
|
const coreUtilsModule = await import('../../utils.js')
|
|
90
124
|
const CodeceptModule = await import('../../codecept.js')
|
|
91
125
|
|
|
126
|
+
console.log(`[Worker ${workerIndex}] Modules imported`)
|
|
127
|
+
|
|
92
128
|
event = eventModule.default
|
|
93
129
|
container = containerModule.default
|
|
94
130
|
getConfig = utilsModule.getConfig
|
|
@@ -98,16 +134,24 @@ initPromise = (async function () {
|
|
|
98
134
|
|
|
99
135
|
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
|
|
100
136
|
|
|
137
|
+
console.log(`[Worker ${workerIndex}] Loading config...`)
|
|
138
|
+
|
|
101
139
|
// IMPORTANT: await is required here since getConfig is async
|
|
102
140
|
const baseConfig = await getConfig(options.config || testRoot)
|
|
103
141
|
|
|
142
|
+
console.log(`[Worker ${workerIndex}] Config loaded, creating Codecept...`)
|
|
143
|
+
|
|
104
144
|
// important deep merge so dynamic things e.g. functions on config are not overridden
|
|
105
145
|
config = deepMerge(baseConfig, overrideConfigs)
|
|
106
146
|
|
|
107
147
|
// Pass workerIndex as child option for output.process() to display worker prefix
|
|
108
148
|
const optsWithChild = { ...options, child: workerIndex }
|
|
109
149
|
codecept = new Codecept(config, optsWithChild)
|
|
150
|
+
|
|
151
|
+
console.log(`[Worker ${workerIndex}] Initializing Codecept...`)
|
|
110
152
|
await codecept.init(testRoot)
|
|
153
|
+
|
|
154
|
+
console.log(`[Worker ${workerIndex}] Loading tests...`)
|
|
111
155
|
codecept.loadTests()
|
|
112
156
|
mocha = container.mocha()
|
|
113
157
|
|
|
@@ -116,9 +160,14 @@ initPromise = (async function () {
|
|
|
116
160
|
// We'll reload test files fresh for each test request
|
|
117
161
|
} else {
|
|
118
162
|
// Legacy mode - filter tests upfront
|
|
163
|
+
console.log(`[Worker ${workerIndex}] Starting test filtering. Assigned ${tests.length} test UIDs`)
|
|
119
164
|
filterTests()
|
|
165
|
+
const finalCount = mocha.suite.total()
|
|
166
|
+
console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests to run`)
|
|
120
167
|
}
|
|
121
168
|
|
|
169
|
+
console.log(`[Worker ${workerIndex}] Initialization complete, starting tests...`)
|
|
170
|
+
|
|
122
171
|
// run tests
|
|
123
172
|
if (poolMode) {
|
|
124
173
|
await runPoolTests()
|
|
@@ -126,10 +175,12 @@ initPromise = (async function () {
|
|
|
126
175
|
await runTests()
|
|
127
176
|
} else {
|
|
128
177
|
// No tests to run, close the worker
|
|
178
|
+
console.error(`[Worker ${workerIndex}] ERROR: No tests found after filtering! Assigned ${tests.length} UIDs but none matched.`)
|
|
129
179
|
parentPort?.close()
|
|
130
180
|
}
|
|
131
181
|
} catch (err) {
|
|
132
|
-
console.error(
|
|
182
|
+
console.error(`[Worker ${workerIndex}] FATAL ERROR:`, err.message)
|
|
183
|
+
console.error(err.stack)
|
|
133
184
|
process.exit(1)
|
|
134
185
|
}
|
|
135
186
|
})()
|
|
@@ -140,6 +191,7 @@ async function runTests() {
|
|
|
140
191
|
try {
|
|
141
192
|
await codecept.bootstrap()
|
|
142
193
|
} catch (err) {
|
|
194
|
+
console.error(`[Worker ${workerIndex}] Bootstrap error:`, err.message)
|
|
143
195
|
throw new Error(`Error while running bootstrap file :${err}`)
|
|
144
196
|
}
|
|
145
197
|
listenToParentThread()
|
|
@@ -147,8 +199,15 @@ async function runTests() {
|
|
|
147
199
|
disablePause()
|
|
148
200
|
try {
|
|
149
201
|
await codecept.run()
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error(`[Worker ${workerIndex}] Runtime error:`, err.message)
|
|
204
|
+
throw err
|
|
150
205
|
} finally {
|
|
151
|
-
|
|
206
|
+
try {
|
|
207
|
+
await codecept.teardown()
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error(`[Worker ${workerIndex}] Teardown error:`, err.message)
|
|
210
|
+
}
|
|
152
211
|
}
|
|
153
212
|
}
|
|
154
213
|
|
|
@@ -336,6 +395,26 @@ function filterTests() {
|
|
|
336
395
|
mocha.files = files
|
|
337
396
|
mocha.loadFiles()
|
|
338
397
|
|
|
398
|
+
// Collect all loaded tests for debugging
|
|
399
|
+
const allLoadedTests = [];
|
|
400
|
+
mocha.suite.eachTest(test => {
|
|
401
|
+
if (test) {
|
|
402
|
+
allLoadedTests.push({ uid: test.uid, title: test.fullTitle() });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
console.log(`[Worker ${workerIndex}] Loaded ${allLoadedTests.length} tests from ${files.length} files`);
|
|
407
|
+
console.log(`[Worker ${workerIndex}] Expecting ${tests.length} test UIDs`);
|
|
408
|
+
|
|
409
|
+
const loadedUids = new Set(allLoadedTests.map(t => t.uid));
|
|
410
|
+
const missingTests = tests.filter(uid => !loadedUids.has(uid));
|
|
411
|
+
|
|
412
|
+
if (missingTests.length > 0) {
|
|
413
|
+
console.error(`[Worker ${workerIndex}] ERROR: ${missingTests.length} assigned tests NOT FOUND in loaded files!`);
|
|
414
|
+
console.error(`[Worker ${workerIndex}] Missing UIDs:`, missingTests);
|
|
415
|
+
console.error(`[Worker ${workerIndex}] Available UIDs:`, Array.from(loadedUids).slice(0, 5), '...');
|
|
416
|
+
}
|
|
417
|
+
|
|
339
418
|
// Recursively filter tests in all suites (including nested ones)
|
|
340
419
|
const filterSuiteTests = (suite) => {
|
|
341
420
|
suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
|
package/lib/workers.js
CHANGED
|
@@ -370,6 +370,9 @@ class Workers extends EventEmitter {
|
|
|
370
370
|
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
371
371
|
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
372
372
|
const files = this.codecept.testFiles
|
|
373
|
+
|
|
374
|
+
// Create a fresh mocha instance to avoid state pollution
|
|
375
|
+
Container.createMocha(this.codecept.config.mocha || {}, this.options)
|
|
373
376
|
const mocha = Container.mocha()
|
|
374
377
|
mocha.files = files
|
|
375
378
|
mocha.loadFiles()
|
|
@@ -384,6 +387,10 @@ class Workers extends EventEmitter {
|
|
|
384
387
|
groupCounter++
|
|
385
388
|
}
|
|
386
389
|
})
|
|
390
|
+
|
|
391
|
+
// Clean up after collecting test UIDs
|
|
392
|
+
mocha.unloadFiles()
|
|
393
|
+
|
|
387
394
|
return groups
|
|
388
395
|
}
|
|
389
396
|
|
|
@@ -452,9 +459,12 @@ class Workers extends EventEmitter {
|
|
|
452
459
|
const files = this.codecept.testFiles
|
|
453
460
|
const groups = populateGroups(numberOfWorkers)
|
|
454
461
|
|
|
462
|
+
// Create a fresh mocha instance to avoid state pollution
|
|
463
|
+
Container.createMocha(this.codecept.config.mocha || {}, this.options)
|
|
455
464
|
const mocha = Container.mocha()
|
|
456
465
|
mocha.files = files
|
|
457
466
|
mocha.loadFiles()
|
|
467
|
+
|
|
458
468
|
mocha.suite.suites.forEach(suite => {
|
|
459
469
|
const i = indexOfSmallestElement(groups)
|
|
460
470
|
suite.tests.forEach(test => {
|
|
@@ -463,6 +473,10 @@ class Workers extends EventEmitter {
|
|
|
463
473
|
}
|
|
464
474
|
})
|
|
465
475
|
})
|
|
476
|
+
|
|
477
|
+
// Clean up after collecting test UIDs
|
|
478
|
+
mocha.unloadFiles()
|
|
479
|
+
|
|
466
480
|
return groups
|
|
467
481
|
}
|
|
468
482
|
|
|
@@ -504,8 +518,24 @@ class Workers extends EventEmitter {
|
|
|
504
518
|
// Workers are already running, this is just a placeholder step
|
|
505
519
|
})
|
|
506
520
|
|
|
521
|
+
// Add overall timeout to prevent infinite hanging
|
|
522
|
+
const overallTimeout = setTimeout(() => {
|
|
523
|
+
console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
|
|
524
|
+
workerThreads.forEach(w => {
|
|
525
|
+
try {
|
|
526
|
+
w.terminate()
|
|
527
|
+
} catch (e) {
|
|
528
|
+
// ignore
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
this._finishRun()
|
|
532
|
+
}, 600000) // 10 minutes
|
|
533
|
+
|
|
507
534
|
return new Promise(resolve => {
|
|
508
|
-
this.on('end',
|
|
535
|
+
this.on('end', () => {
|
|
536
|
+
clearTimeout(overallTimeout)
|
|
537
|
+
resolve()
|
|
538
|
+
})
|
|
509
539
|
})
|
|
510
540
|
}
|
|
511
541
|
|
|
@@ -528,8 +558,37 @@ class Workers extends EventEmitter {
|
|
|
528
558
|
if (this.isPoolMode) {
|
|
529
559
|
this.activeWorkers.set(worker, { available: true, workerIndex: null })
|
|
530
560
|
}
|
|
561
|
+
|
|
562
|
+
// Track last activity time to detect hanging workers
|
|
563
|
+
let lastActivity = Date.now()
|
|
564
|
+
let currentTest = null
|
|
565
|
+
const workerTimeout = 300000 // 5 minutes
|
|
566
|
+
|
|
567
|
+
const timeoutChecker = setInterval(() => {
|
|
568
|
+
const elapsed = Date.now() - lastActivity
|
|
569
|
+
if (elapsed > workerTimeout) {
|
|
570
|
+
console.error(`[Main] Worker appears to be hanging (no activity for ${Math.floor(elapsed/1000)}s). Terminating...`)
|
|
571
|
+
if (currentTest) {
|
|
572
|
+
console.error(`[Main] Last test: ${currentTest}`)
|
|
573
|
+
}
|
|
574
|
+
clearInterval(timeoutChecker)
|
|
575
|
+
worker.terminate()
|
|
576
|
+
}
|
|
577
|
+
}, 30000) // Check every 30 seconds
|
|
531
578
|
|
|
532
579
|
worker.on('message', message => {
|
|
580
|
+
lastActivity = Date.now() // Update activity timestamp
|
|
581
|
+
|
|
582
|
+
// Track current test
|
|
583
|
+
if (message.event === event.test.started && message.data) {
|
|
584
|
+
currentTest = message.data.title || message.data.fullTitle
|
|
585
|
+
console.log(`[Worker ${message.workerIndex}] Started: ${currentTest}`)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (message.event === event.test.finished && message.data) {
|
|
589
|
+
console.log(`[Worker ${message.workerIndex}] Finished: ${message.data.title || currentTest}`)
|
|
590
|
+
}
|
|
591
|
+
|
|
533
592
|
output.process(message.workerIndex)
|
|
534
593
|
|
|
535
594
|
// Handle test requests for pool mode
|
|
@@ -646,11 +705,27 @@ class Workers extends EventEmitter {
|
|
|
646
705
|
})
|
|
647
706
|
|
|
648
707
|
worker.on('error', err => {
|
|
708
|
+
console.error(`[Main] Worker error:`, err.message || err)
|
|
709
|
+
if (currentTest) {
|
|
710
|
+
console.error(`[Main] Failed during test: ${currentTest}`)
|
|
711
|
+
}
|
|
649
712
|
this.errors.push(err)
|
|
650
713
|
})
|
|
651
714
|
|
|
652
|
-
worker.on('exit', () => {
|
|
715
|
+
worker.on('exit', (code) => {
|
|
716
|
+
clearInterval(timeoutChecker)
|
|
653
717
|
this.closedWorkers += 1
|
|
718
|
+
|
|
719
|
+
if (code !== 0) {
|
|
720
|
+
console.error(`[Main] Worker exited with code ${code}`)
|
|
721
|
+
if (currentTest) {
|
|
722
|
+
console.error(`[Main] Last test running: ${currentTest}`)
|
|
723
|
+
}
|
|
724
|
+
// Mark as failed
|
|
725
|
+
process.exitCode = 1
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
console.log(`[Main] Workers closed: ${this.closedWorkers}/${this.numberOfWorkers}`)
|
|
654
729
|
|
|
655
730
|
if (this.isPoolMode) {
|
|
656
731
|
// Pool mode: finish when all workers have exited and no more tests
|
|
@@ -665,8 +740,9 @@ class Workers extends EventEmitter {
|
|
|
665
740
|
}
|
|
666
741
|
|
|
667
742
|
_finishRun() {
|
|
743
|
+
console.log('[Main] Finishing test run...')
|
|
668
744
|
event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
|
|
669
|
-
if (Container.result().hasFailed) {
|
|
745
|
+
if (Container.result().hasFailed || this.errors.length > 0) {
|
|
670
746
|
process.exitCode = 1
|
|
671
747
|
} else {
|
|
672
748
|
process.exitCode = 0
|
|
@@ -706,8 +782,10 @@ class Workers extends EventEmitter {
|
|
|
706
782
|
this._testStates.clear()
|
|
707
783
|
}
|
|
708
784
|
|
|
785
|
+
console.log('[Main] Emitting final results...')
|
|
709
786
|
this.emit(event.all.result, Container.result())
|
|
710
787
|
event.dispatcher.emit(event.workers.result, Container.result())
|
|
788
|
+
console.log('[Main] Emitting end event...')
|
|
711
789
|
this.emit('end') // internal event
|
|
712
790
|
}
|
|
713
791
|
|