codeceptjs 3.7.5-beta.1 → 3.7.5-beta.10
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 +23 -0
- package/lib/command/run-failed-tests.js +263 -0
- package/lib/helper/Playwright.js +348 -28
- package/lib/mocha/test.js +1 -0
- package/lib/plugin/failedTestsTracker.js +411 -0
- package/lib/workers.js +16 -3
- package/package.json +1 -1
- package/typings/promiseBasedTypes.d.ts +10 -49
- package/typings/types.d.ts +10 -60
package/bin/codecept.js
CHANGED
|
@@ -296,6 +296,29 @@ 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
|
+
|
|
299
322
|
program.on('command:*', cmd => {
|
|
300
323
|
console.log(`\nUnknown command ${cmd}\n`)
|
|
301
324
|
program.outputHelp()
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { getConfig, printError, getTestRoot, createOutputDir } = require('./utils')
|
|
4
|
+
const Config = require('../config')
|
|
5
|
+
const store = require('../store')
|
|
6
|
+
const Codecept = require('../codecept')
|
|
7
|
+
const output = require('../output')
|
|
8
|
+
const Workers = require('../workers')
|
|
9
|
+
const { tryOrDefault } = require('../utils')
|
|
10
|
+
|
|
11
|
+
module.exports = async function (options) {
|
|
12
|
+
// registering options globally to use in config
|
|
13
|
+
if (options.profile) {
|
|
14
|
+
process.env.profile = options.profile
|
|
15
|
+
}
|
|
16
|
+
if (options.verbose || options.debug) store.debugMode = true
|
|
17
|
+
|
|
18
|
+
const configFile = options.config
|
|
19
|
+
let config = getConfig(configFile)
|
|
20
|
+
|
|
21
|
+
if (options.override) {
|
|
22
|
+
config = Config.append(JSON.parse(options.override))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const testRoot = getTestRoot(configFile)
|
|
26
|
+
createOutputDir(config, testRoot)
|
|
27
|
+
|
|
28
|
+
// Determine failed tests file path - respect CodeceptJS output directory
|
|
29
|
+
const failedTestsFile = options.file || 'failed-tests.json'
|
|
30
|
+
const failedTestsPath = path.isAbsolute(failedTestsFile)
|
|
31
|
+
? failedTestsFile
|
|
32
|
+
: path.resolve(global.output_dir || './output', failedTestsFile)
|
|
33
|
+
|
|
34
|
+
// Check if failed tests file exists
|
|
35
|
+
if (!fs.existsSync(failedTestsPath)) {
|
|
36
|
+
output.error(`Failed tests file not found: ${failedTestsPath}`)
|
|
37
|
+
output.print('Run tests first to generate a failed tests file, or specify a different file with --file option')
|
|
38
|
+
process.exitCode = 1
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let failedTestsData
|
|
43
|
+
try {
|
|
44
|
+
const fileContent = fs.readFileSync(failedTestsPath, 'utf8')
|
|
45
|
+
failedTestsData = JSON.parse(fileContent)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
output.error(`Failed to read or parse failed tests file: ${error.message}`)
|
|
48
|
+
process.exitCode = 1
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!failedTestsData.tests || failedTestsData.tests.length === 0) {
|
|
53
|
+
output.print('No failed tests found in the file')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
output.print(`Found ${failedTestsData.tests.length} failed tests from ${failedTestsData.timestamp}`)
|
|
58
|
+
|
|
59
|
+
// Debug: Show what's in the failed tests data
|
|
60
|
+
if (options.verbose) {
|
|
61
|
+
output.print('\nFailed tests data structure:')
|
|
62
|
+
failedTestsData.tests.forEach((test, index) => {
|
|
63
|
+
output.print(` ${index + 1}. Title: "${test.title}", UID: "${test.uid}", File: "${test.file}"`)
|
|
64
|
+
})
|
|
65
|
+
output.print('')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build test patterns from failed tests
|
|
69
|
+
const testPatterns = []
|
|
70
|
+
const testsByFile = new Map()
|
|
71
|
+
|
|
72
|
+
// Group tests by file for more efficient execution
|
|
73
|
+
failedTestsData.tests.forEach(test => {
|
|
74
|
+
if (test.file) {
|
|
75
|
+
if (!testsByFile.has(test.file)) {
|
|
76
|
+
testsByFile.set(test.file, [])
|
|
77
|
+
}
|
|
78
|
+
testsByFile.get(test.file).push(test)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Build precise test selection from failed tests
|
|
83
|
+
if (testsByFile.size > 0) {
|
|
84
|
+
// Use file paths for loading tests
|
|
85
|
+
for (const [file, tests] of testsByFile) {
|
|
86
|
+
testPatterns.push(file)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract exact test UIDs for precise filtering
|
|
90
|
+
const failedTestUIDs = failedTestsData.tests
|
|
91
|
+
.map(test => test.uid)
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
|
|
94
|
+
if (failedTestUIDs.length > 0) {
|
|
95
|
+
// Pass UIDs to options for precise test filtering
|
|
96
|
+
options.tests = failedTestUIDs
|
|
97
|
+
output.print(`Targeting ${failedTestUIDs.length} specific failed tests by UID`)
|
|
98
|
+
} else {
|
|
99
|
+
// Fallback to title-based grep if no UIDs available
|
|
100
|
+
const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
|
|
101
|
+
if (testTitles.length > 0) {
|
|
102
|
+
const grepPattern = testTitles.map(title => `^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`).join('|')
|
|
103
|
+
options.grep = grepPattern
|
|
104
|
+
output.print(`Targeting failed tests by title pattern (no UIDs available)`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Fallback: use test titles with exact match grep
|
|
109
|
+
const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
|
|
110
|
+
if (testTitles.length > 0) {
|
|
111
|
+
const grepPattern = testTitles.map(title => `^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`).join('|')
|
|
112
|
+
options.grep = grepPattern
|
|
113
|
+
output.print(`Targeting failed tests by title pattern (no file info available)`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if user wants to run with workers
|
|
118
|
+
if (options.workers) {
|
|
119
|
+
await runWithWorkers(config, options, testPatterns, failedTestsData)
|
|
120
|
+
} else {
|
|
121
|
+
await runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runWithWorkers(config, options, testPatterns, failedTestsData) {
|
|
126
|
+
const numberOfWorkers = parseInt(options.workers, 10)
|
|
127
|
+
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override || '{}'), {})
|
|
128
|
+
|
|
129
|
+
// Determine test split strategy
|
|
130
|
+
let by = 'test' // default for failed tests
|
|
131
|
+
if (options.by) {
|
|
132
|
+
by = options.by
|
|
133
|
+
} else if (options.suites) {
|
|
134
|
+
by = 'suite'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate the by option
|
|
138
|
+
const validStrategies = ['test', 'suite', 'pool']
|
|
139
|
+
if (!validStrategies.includes(by)) {
|
|
140
|
+
throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const workerConfig = {
|
|
144
|
+
by,
|
|
145
|
+
testConfig: options.config,
|
|
146
|
+
options,
|
|
147
|
+
selectedRuns: undefined,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// If we have specific test UIDs, override the worker test selection
|
|
151
|
+
if (options.tests && options.tests.length > 0) {
|
|
152
|
+
workerConfig.by = 'test' // Force test-level distribution for precise targeting
|
|
153
|
+
output.print(`Using precise test UID targeting for ${options.tests.length} failed tests`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
output.print(`CodeceptJS v${require('../codecept').version()}`)
|
|
157
|
+
output.print(`Re-running ${failedTestsData.tests.length} failed tests in ${output.styles.bold(numberOfWorkers)} workers...`)
|
|
158
|
+
output.print()
|
|
159
|
+
store.hasWorkers = true
|
|
160
|
+
|
|
161
|
+
const workers = new Workers(numberOfWorkers, workerConfig)
|
|
162
|
+
workers.overrideConfig(overrideConfigs)
|
|
163
|
+
|
|
164
|
+
// Set up event listeners for worker output
|
|
165
|
+
workers.on('test.failed', test => {
|
|
166
|
+
output.test.failed(test)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
workers.on('test.passed', test => {
|
|
170
|
+
output.test.passed(test)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
workers.on('test.skipped', test => {
|
|
174
|
+
output.test.skipped(test)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
workers.on('all.result', result => {
|
|
178
|
+
workers.printResults()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
if (options.verbose || options.debug) store.debugMode = true
|
|
183
|
+
|
|
184
|
+
if (options.verbose) {
|
|
185
|
+
output.print('\nFailed tests to re-run with workers:')
|
|
186
|
+
failedTestsData.tests.forEach((test, index) => {
|
|
187
|
+
output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
|
|
188
|
+
if (test.error && test.error.message) {
|
|
189
|
+
output.print(` Error: ${test.error.message}`)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
output.print('')
|
|
193
|
+
|
|
194
|
+
const { getMachineInfo } = require('./info')
|
|
195
|
+
await getMachineInfo()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await workers.bootstrapAll()
|
|
199
|
+
await workers.run()
|
|
200
|
+
} catch (err) {
|
|
201
|
+
printError(err)
|
|
202
|
+
process.exitCode = 1
|
|
203
|
+
} finally {
|
|
204
|
+
await workers.teardownAll()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot) {
|
|
209
|
+
const codecept = new Codecept(config, options)
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
codecept.init(testRoot)
|
|
213
|
+
await codecept.bootstrap()
|
|
214
|
+
|
|
215
|
+
// Load tests - if we have specific patterns, use them, otherwise load all and filter with grep
|
|
216
|
+
if (testPatterns.length > 0) {
|
|
217
|
+
codecept.loadTests(testPatterns.join(' '))
|
|
218
|
+
} else {
|
|
219
|
+
codecept.loadTests()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If we have specific test UIDs, filter the loaded tests to only include those
|
|
223
|
+
if (options.tests && options.tests.length > 0) {
|
|
224
|
+
const Container = require('../container')
|
|
225
|
+
const mocha = Container.mocha()
|
|
226
|
+
|
|
227
|
+
// Filter suites to only include tests with matching UIDs
|
|
228
|
+
for (const suite of mocha.suite.suites) {
|
|
229
|
+
suite.tests = suite.tests.filter(test => options.tests.includes(test.uid))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Remove empty suites
|
|
233
|
+
mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
|
|
234
|
+
|
|
235
|
+
output.print(`Filtered to ${options.tests.length} specific failed tests by UID`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (options.verbose) {
|
|
239
|
+
global.debugMode = true
|
|
240
|
+
const { getMachineInfo } = require('./info')
|
|
241
|
+
await getMachineInfo()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Display information about what we're running
|
|
245
|
+
if (options.verbose) {
|
|
246
|
+
output.print('\nFailed tests to re-run:')
|
|
247
|
+
failedTestsData.tests.forEach((test, index) => {
|
|
248
|
+
output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
|
|
249
|
+
if (test.error && test.error.message) {
|
|
250
|
+
output.print(` Error: ${test.error.message}`)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
output.print('')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await codecept.run()
|
|
257
|
+
} catch (err) {
|
|
258
|
+
printError(err)
|
|
259
|
+
process.exitCode = 1
|
|
260
|
+
} finally {
|
|
261
|
+
await codecept.teardown()
|
|
262
|
+
}
|
|
263
|
+
}
|