codeceptjs 3.7.5-beta.1 → 3.7.5-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/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,276 @@
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
+ // Use stable identifiers (file:title) instead of UIDs since UIDs change between runs
91
+ const failedTestStableIds = failedTestsData.tests.map(test => test.stableId || `${test.file}:${test.title}`).filter(Boolean)
92
+
93
+ if (failedTestStableIds.length > 0) {
94
+ output.print(`Targeting failed tests by stable ID (${failedTestStableIds.length} tests)`)
95
+ options.stableIds = failedTestStableIds
96
+ } else {
97
+ output.print('Targeting failed tests by title pattern (no stable IDs available)')
98
+ // Fallback to grep pattern matching if no stable IDs
99
+ const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
100
+ if (testTitles.length > 0) {
101
+ const escapedTitles = testTitles.map(title => title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
102
+ options.grep = new RegExp(`^(${escapedTitles.join('|')})$`)
103
+ }
104
+ }
105
+ } else {
106
+ // Fallback: use test titles with exact match grep
107
+ const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
108
+ if (testTitles.length > 0) {
109
+ const grepPattern = testTitles.map(title => `^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`).join('|')
110
+ options.grep = grepPattern
111
+ output.print(`Targeting failed tests by title pattern (no file info available)`)
112
+ }
113
+ }
114
+
115
+ // Check if user wants to run with workers
116
+ if (options.workers) {
117
+ await runWithWorkers(config, options, testPatterns, failedTestsData)
118
+ } else {
119
+ await runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot)
120
+ }
121
+ }
122
+
123
+ async function runWithWorkers(config, options, testPatterns, failedTestsData) {
124
+ const numberOfWorkers = parseInt(options.workers, 10)
125
+ const overrideConfigs = tryOrDefault(() => JSON.parse(options.override || '{}'), {})
126
+
127
+ // Determine test split strategy
128
+ let by = 'test' // default for failed tests
129
+ if (options.by) {
130
+ by = options.by
131
+ } else if (options.suites) {
132
+ by = 'suite'
133
+ }
134
+
135
+ // Validate the by option
136
+ const validStrategies = ['test', 'suite', 'pool']
137
+ if (!validStrategies.includes(by)) {
138
+ throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
139
+ }
140
+
141
+ const workerConfig = {
142
+ by,
143
+ testConfig: options.config,
144
+ options,
145
+ selectedRuns: undefined,
146
+ }
147
+
148
+ // If we have specific test UIDs, override the worker test selection
149
+ if (options.tests && options.tests.length > 0) {
150
+ workerConfig.by = 'test' // Force test-level distribution for precise targeting
151
+ output.print(`Using precise test UID targeting for ${options.tests.length} failed tests`)
152
+ }
153
+
154
+ output.print(`CodeceptJS v${require('../codecept').version()}`)
155
+ output.print(`Re-running ${failedTestsData.tests.length} failed tests in ${output.styles.bold(numberOfWorkers)} workers...`)
156
+ output.print()
157
+ store.hasWorkers = true
158
+
159
+ const workers = new Workers(numberOfWorkers, workerConfig)
160
+ workers.overrideConfig(overrideConfigs)
161
+
162
+ // Set up event listeners for worker output
163
+ workers.on('test.failed', test => {
164
+ output.test.failed(test)
165
+ })
166
+
167
+ workers.on('test.passed', test => {
168
+ output.test.passed(test)
169
+ })
170
+
171
+ workers.on('test.skipped', test => {
172
+ output.test.skipped(test)
173
+ })
174
+
175
+ workers.on('all.result', result => {
176
+ workers.printResults()
177
+ })
178
+
179
+ try {
180
+ if (options.verbose || options.debug) store.debugMode = true
181
+
182
+ if (options.verbose) {
183
+ output.print('\nFailed tests to re-run with workers:')
184
+ failedTestsData.tests.forEach((test, index) => {
185
+ output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
186
+ if (test.error && test.error.message) {
187
+ output.print(` Error: ${test.error.message}`)
188
+ }
189
+ })
190
+ output.print('')
191
+
192
+ const { getMachineInfo } = require('./info')
193
+ await getMachineInfo()
194
+ }
195
+
196
+ await workers.bootstrapAll()
197
+ await workers.run()
198
+ } catch (err) {
199
+ printError(err)
200
+ process.exitCode = 1
201
+ } finally {
202
+ await workers.teardownAll()
203
+ }
204
+ }
205
+
206
+ async function runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot) {
207
+ const codecept = new Codecept(config, options)
208
+
209
+ try {
210
+ codecept.init(testRoot)
211
+ await codecept.bootstrap()
212
+
213
+ // Load tests - if we have specific patterns, use them, otherwise load all and filter with grep
214
+ if (testPatterns.length > 0) {
215
+ codecept.loadTests(testPatterns.join(' '))
216
+ } else {
217
+ codecept.loadTests()
218
+ }
219
+
220
+ // If we have specific test UIDs or stable IDs, filter the loaded tests to only include those
221
+ if ((options.tests && options.tests.length > 0) || (options.stableIds && options.stableIds.length > 0)) {
222
+ const Container = require('../container')
223
+ const mocha = Container.mocha()
224
+
225
+ // Filter suites to only include tests with matching UIDs or stable IDs
226
+ for (const suite of mocha.suite.suites) {
227
+ suite.tests = suite.tests.filter(test => {
228
+ // Check UID match first
229
+ if (options.tests && options.tests.includes(test.uid)) {
230
+ return true
231
+ }
232
+ // Check stable ID match
233
+ if (options.stableIds) {
234
+ const testFile = test.file || test.parent?.file || 'unknown'
235
+ const testTitle = test.title || 'unknown'
236
+ const testStableId = `${testFile}:${testTitle}`
237
+ return options.stableIds.includes(testStableId)
238
+ }
239
+ return false
240
+ })
241
+ }
242
+
243
+ // Remove empty suites
244
+ mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
245
+
246
+ const filterType = options.stableIds ? 'stable ID' : 'UID'
247
+ const filterCount = options.stableIds ? options.stableIds.length : options.tests.length
248
+ output.print(`Filtered to ${filterCount} specific failed tests by ${filterType}`)
249
+ }
250
+
251
+ if (options.verbose) {
252
+ global.debugMode = true
253
+ const { getMachineInfo } = require('./info')
254
+ await getMachineInfo()
255
+ }
256
+
257
+ // Display information about what we're running
258
+ if (options.verbose) {
259
+ output.print('\nFailed tests to re-run:')
260
+ failedTestsData.tests.forEach((test, index) => {
261
+ output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
262
+ if (test.error && test.error.message) {
263
+ output.print(` Error: ${test.error.message}`)
264
+ }
265
+ })
266
+ output.print('')
267
+ }
268
+
269
+ await codecept.run()
270
+ } catch (err) {
271
+ printError(err)
272
+ process.exitCode = 1
273
+ } finally {
274
+ await codecept.teardown()
275
+ }
276
+ }