Repository: Codeception/CodeceptJS (js)

Issue:
Feature Level Retires not being correctly reported with run-workers
#### What are you trying to achieve?
We have some flaky tests that we use the retry function in CodeceptJS for, and it looks like when we run the tests with run-workers and a test fails, but then passes on the retry, the failure is reported and the test fun fails completely.

#### What do you get instead?
When a retry is executed and subsequently passes, the run should succeed 

### Details

* CodeceptJS version: 2.3.3
* NodeJS Version:v12.4.0
* Operating System:Alpine Linux
* WebDriverIO: 5.15.1


Pull request description:
fix: Avoid failiure report on successful retry in worker
## Motivation/Description of the PR
Tests are not reported as passed if the test is passed in a retry while using run-workers.

While appending test status, passing of already failed test is not accounted for. So adding a map with `test.id` and appending status accordingly fixes the issue.

- Fixes #1985 

## Type of change

- [ ] Breaking changes
- [ ] New functionality
- [X] Bug fix
- [ ] Documentation changes/updates
- [ ] Hot fix
- [ ] Markdown files fix - not related to source code

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [X] Tests have been added
- [ ] Documentation has been added (Run `npm run docs`)
- [ ] Lint checking (Run `npm run lint`)
- [ ] Local tests are passed (Run `npm test`)

Patch:
diff --git a/.travis.yml b/.travis.yml
index 44f65d5d..0d059486 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
 language: node_js
 node_js:
-- 8
+- 11
 - 12
 sudo: required
 dist: trusty
@@ -26,8 +26,9 @@ before_script:
 - docker run -d --net=host --shm-size=2g selenium/standalone-chrome:3.141.59-oxygen
 - export DBUS_SESSION_BUS_ADDRESS=/dev/null
 - export DISPLAY=:99.0
+- sleep 3
 - chmod -R 777 test/data
 - php -S 127.0.0.1:8000 -t test/data/app >/dev/null 2>&1 &
 script:
-- npm test
-- './node_modules/.bin/mocha test/helper/${HELPER}_test.js'
+- npm i mocha-parallel-tests
+- 'travis_wait ./node_modules/.bin/mocha-parallel-tests test/helper/${HELPER}_test.js'
diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js
index bf1a1e2d..938eb6b4 100644
--- a/lib/command/run-workers.js
+++ b/lib/command/run-workers.js
@@ -22,7 +22,7 @@ const stats = {
 let numberOfWorkersClosed = 0;
 const hasFailures = () => stats.failures || errors.length;
 const pathToWorker = path.join(__dirname, 'workers', 'runTests.js');
-const finishedTests = [];
+const finishedTests = {};
 const errors = [];
 
 module.exports = function (workers, options) {
@@ -87,10 +87,12 @@ module.exports = function (workers, options) {
 
       switch (message.event) {
         case event.test.failed:
-          finishedTests.push(repackTest(message.data));
+          updateFinishedTests(repackTest(message.data));
           output.test.failed(repackTest(message.data));
           break;
-        case event.test.passed: output.test.passed(repackTest(message.data)); break;
+        case event.test.passed: output.test.passed(repackTest(message.data));
+          updateFinishedTests(repackTest(message.data));
+          break;
         case event.suite.before: output.suite.started(message.data); break;
         case event.all.after: appendStats(message.data); break;
       }
@@ -125,7 +127,10 @@ function printResults() {
   if (stats.failures) {
     output.print();
     output.print('-- FAILURES:');
-    Base.list(finishedTests.filter(t => t.err));
+    const failedList = Object.keys(finishedTests)
+      .filter(key => finishedTests[key].err)
+      .map(key => finishedTests[key]);
+    Base.list(failedList);
   }
 }
 
@@ -209,3 +214,12 @@ function simplifyObject(object) {
       return obj;
     }, {});
 }
+
+const updateFinishedTests = (test) => {
+  const { id } = test;
+  if (finishedTests[id]) {
+    const stats = { passes: 0, failures: -1, tests: 0 };
+    appendStats(stats);
+  }
+  finishedTests[id] = test;
+};
diff --git a/package.json b/package.json
index 9f85ecc5..896aed90 100644
--- a/package.json
+++ b/package.json
@@ -121,7 +121,8 @@
     "wdio-docker-service": "^1.5.0",
     "webdriverio": "^5.18.4",
     "xmldom": "^0.1.31",
-    "xpath": "0.0.27"
+    "xpath": "0.0.27",
+    "mocha-parallel-tests": "^2.2.2"
   },
   "engines": {
     "node": ">=8.9.1",


Test patch:
diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js
index ba66ebee..12372f17 100644
--- a/lib/command/workers/runTests.js
+++ b/lib/command/workers/runTests.js
@@ -78,6 +78,7 @@ function initializeListeners() {
     }
 
     return {
+      id: test.id,
       workerIndex,
       title: test.title,
       status: test.status,
diff --git a/test/data/sandbox/workers/retry_test.workers.js b/test/data/sandbox/workers/retry_test.workers.js
new file mode 100644
index 00000000..2cbf435b
--- /dev/null
+++ b/test/data/sandbox/workers/retry_test.workers.js
@@ -0,0 +1,10 @@
+const assert = require('assert');
+
+Feature('Retry Workers');
+
+let tries = 0;
+
+Scenario('retry a test', { retries: 2 }, (I) => {
+  tries++;
+  assert.equal(tries, 2);
+});
diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js
index a9a63bc7..1cd05074 100644
--- a/test/runner/run_workers_test.js
+++ b/test/runner/run_workers_test.js
@@ -102,4 +102,13 @@ describe('CodeceptJS Workers Runner', function () {
       done();
     });
   });
+
+  it('should retry test', function (done) {
+    if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version');
+    exec(`${codecept_run} 2 --grep "retry"`, (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('OK  | 1 passed');
+      done();
+    });
+  });
 });