Repository: mochajs/mocha

Problem statement:
Mocha runner throws if test.pending is set to true in the 'test' event
I have a script that hooks on to mocha events and dynamically determines which tests to run at runtime. I do this by setting `args.pending = true` in the `test` hook. 7.0.0 seems to break this behavior.

Here's a sample repro
```javascript
function mochaRun() {

    const mochaLib = require('mocha');
    mochaLib.Suite.prototype.beforeAll = () => { };
    mochaLib.Suite.prototype.afterAll = () => { };
    mochaLib.Suite.prototype.beforeEach = () => { };
    mochaLib.Suite.prototype.afterEach = () => { };

    this.mocha = new mochaLib({});

    this.mocha.addFile("test.js");

    const runner = this.mocha.run();
    
    runner.setMaxListeners(20);

    runner.on('test', (args) => { args.pending = true; });
}

// Use domain to catch the error in mocha
const domain = require('domain');
const executionDomain = domain.create();

executionDomain.on('error', (err) => {
    console.error(err);
});

executionDomain.run(() => {
    mochaRun()
});
```

with sample `test.js`:
```javascript
const assert = require('assert');
describe('suite a', () => {
    it('test case a1', () => {

    });
    it('test case a2', () => {
        assert.fail('failure');
    });
})
```

On running this mocha throws with the error `Cannot read property 'parent' of undefined`

```
  suite a
    - test case a1
TypeError: Cannot read property 'parent' of undefined
    at Runner.parents (E:\mocharepro\node_modules\mocha\lib\runner.js:501:16)
    at Runner.hookUp (E:\mocharepro\node_modules\mocha\lib\runner.js:475:41)
    at E:\mocharepro\node_modules\mocha\lib\runner.js:658:21
    at next (E:\mocharepro\node_modules\mocha\lib\runner.js:450:14)
    at E:\mocharepro\node_modules\mocha\lib\runner.js:460:7
    at next (E:\mocharepro\node_modules\mocha\lib\runner.js:362:14)
    at Immediate.<anonymous> (E:\mocharepro\node_modules\mocha\lib\runner.js:428:5)
    at processImmediate (internal/timers.js:439:21)
    at process.topLevelDomainCallback (domain.js:130:23) {
  domainThrown: true
}
```

If this is now the expected behavior how do I get around it and dynamically determine which tests to skip.


Hints:
@karanjitsingh could you please patch [following line](https://github.com/mochajs/mocha/blob/master/lib/runner.js#L657) in "lib/runner.js":
- old: `self.suite = errSuite;`
- new: `self.suite = errSuite || self.suite;`
@karanjitsingh our CI tests haven't covered this test scenario, since they didn't fail in the past. So if you just open a PR with exactly my proposed line, our CI test have no significance, they don't prove wether the bug is fixed or not.
- either you test this patch in your environment and tell me wether it works or  not
- or you add a test to your PR which covers this scenario
- or you do both items above
@juergba, Not sure how to test this change, if there is a test for that block of code please point me to it. Let me know if this needs to be a unit / functional test with helpful code pointers.

I can confirm that the change is working for me.

![image](https://user-images.githubusercontent.com/4632805/72778464-17e96780-3c3f-11ea-81bf-13d423085b92.png)


Patch:
diff --git a/lib/runner.js b/lib/runner.js
--- a/lib/runner.js
+++ b/lib/runner.js
@@ -654,7 +654,7 @@ Runner.prototype.runTests = function(suite, fn) {
         self.emit(constants.EVENT_TEST_END, test);
         // skip inner afterEach hooks below errSuite level
         var origSuite = self.suite;
-        self.suite = errSuite;
+        self.suite = errSuite || self.suite;
         return self.hookUp(HOOK_TYPE_AFTER_EACH, function(e, eSuite) {
           self.suite = origSuite;
           next(e, eSuite);


Test patch:
diff --git a/test/integration/fixtures/pending/programmatic.fixture.js b/test/integration/fixtures/pending/programmatic.fixture.js
new file mode 100644
--- /dev/null
+++ b/test/integration/fixtures/pending/programmatic.fixture.js
@@ -0,0 +1,8 @@
+'use strict';
+const Mocha = require('../../../../lib/mocha');
+
+const mocha = new Mocha({reporter: 'json'});
+mocha.addFile("./test/integration/fixtures/__default__.fixture.js");
+
+const runner = mocha.run();
+runner.on('test', function (test) { test.pending = true; });
diff --git a/test/integration/helpers.js b/test/integration/helpers.js
--- a/test/integration/helpers.js
+++ b/test/integration/helpers.js
@@ -143,6 +143,8 @@ module.exports = {
 
   invokeMochaAsync: invokeMochaAsync,
 
+  invokeNode: invokeNode,
+
   /**
    * Resolves the path to a fixture to the full path.
    */
@@ -227,6 +229,19 @@ function invokeMochaAsync(args, opts) {
   return [mochaProcess, resultPromise];
 }
 
+/**
+ * Invokes Node without Mocha binary with the given arguments,
+ * when Mocha is used programmatically.
+ */
+function invokeNode(args, fn, opts) {
+  if (typeof args === 'function') {
+    opts = fn;
+    fn = args;
+    args = [];
+  }
+  return _spawnMochaWithListeners(args, fn, opts);
+}
+
 function invokeSubMocha(args, fn, opts) {
   if (typeof args === 'function') {
     opts = fn;
diff --git a/test/integration/pending.spec.js b/test/integration/pending.spec.js
--- a/test/integration/pending.spec.js
+++ b/test/integration/pending.spec.js
@@ -1,9 +1,12 @@
 'use strict';
 
 var assert = require('assert');
-var run = require('./helpers').runMochaJSON;
-var runMocha = require('./helpers').runMocha;
-var splitRegExp = require('./helpers').splitRegExp;
+var helpers = require('./helpers');
+var run = helpers.runMochaJSON;
+var runMocha = helpers.runMocha;
+var splitRegExp = helpers.splitRegExp;
+var invokeNode = helpers.invokeNode;
+var toJSONRunResult = helpers.toJSONRunResult;
 var args = [];
 
 describe('pending', function() {
@@ -323,4 +326,21 @@ describe('pending', function() {
       });
     });
   });
+
+  describe('programmatic usage', function() {
+    it('should skip the test by listening to test event', function(done) {
+      var path = require.resolve('./fixtures/pending/programmatic.fixture.js');
+      invokeNode([path], function(err, res) {
+        if (err) {
+          return done(err);
+        }
+        var result = toJSONRunResult(res);
+        expect(result, 'to have passed')
+          .and('to have passed test count', 0)
+          .and('to have pending test count', 1)
+          .and('to have pending test order', 'should succeed');
+        done();
+      });
+    });
+  });
 });