diff --git a/bin/codecept.js b/bin/codecept.js index b1e0a3737..d1b57f42a 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -95,6 +95,7 @@ program.command('run [test]') .option('--features', 'run only *.feature files and skip tests') .option('--tests', 'run only JS test files and skip features') .option('-p, --plugins ', 'enable plugins, comma-separated') + .option('--rerun-tests','to run the selected / failed tests from last execution') // mocha options .option('--colors', 'force enabling of colors') @@ -133,6 +134,7 @@ program.command('run-workers ') .option('-p, --plugins ', 'enable plugins, comma-separated') .option('-O, --reporter-options ', 'reporter-specific options') .option('-R, --reporter ', 'specify the reporter to use') + .option('--rerun-tests','to run the selected / failed tests from last execution') .action(require('../lib/command/run-workers')); program.command('run-multiple [suites...]') diff --git a/lib/codecept.js b/lib/codecept.js index d355f8ef4..e415e4d25 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -1,4 +1,5 @@ const { existsSync, readFileSync } = require('fs'); +const fs = require('fs'); const glob = require('glob'); const fsPath = require('path'); const { resolve } = require('path'); @@ -7,7 +8,8 @@ const container = require('./container'); const Config = require('./config'); const event = require('./event'); const runHook = require('./hooks'); -const output = require('./output'); +const {output,print} = require('./output'); +const FailedTestsRerun = require('./rerunFailed'); /** * CodeceptJS runner @@ -122,25 +124,34 @@ class Codecept { * * @param {string} [pattern] */ - loadTests(pattern) { + loadTests(pattern){ + let flag = 0; + let invalidTest = []; const options = { cwd: global.codecept_dir, }; - - let patterns = [pattern]; - if (!pattern) { - patterns = []; - if (this.config.tests && !this.opts.features) patterns.push(this.config.tests); - if (this.config.gherkin.features && !this.opts.tests) patterns.push(this.config.gherkin.features); + if(this.opts.rerunTests){ + print('Re-running The Selected/Failed Scripts ..'); + const failedTestRerun = new FailedTestsRerun(); + failedTestRerun.checkForFailedTestsExist(this.opts); + this.testFiles= JSON.parse(fs.readFileSync('failedCases.json', 'utf8')); + failedTestRerun.checkForFileExistence(this.testFiles); } - - for (pattern of patterns) { - glob.sync(pattern, options).forEach((file) => { - if (!fsPath.isAbsolute(file)) { - file = fsPath.join(global.codecept_dir, file); - } - this.testFiles.push(fsPath.resolve(file)); - }); + else { + let patterns = [pattern]; + if (!pattern) { + patterns = []; + if (this.config.tests && !this.opts.features) patterns.push(this.config.tests); + if (this.config.gherkin.features && !this.opts.tests) patterns.push(this.config.gherkin.features); + } + for (pattern of patterns) { + glob.sync(pattern, options).forEach((file) => { + if (!fsPath.isAbsolute(file)) { + file = fsPath.join(global.codecept_dir, file); + } + this.testFiles.push(fsPath.resolve(file)); + }); + } } } diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js index c578c39e4..07e56ccc1 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -4,6 +4,8 @@ const { tryOrDefault } = require('../utils'); const output = require('../output'); const event = require('../event'); const Workers = require('../workers'); +const FailedTestRerun = require('./../rerunFailed'); +const failedTestRerun = new FailedTestRerun(); module.exports = async function (workerCount, options) { satisfyNodeVersion( diff --git a/lib/command/run.js b/lib/command/run.js index d70972e0e..88a8ecaa5 100644 --- a/lib/command/run.js +++ b/lib/command/run.js @@ -3,6 +3,9 @@ const { } = require('./utils'); const Config = require('../config'); const Codecept = require('../codecept'); +const FailedTestRerun = require('../rerunFailed'); + +const failedTestRerun = new FailedTestRerun(); module.exports = async function (test, options) { // registering options globally to use in config @@ -19,7 +22,6 @@ module.exports = async function (test, options) { createOutputDir(config, testRoot); const codecept = new Codecept(config, options); - try { codecept.init(testRoot); await codecept.bootstrap(); diff --git a/lib/listener/exit.js b/lib/listener/exit.js index 2362acc33..37be0133b 100644 --- a/lib/listener/exit.js +++ b/lib/listener/exit.js @@ -2,12 +2,18 @@ const event = require('../event'); module.exports = function () { let failedTests = []; + let failedTestName = []; + let passedTest = []; + const FailedTestRerun = require('./../rerunFailed'); + const failedTestRerun = new FailedTestRerun(); event.dispatcher.on(event.test.failed, (testOrSuite) => { // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object // is a suite and not a test const id = testOrSuite.id || (testOrSuite.ctx && testOrSuite.ctx.test.id) || 'empty'; + const name = testOrSuite.file; failedTests.push(id); + failedTestName.push(name); }); // if test was successful after retries @@ -15,7 +21,9 @@ module.exports = function () { // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object // is a suite and not a test const id = testOrSuite.id || (testOrSuite.ctx && testOrSuite.ctx.test.id) || 'empty'; + const name = testOrSuite.file; failedTests = failedTests.filter(failed => id !== failed); + failedTestRerun.removePassedTests(name); }); event.dispatcher.on(event.all.result, () => { @@ -24,6 +32,12 @@ module.exports = function () { } }); + event.dispatcher.on(event.all.after, () => { + // Writes the Failed Test Names In The JSON File For Rerun + failedTestRerun.writeFailedTests(failedTestName); + failedTestRerun.checkAndRemoveFailedCasesFile(failedTestName) + }); + process.on('beforeExit', (code) => { if (code) { process.exit(code); diff --git a/lib/listener/steps.js b/lib/listener/steps.js index 660bf015b..d5ffb0200 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -3,6 +3,7 @@ const store = require('../store'); let currentTest; let currentHook; +let failedTests = new Array(0); /** * Register steps inside tests @@ -33,6 +34,12 @@ module.exports = function () { }); event.dispatcher.on(event.test.failed, () => { + //Getting Failed Test Scrips To Add in JSON File for Rerun + // @ts-ignore + let failedTestName = currentTest.file; + failedTests.push(failedTestName); + + const cutSteps = function (current) { const failureIndex = current.steps.findIndex(el => el.status === 'failed'); // To be sure that failed test will be failed in report diff --git a/lib/rerunFailed.js b/lib/rerunFailed.js new file mode 100644 index 000000000..e6143966a --- /dev/null +++ b/lib/rerunFailed.js @@ -0,0 +1,112 @@ +const fs = require('fs'); +const { print } = require('codeceptjs/lib/output'); + +class FailedTestsRerun { + /** + * Initial Check To Find the Existence Of Failed Cases JSON File + * and Content + * @param options + */ + checkForFailedTestsExist(options) { + if (options.rerunTests) { + if (!fs.existsSync('failedCases.json')) { + print('There Are No Failed Tests In Previous Execution'); + process.exit(); + } else { + const failedTests = this.getFailedTestContent(); + if (failedTests.length === 0 || failedTests.toString() === '') { + print('There Are No Files Present In the Failed Cases Json File'); + process.exit(); + } + } + } + } + + /** + * Checks If The Valid Files are Passed in Failed Cases Json File + *@param testFiles + */ + checkForFileExistence(testFiles) { + const invalidTest = []; + let flag = 0; + for (let i = 0; i < testFiles.length; i++) { + const path = testFiles[i]; + if (!fs.existsSync(path)) { + invalidTest.push(path); + flag++; + } + } + if (flag > 0) { + // eslint-disable-next-line no-useless-concat + print('\nInvalid File(s) Found :' + '\n'); + for (let j = 0; j < invalidTest.length; j++) { + print(invalidTest[j]); + } + process.exit(); + } + } + + /** + * Returns The Content In JSON File For File/Content existence Check + * @return {string[]} + */ + getFailedTestContent() { + return fs.readFileSync('failedCases.json', { encoding: 'utf8' }).split(','); + } + + /** + * Returns The Failed/Selected Tests + * @return {any} + */ + getFailedTests() { + return JSON.parse(fs.readFileSync('failedCases.json', 'utf8')); + } + + /** + * Writes The Failed Tests in The Failed Cases JSON File Post Execution + * @param failedTests + */ + writeFailedTests(failedTests) { + if (failedTests.length > 0) { + fs.writeFileSync('failedCases.json', JSON.stringify(failedTests), (err) => { + if (err) { return print(err); } + }); + } + } + + /** + * Removes The Passed Test From The Failed Cases JSON During the Execution + * Deletes the File When All Tests Are Passed + * @param passedTest + */ + removePassedTests(passedTest) { + if (fs.existsSync('failedCases.json')) { + const currentFile = JSON.parse(fs.readFileSync('failedCases.json', 'utf8')); + currentFile.forEach((t) => { + if (currentFile.includes(passedTest)) { + const index = currentFile.indexOf(t); + if (index > -1) { + currentFile.splice(index, 1); + } + } + }); + fs.writeFile('failedCases.json', JSON.stringify(currentFile), (err) => { + if (err) throw err; + }); + } + } + + /** + * To Remove The Failed Cases JSON file if No Tests Are There + * @param failedTests + */ + checkAndRemoveFailedCasesFile(failedTests) { + if (failedTests.length < 1) { + if (fs.existsSync('failedCases.json')) { + fs.unlinkSync('failedCases.json'); + } + } + } +} + +module.exports = FailedTestsRerun; diff --git a/lib/workers.js b/lib/workers.js index 8a908684e..43616c992 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -16,6 +16,14 @@ const recorder = require('./recorder'); const runHook = require('./hooks'); const WorkerStorage = require('./workerStorage'); +const FailedTestRerun = require('./rerunFailed'); + +const failedTestRerun = new FailedTestRerun(); + +const testsArrObj = []; +const failedTests = []; +const passedTests = []; + const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js'); const initializeCodecept = (configPath, options = {}) => { @@ -56,7 +64,6 @@ const createWorker = (workerObject) => { }, }); worker.on('error', err => output.error(`Worker Error: ${err.stack}`)); - WorkerStorage.addWorker(worker); return worker; }; @@ -241,6 +248,7 @@ class Workers extends EventEmitter { mocha.suite.eachTest((test) => { const i = groupCounter % groups.length; if (test) { + testsArrObj.push(test); const { id } = test; groups[i].push(id); groupCounter++; @@ -263,6 +271,7 @@ class Workers extends EventEmitter { const i = indexOfSmallestElement(groups); suite.tests.forEach((test) => { if (test) { + testsArrObj.push(test); const { id } = test; groups[i].push(id); } @@ -325,10 +334,16 @@ class Workers extends EventEmitter { break; case event.test.failed: this._updateFinishedTests(repackTest(message.data)); + // eslint-disable-next-line no-case-declarations + const failTest = testsArrObj.filter(t => t.id === message.data.id); + failedTests.push(failTest[0].file); this.emit(event.test.failed, repackTest(message.data)); break; case event.test.passed: this._updateFinishedTests(repackTest(message.data)); + // eslint-disable-next-line no-case-declarations + const passTest = testsArrObj.filter(t => t.id === message.data.id); + passedTests.push(passTest[0].file); this.emit(event.test.passed, repackTest(message.data)); break; case event.test.skipped: @@ -340,6 +355,7 @@ class Workers extends EventEmitter { this.emit(event.test.after, repackTest(message.data)); break; case event.all.after: + failedTestRerun.writeFailedTests(failedTests); this._appendStats(message.data); break; } });