codeceptjs 3.7.5-beta.9 → 3.7.6-beta.1
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/bin/codecept.js +0 -23
- package/lib/effects.js +1 -1
- package/lib/helper/Appium.js +1 -1
- package/lib/helper/Playwright.js +3 -4
- package/lib/helper/Puppeteer.js +5 -1
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +6 -3
- package/lib/helper/network/actions.js +4 -2
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/mocha/test.js +0 -1
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/recorder.js +19 -3
- package/lib/retryCoordinator.js +207 -0
- package/lib/utils.js +54 -4
- package/package.json +32 -24
- package/typings/promiseBasedTypes.d.ts +59 -4
- package/typings/types.d.ts +71 -5
- package/lib/command/run-failed-tests.js +0 -218
- package/lib/plugin/failedTestsTracker.js +0 -374
package/bin/codecept.js
CHANGED
|
@@ -296,29 +296,6 @@ program
|
|
|
296
296
|
|
|
297
297
|
.action(require('../lib/command/run-rerun'))
|
|
298
298
|
|
|
299
|
-
program
|
|
300
|
-
.command('run-failed-tests')
|
|
301
|
-
.description('Re-run tests that failed in the previous test run')
|
|
302
|
-
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
303
|
-
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
304
|
-
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
305
|
-
.option(commandFlags.debug.flag, commandFlags.debug.description)
|
|
306
|
-
.option(commandFlags.steps.flag, commandFlags.steps.description)
|
|
307
|
-
.option('-o, --override [value]', 'override current config options')
|
|
308
|
-
.option('-f, --file [path]', 'path to failed tests file (default: ./failed-tests.json)')
|
|
309
|
-
.option('-g, --grep <pattern>', 'only run failed tests matching <pattern>')
|
|
310
|
-
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
|
|
311
|
-
.option('--features', 'run only *.feature files and skip tests')
|
|
312
|
-
.option('--tests', 'run only JS test files and skip features')
|
|
313
|
-
.option('--colors', 'force enabling of colors')
|
|
314
|
-
.option('--no-colors', 'force disabling of colors')
|
|
315
|
-
.option('-R, --reporter <name>', 'specify the reporter to use')
|
|
316
|
-
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
|
|
317
|
-
.option('--workers <number>', 'run failed tests in parallel using specified number of workers')
|
|
318
|
-
.option('--suites', 'parallel execution of suites not single tests (when using --workers)')
|
|
319
|
-
.option('--by <strategy>', 'test distribution strategy when using --workers: "test", "suite", or "pool"')
|
|
320
|
-
.action(errorHandler(require('../lib/command/run-failed-tests')))
|
|
321
|
-
|
|
322
299
|
program.on('command:*', cmd => {
|
|
323
300
|
console.log(`\nUnknown command ${cmd}\n`)
|
|
324
301
|
program.outputHelp()
|
package/lib/effects.js
CHANGED
|
@@ -89,7 +89,7 @@ async function hopeThat(callback) {
|
|
|
89
89
|
* - Restores the session state after each attempt, whether successful or not.
|
|
90
90
|
*
|
|
91
91
|
* @example
|
|
92
|
-
* const {
|
|
92
|
+
* const { retryTo } = require('codeceptjs/effects')
|
|
93
93
|
* await retryTo((tries) => {
|
|
94
94
|
* if (tries < 3) {
|
|
95
95
|
* I.see('Non-existent element'); // Simulates a failure
|
package/lib/helper/Appium.js
CHANGED
|
@@ -275,7 +275,7 @@ class Appium extends Webdriver {
|
|
|
275
275
|
const _convertedCaps = {}
|
|
276
276
|
for (const [key, value] of Object.entries(capabilities)) {
|
|
277
277
|
if (!key.startsWith(vendorPrefix.appium)) {
|
|
278
|
-
if (key !== 'platformName' && key !== 'bstack:options') {
|
|
278
|
+
if (key !== 'platformName' && key !== 'bstack:options' && key !== 'sauce:options') {
|
|
279
279
|
_convertedCaps[`${vendorPrefix.appium}:${key}`] = value
|
|
280
280
|
} else {
|
|
281
281
|
_convertedCaps[`${key}`] = value
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -360,10 +360,6 @@ class Playwright extends Helper {
|
|
|
360
360
|
// override defaults with config
|
|
361
361
|
this._setConfig(config)
|
|
362
362
|
|
|
363
|
-
// Call _init() to register selector engines - use setTimeout to avoid blocking constructor
|
|
364
|
-
setTimeout(() => {
|
|
365
|
-
this._init().catch(console.error)
|
|
366
|
-
}, 0)
|
|
367
363
|
}
|
|
368
364
|
|
|
369
365
|
_validateConfig(config) {
|
|
@@ -642,6 +638,9 @@ class Playwright extends Helper {
|
|
|
642
638
|
async _after() {
|
|
643
639
|
if (!this.isRunning) return
|
|
644
640
|
|
|
641
|
+
// Clear popup state to prevent leakage between tests
|
|
642
|
+
popupStore.clear()
|
|
643
|
+
|
|
645
644
|
if (this.isElectron) {
|
|
646
645
|
this.browser.close()
|
|
647
646
|
this.electronSessions.forEach(session => session.close())
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -330,6 +330,9 @@ class Puppeteer extends Helper {
|
|
|
330
330
|
async _after() {
|
|
331
331
|
if (!this.isRunning) return
|
|
332
332
|
|
|
333
|
+
// Clear popup state to prevent leakage between tests
|
|
334
|
+
popupStore.clear()
|
|
335
|
+
|
|
333
336
|
// close other sessions
|
|
334
337
|
const contexts = this.browser.browserContexts()
|
|
335
338
|
const defaultCtx = contexts.shift()
|
|
@@ -2588,7 +2591,8 @@ class Puppeteer extends Helper {
|
|
|
2588
2591
|
*
|
|
2589
2592
|
* {{> stopRecordingTraffic }}
|
|
2590
2593
|
*/
|
|
2591
|
-
stopRecordingTraffic() {
|
|
2594
|
+
async stopRecordingTraffic() {
|
|
2595
|
+
await this.page.setRequestInterception(false)
|
|
2592
2596
|
stopRecordingTraffic.call(this)
|
|
2593
2597
|
}
|
|
2594
2598
|
|
package/lib/helper/REST.js
CHANGED
|
@@ -281,6 +281,27 @@ class REST extends Helper {
|
|
|
281
281
|
return this._executeRequest(request)
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Send HEAD request to REST API
|
|
286
|
+
*
|
|
287
|
+
* ```js
|
|
288
|
+
* I.sendHeadRequest('/api/users.json');
|
|
289
|
+
* ```
|
|
290
|
+
*
|
|
291
|
+
* @param {*} url
|
|
292
|
+
* @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
|
|
293
|
+
*
|
|
294
|
+
* @returns {Promise<*>} response
|
|
295
|
+
*/
|
|
296
|
+
async sendHeadRequest(url, headers = {}) {
|
|
297
|
+
const request = {
|
|
298
|
+
baseURL: this._url(url),
|
|
299
|
+
method: 'HEAD',
|
|
300
|
+
headers,
|
|
301
|
+
}
|
|
302
|
+
return this._executeRequest(request)
|
|
303
|
+
}
|
|
304
|
+
|
|
284
305
|
/**
|
|
285
306
|
* Sends POST request to API.
|
|
286
307
|
*
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -491,7 +491,7 @@ class WebDriver extends Helper {
|
|
|
491
491
|
}
|
|
492
492
|
config.capabilities.browserName = config.browser || config.capabilities.browserName
|
|
493
493
|
|
|
494
|
-
// WebDriver Bidi Protocol. Default:
|
|
494
|
+
// WebDriver Bidi Protocol. Default: true
|
|
495
495
|
config.capabilities.webSocketUrl = config.bidiProtocol ?? config.capabilities.webSocketUrl ?? true
|
|
496
496
|
|
|
497
497
|
config.capabilities.browserVersion = config.browserVersion || config.capabilities.browserVersion
|
|
@@ -629,8 +629,11 @@ class WebDriver extends Helper {
|
|
|
629
629
|
|
|
630
630
|
this.browser.on('dialog', () => {})
|
|
631
631
|
|
|
632
|
-
|
|
633
|
-
this.browser.
|
|
632
|
+
// Check for Bidi, because "sessionSubscribe" is an exclusive Bidi protocol feature. Otherwise, error will be thrown.
|
|
633
|
+
if (this.browser.capabilities && this.browser.capabilities.webSocketUrl) {
|
|
634
|
+
await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
|
|
635
|
+
this.browser.on('log.entryAdded', logEvents)
|
|
636
|
+
}
|
|
634
637
|
|
|
635
638
|
return this.browser
|
|
636
639
|
}
|
|
@@ -30,7 +30,7 @@ async function seeTraffic({
|
|
|
30
30
|
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
if (!this.
|
|
33
|
+
if (!this.recordedAtLeastOnce) {
|
|
34
34
|
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -66,7 +66,7 @@ async function seeTraffic({
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
async function grabRecordedNetworkTraffics() {
|
|
69
|
-
if (!this.
|
|
69
|
+
if (!this.recordedAtLeastOnce) {
|
|
70
70
|
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -107,6 +107,8 @@ async function grabRecordedNetworkTraffics() {
|
|
|
107
107
|
function stopRecordingTraffic() {
|
|
108
108
|
// @ts-ignore
|
|
109
109
|
this.page.removeAllListeners('request');
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
this.page.removeAllListeners('requestfinished');
|
|
110
112
|
this.recording = false;
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const event = require('../event')
|
|
2
|
+
const output = require('../output')
|
|
3
|
+
const Config = require('../config')
|
|
4
|
+
const { isNotSet } = require('../utils')
|
|
5
|
+
|
|
6
|
+
const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Priority levels for retry mechanisms (higher number = higher priority)
|
|
10
|
+
* This ensures consistent behavior when multiple retry mechanisms are active
|
|
11
|
+
*/
|
|
12
|
+
const RETRY_PRIORITIES = {
|
|
13
|
+
MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
|
|
14
|
+
STEP_PLUGIN: 50, // retryFailedStep plugin
|
|
15
|
+
SCENARIO_CONFIG: 30, // Global scenario retry config
|
|
16
|
+
FEATURE_CONFIG: 20, // Global feature retry config
|
|
17
|
+
HOOK_CONFIG: 10, // Hook retry config - lowest priority
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enhanced global retry mechanism that coordinates with other retry types
|
|
22
|
+
*/
|
|
23
|
+
module.exports = function () {
|
|
24
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
25
|
+
let retryConfig = Config.get('retry')
|
|
26
|
+
if (!retryConfig) return
|
|
27
|
+
|
|
28
|
+
if (Number.isInteger(+retryConfig)) {
|
|
29
|
+
// is number - apply as feature-level retry
|
|
30
|
+
const retryNum = +retryConfig
|
|
31
|
+
output.log(`[Global Retry] Feature retries: ${retryNum}`)
|
|
32
|
+
|
|
33
|
+
// Only set if not already set by higher priority mechanism
|
|
34
|
+
if (isNotSet(suite.retries())) {
|
|
35
|
+
suite.retries(retryNum)
|
|
36
|
+
suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
|
|
37
|
+
}
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!Array.isArray(retryConfig)) {
|
|
42
|
+
retryConfig = [retryConfig]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const config of retryConfig) {
|
|
46
|
+
if (config.grep) {
|
|
47
|
+
if (!suite.title.includes(config.grep)) continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle hook retries with priority awareness
|
|
51
|
+
hooks
|
|
52
|
+
.filter(hook => !!config[hook])
|
|
53
|
+
.forEach(hook => {
|
|
54
|
+
const retryKey = `retry${hook}`
|
|
55
|
+
if (isNotSet(suite.opts[retryKey])) {
|
|
56
|
+
suite.opts[retryKey] = config[hook]
|
|
57
|
+
suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Handle feature-level retries
|
|
62
|
+
if (config.Feature) {
|
|
63
|
+
if (isNotSet(suite.retries()) || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
|
|
64
|
+
suite.retries(config.Feature)
|
|
65
|
+
suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
|
|
66
|
+
output.log(`[Global Retry] Feature retries: ${config.Feature}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
event.dispatcher.on(event.test.before, test => {
|
|
73
|
+
let retryConfig = Config.get('retry')
|
|
74
|
+
if (!retryConfig) return
|
|
75
|
+
|
|
76
|
+
if (Number.isInteger(+retryConfig)) {
|
|
77
|
+
// Only set if not already set by higher priority mechanism
|
|
78
|
+
if (test.retries() === -1) {
|
|
79
|
+
test.retries(retryConfig)
|
|
80
|
+
test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
|
|
81
|
+
output.log(`[Global Retry] Scenario retries: ${retryConfig}`)
|
|
82
|
+
}
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!Array.isArray(retryConfig)) {
|
|
87
|
+
retryConfig = [retryConfig]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
retryConfig = retryConfig.filter(config => !!config.Scenario)
|
|
91
|
+
|
|
92
|
+
for (const config of retryConfig) {
|
|
93
|
+
if (config.grep) {
|
|
94
|
+
if (!test.fullTitle().includes(config.grep)) continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.Scenario) {
|
|
98
|
+
// Respect priority system
|
|
99
|
+
if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
|
|
100
|
+
test.retries(config.Scenario)
|
|
101
|
+
test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
|
|
102
|
+
output.log(`[Global Retry] Scenario retries: ${config.Scenario}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Export priority constants for use by other retry mechanisms
|
|
110
|
+
module.exports.RETRY_PRIORITIES = RETRY_PRIORITIES
|
package/lib/mocha/test.js
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const event = require('../event')
|
|
2
|
+
const recorder = require('../recorder')
|
|
3
|
+
const store = require('../store')
|
|
4
|
+
const output = require('../output')
|
|
5
|
+
const { RETRY_PRIORITIES } = require('../retryCoordinator')
|
|
6
|
+
|
|
7
|
+
const defaultConfig = {
|
|
8
|
+
retries: 3,
|
|
9
|
+
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
|
|
10
|
+
factor: 1.5,
|
|
11
|
+
ignoredSteps: [],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enhanced retryFailedStep plugin that coordinates with other retry mechanisms
|
|
16
|
+
*
|
|
17
|
+
* This plugin provides step-level retries and coordinates with global retry settings
|
|
18
|
+
* to avoid conflicts and provide predictable behavior.
|
|
19
|
+
*/
|
|
20
|
+
module.exports = config => {
|
|
21
|
+
config = Object.assign({}, defaultConfig, config)
|
|
22
|
+
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
|
|
23
|
+
const customWhen = config.when
|
|
24
|
+
|
|
25
|
+
let enableRetry = false
|
|
26
|
+
|
|
27
|
+
const when = err => {
|
|
28
|
+
if (!enableRetry) return false
|
|
29
|
+
if (store.debugMode) return false
|
|
30
|
+
if (!store.autoRetries) return false
|
|
31
|
+
if (customWhen) return customWhen(err)
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
config.when = when
|
|
35
|
+
|
|
36
|
+
event.dispatcher.on(event.step.started, step => {
|
|
37
|
+
// if a step is ignored - return
|
|
38
|
+
for (const ignored of config.ignoredSteps) {
|
|
39
|
+
if (step.name === ignored) return
|
|
40
|
+
if (ignored instanceof RegExp) {
|
|
41
|
+
if (step.name.match(ignored)) return
|
|
42
|
+
} else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
|
|
43
|
+
}
|
|
44
|
+
enableRetry = true // enable retry for a step
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
event.dispatcher.on(event.step.finished, () => {
|
|
48
|
+
enableRetry = false
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
event.dispatcher.on(event.test.before, test => {
|
|
52
|
+
// pass disableRetryFailedStep is a preferred way to disable retries
|
|
53
|
+
// test.disableRetryFailedStep is used for backward compatibility
|
|
54
|
+
if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
|
|
55
|
+
store.autoRetries = false
|
|
56
|
+
output.log(`[Step Retry] Disabled for test: ${test.title}`)
|
|
57
|
+
return // disable retry when a test is not active
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if step retries should be disabled due to higher priority scenario retries
|
|
61
|
+
const scenarioRetries = test.retries()
|
|
62
|
+
const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
|
|
63
|
+
const scenarioPriority = test.opts.retryPriority || 0
|
|
64
|
+
|
|
65
|
+
if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
|
|
66
|
+
// Scenario retries are configured with higher or equal priority
|
|
67
|
+
// Option 1: Disable step retries (conservative approach)
|
|
68
|
+
store.autoRetries = false
|
|
69
|
+
output.log(`[Step Retry] Deferred to scenario retries (${scenarioRetries} retries)`)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
// Option 2: Reduce step retries to avoid excessive total retries
|
|
73
|
+
// const reducedStepRetries = Math.max(1, Math.floor(config.retries / scenarioRetries))
|
|
74
|
+
// config.retries = reducedStepRetries
|
|
75
|
+
// output.log(`[Step Retry] Reduced to ${reducedStepRetries} retries due to scenario retries (${scenarioRetries})`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// this option is used to set the retries inside _before() block of helpers
|
|
79
|
+
store.autoRetries = true
|
|
80
|
+
test.opts.conditionalRetries = config.retries
|
|
81
|
+
test.opts.stepRetryPriority = stepRetryPriority
|
|
82
|
+
|
|
83
|
+
recorder.retry(config)
|
|
84
|
+
|
|
85
|
+
output.log(`[Step Retry] Enabled with ${config.retries} retries for test: ${test.title}`)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Add coordination info for debugging
|
|
89
|
+
event.dispatcher.on(event.test.finished, test => {
|
|
90
|
+
if (test.state === 'passed' && test.opts.conditionalRetries && store.autoRetries) {
|
|
91
|
+
const stepRetries = test.opts.conditionalRetries || 0
|
|
92
|
+
const scenarioRetries = test.retries() || 0
|
|
93
|
+
|
|
94
|
+
if (stepRetries > 0 && scenarioRetries > 0) {
|
|
95
|
+
output.log(`[Retry Coordination] Test used both step retries (${stepRetries}) and scenario retries (${scenarioRetries})`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
package/lib/recorder.js
CHANGED
|
@@ -11,6 +11,7 @@ let running = false
|
|
|
11
11
|
let errFn
|
|
12
12
|
let queueId = 0
|
|
13
13
|
let sessionId = null
|
|
14
|
+
let sessionStack = [] // Stack to support nested sessions
|
|
14
15
|
let asyncErr = null
|
|
15
16
|
let ignoredErrs = []
|
|
16
17
|
|
|
@@ -89,6 +90,7 @@ module.exports = {
|
|
|
89
90
|
if (promise && running) this.catch()
|
|
90
91
|
queueId++
|
|
91
92
|
sessionId = null
|
|
93
|
+
sessionStack = [] // Clear the session stack
|
|
92
94
|
asyncErr = null
|
|
93
95
|
log(`${currentQueue()} Starting recording promises`)
|
|
94
96
|
promise = Promise.resolve()
|
|
@@ -123,8 +125,13 @@ module.exports = {
|
|
|
123
125
|
*/
|
|
124
126
|
start(name) {
|
|
125
127
|
if (sessionId) {
|
|
126
|
-
debug(`${currentQueue()}Session already started as ${sessionId}`)
|
|
127
|
-
|
|
128
|
+
debug(`${currentQueue()}Session already started as ${sessionId}, nesting session ${name}`)
|
|
129
|
+
// Push current session to stack instead of restoring it
|
|
130
|
+
sessionStack.push({
|
|
131
|
+
id: sessionId,
|
|
132
|
+
promise: promise,
|
|
133
|
+
running: this.running,
|
|
134
|
+
})
|
|
128
135
|
}
|
|
129
136
|
debug(`${currentQueue()}Starting <${name}> session`)
|
|
130
137
|
tasks.push('--->')
|
|
@@ -142,9 +149,18 @@ module.exports = {
|
|
|
142
149
|
tasks.push('<---')
|
|
143
150
|
debug(`${currentQueue()}Finalize <${name}> session`)
|
|
144
151
|
this.running = false
|
|
145
|
-
sessionId = null
|
|
146
152
|
this.catch(errFn)
|
|
147
153
|
promise = promise.then(() => oldPromises.pop())
|
|
154
|
+
|
|
155
|
+
// Restore parent session from stack if available
|
|
156
|
+
if (sessionStack.length > 0) {
|
|
157
|
+
const parentSession = sessionStack.pop()
|
|
158
|
+
sessionId = parentSession.id
|
|
159
|
+
this.running = parentSession.running
|
|
160
|
+
debug(`${currentQueue()}Restored parent session <${sessionId}>`)
|
|
161
|
+
} else {
|
|
162
|
+
sessionId = null
|
|
163
|
+
}
|
|
148
164
|
},
|
|
149
165
|
|
|
150
166
|
/**
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const output = require('./output')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry Coordinator - Central coordination for all retry mechanisms
|
|
5
|
+
*
|
|
6
|
+
* This module provides:
|
|
7
|
+
* 1. Priority-based retry coordination
|
|
8
|
+
* 2. Unified configuration validation
|
|
9
|
+
* 3. Consolidated retry reporting
|
|
10
|
+
* 4. Conflict detection and resolution
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Priority levels for retry mechanisms (higher number = higher priority)
|
|
15
|
+
*/
|
|
16
|
+
const RETRY_PRIORITIES = {
|
|
17
|
+
MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
|
|
18
|
+
STEP_PLUGIN: 50, // retryFailedStep plugin
|
|
19
|
+
SCENARIO_CONFIG: 30, // Global scenario retry config
|
|
20
|
+
FEATURE_CONFIG: 20, // Global feature retry config
|
|
21
|
+
HOOK_CONFIG: 10, // Hook retry config - lowest priority
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retry mechanism types
|
|
26
|
+
*/
|
|
27
|
+
const RETRY_TYPES = {
|
|
28
|
+
MANUAL_STEP: 'manual-step',
|
|
29
|
+
STEP_PLUGIN: 'step-plugin',
|
|
30
|
+
SCENARIO: 'scenario',
|
|
31
|
+
FEATURE: 'feature',
|
|
32
|
+
HOOK: 'hook',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Global retry coordination state
|
|
37
|
+
*/
|
|
38
|
+
let retryState = {
|
|
39
|
+
activeTest: null,
|
|
40
|
+
activeSuite: null,
|
|
41
|
+
retryHistory: [],
|
|
42
|
+
conflicts: [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers a retry mechanism for coordination
|
|
47
|
+
* @param {string} type - Type of retry mechanism
|
|
48
|
+
* @param {Object} config - Retry configuration
|
|
49
|
+
* @param {Object} target - Target object (test, suite, etc.)
|
|
50
|
+
* @param {number} priority - Priority level
|
|
51
|
+
*/
|
|
52
|
+
function registerRetry(type, config, target, priority = 0) {
|
|
53
|
+
const retryInfo = {
|
|
54
|
+
type,
|
|
55
|
+
config,
|
|
56
|
+
target,
|
|
57
|
+
priority,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect conflicts
|
|
62
|
+
const existingRetries = retryState.retryHistory.filter(r => r.target === target && r.type !== type && r.priority !== priority)
|
|
63
|
+
|
|
64
|
+
if (existingRetries.length > 0) {
|
|
65
|
+
const conflict = {
|
|
66
|
+
newRetry: retryInfo,
|
|
67
|
+
existingRetries: existingRetries,
|
|
68
|
+
resolved: false,
|
|
69
|
+
}
|
|
70
|
+
retryState.conflicts.push(conflict)
|
|
71
|
+
handleRetryConflict(conflict)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
retryState.retryHistory.push(retryInfo)
|
|
75
|
+
|
|
76
|
+
output.log(`[Retry Coordinator] Registered ${type} retry (priority: ${priority})`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handles conflicts between retry mechanisms
|
|
81
|
+
* @param {Object} conflict - Conflict information
|
|
82
|
+
*/
|
|
83
|
+
function handleRetryConflict(conflict) {
|
|
84
|
+
const { newRetry, existingRetries } = conflict
|
|
85
|
+
|
|
86
|
+
// Find highest priority retry
|
|
87
|
+
const allRetries = [newRetry, ...existingRetries]
|
|
88
|
+
const highestPriority = Math.max(...allRetries.map(r => r.priority))
|
|
89
|
+
const winningRetry = allRetries.find(r => r.priority === highestPriority)
|
|
90
|
+
|
|
91
|
+
// Log the conflict resolution
|
|
92
|
+
output.log(`[Retry Coordinator] Conflict detected:`)
|
|
93
|
+
allRetries.forEach(retry => {
|
|
94
|
+
const status = retry === winningRetry ? 'ACTIVE' : 'DEFERRED'
|
|
95
|
+
output.log(` - ${retry.type} (priority: ${retry.priority}) [${status}]`)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
conflict.resolved = true
|
|
99
|
+
conflict.winner = winningRetry
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Gets the effective retry configuration for a target
|
|
104
|
+
* @param {Object} target - Target object (test, suite, etc.)
|
|
105
|
+
* @returns {Object} Effective retry configuration
|
|
106
|
+
*/
|
|
107
|
+
function getEffectiveRetryConfig(target) {
|
|
108
|
+
const targetRetries = retryState.retryHistory.filter(r => r.target === target)
|
|
109
|
+
|
|
110
|
+
if (targetRetries.length === 0) {
|
|
111
|
+
return { type: 'none', retries: 0 }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find highest priority retry
|
|
115
|
+
const highestPriority = Math.max(...targetRetries.map(r => r.priority))
|
|
116
|
+
const effectiveRetry = targetRetries.find(r => r.priority === highestPriority)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
type: effectiveRetry.type,
|
|
120
|
+
retries: effectiveRetry.config.retries || effectiveRetry.config,
|
|
121
|
+
config: effectiveRetry.config,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generates a retry summary report
|
|
127
|
+
* @returns {Object} Retry summary
|
|
128
|
+
*/
|
|
129
|
+
function generateRetrySummary() {
|
|
130
|
+
const summary = {
|
|
131
|
+
totalRetryMechanisms: retryState.retryHistory.length,
|
|
132
|
+
conflicts: retryState.conflicts.length,
|
|
133
|
+
byType: {},
|
|
134
|
+
recommendations: [],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Count by type
|
|
138
|
+
retryState.retryHistory.forEach(retry => {
|
|
139
|
+
summary.byType[retry.type] = (summary.byType[retry.type] || 0) + 1
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Generate recommendations
|
|
143
|
+
if (summary.conflicts > 0) {
|
|
144
|
+
summary.recommendations.push('Consider consolidating retry configurations to avoid conflicts')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (summary.byType[RETRY_TYPES.STEP_PLUGIN] && summary.byType[RETRY_TYPES.SCENARIO]) {
|
|
148
|
+
summary.recommendations.push('Step-level and scenario-level retries are both active - consider using only one approach')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return summary
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resets the retry coordination state (useful for testing)
|
|
156
|
+
*/
|
|
157
|
+
function reset() {
|
|
158
|
+
retryState = {
|
|
159
|
+
activeTest: null,
|
|
160
|
+
activeSuite: null,
|
|
161
|
+
retryHistory: [],
|
|
162
|
+
conflicts: [],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validates retry configuration for common issues
|
|
168
|
+
* @param {Object} config - Configuration object
|
|
169
|
+
* @returns {Array} Array of validation warnings
|
|
170
|
+
*/
|
|
171
|
+
function validateConfig(config) {
|
|
172
|
+
const warnings = []
|
|
173
|
+
|
|
174
|
+
if (!config) return warnings
|
|
175
|
+
|
|
176
|
+
// Check for potential configuration conflicts
|
|
177
|
+
if (config.retry && config.plugins && config.plugins.retryFailedStep) {
|
|
178
|
+
const globalRetries = typeof config.retry === 'number' ? config.retry : config.retry.Scenario || config.retry.Feature
|
|
179
|
+
const stepRetries = config.plugins.retryFailedStep.retries || 3
|
|
180
|
+
|
|
181
|
+
if (globalRetries && stepRetries) {
|
|
182
|
+
warnings.push(`Both global retries (${globalRetries}) and step retries (${stepRetries}) are configured - total executions could be ${globalRetries * (stepRetries + 1)}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for excessive retry counts
|
|
187
|
+
if (config.retry) {
|
|
188
|
+
const retryValues = typeof config.retry === 'number' ? [config.retry] : Object.values(config.retry)
|
|
189
|
+
const maxRetries = Math.max(...retryValues.filter(v => typeof v === 'number'))
|
|
190
|
+
|
|
191
|
+
if (maxRetries > 5) {
|
|
192
|
+
warnings.push(`High retry count detected (${maxRetries}) - consider investigating test stability instead`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return warnings
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
RETRY_PRIORITIES,
|
|
201
|
+
RETRY_TYPES,
|
|
202
|
+
registerRetry,
|
|
203
|
+
getEffectiveRetryConfig,
|
|
204
|
+
generateRetrySummary,
|
|
205
|
+
validateConfig,
|
|
206
|
+
reset,
|
|
207
|
+
}
|