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 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
+ }