Repository: ember-cli/ember-cli
Pull request #7086 (2017-06-03 01:05:03)

Mock process
Solves #7073 

This pr introduces `MockProcess` test helper. `MockProcess` and `MockProcess.stdin` both inherit `EventEmitter`. Non-`EventEmitter` methods are implemented as `testdouble.function()`s.

It allows to test `process.exit`, `SIGINT`, `CTRL+C` behaviors in a more safe and flexible way.
```js
let process = new MockProcess({
  platform: 'win',
  stdin: {
    isTTY: true,
  },
});

willInterruptProcess.capture(process);

process.stdin.emit('data', [0x03]); // CTRL+C

td.verify(process.exit());
```

**General Changes:**

- `will-interrupt-process` now gets new ~~public~~ method `.capture(EventEmitter process)`
- `cli/index` captures the process once it is invoked
- `tests/helpers/ember` now passes `MockProcess` instance to the `cli/index`


**Todo:**

 - [x] unit tests for new ~~public~~ api in `will-interrupt-process`
 - [x] collapse/rework `.free()` and `.teardown()`
 - [x] is it acceptable to [move `process` capture from the top of the `cli/index` to the invocation](https://github.com/ember-cli/ember-cli/pull/7086/files#diff-3386b9e4d97ad97b803f4678beb66c5aR70)? and why not. explanation - https://github.com/ember-cli/ember-cli/pull/7086#discussion_r121495275
 - [ ] ~~merge #7019~~ now i think if this pr lands first it can improve test setup in #7019.
 - [ ] ~~merge #7041~~ now i think if this pr lands first it can improve test setup in #7041.
 - [x] rebase
 - [ ] optional. expand build task integration test to cover builder interruption/cleanup

ro0gr: @stefanpenner thank you for review. I'll address your items soon. 

Currently I'm blocked with the smoke tests on my local machine. Some of them are failing even in the master with a strange message:
>  1) Acceptance: addon-smoke-test "before all" hook
Directory 'C:\\Users\\ro0gr\\workspace\\ember-cli\\tmp\\some_cool_addon-8kyFSwDn.tmp' already exists

While debugging I can see that [`createTestTargets` creates a temp dir](https://github.com/ember-cli/ember-cli/blob/master/tests/helpers/acceptance.js#L54) which then is used in the [`ember addon`](https://github.com/ember-cli/ember-cli/blob/master/tests/helpers/acceptance.js#L59) command. This `ember addon` command throws an exception mentioned above which seems reasonable because we try to create an `addon` in existing directory. At this moment I can't figure out what exactly trolls me so hard. Investigating...

While I'm investigating this I'm also making some [suspicious things](https://github.com/ember-cli/ember-cli/pull/7086/files#diff-3386b9e4d97ad97b803f4678beb66c5aR68) in this pr in order to fix smoke slow test failures on CI. 

stefanpenner: re-running appveyor
ro0gr: I believe this pr is ready for review and good to go in its current state. 
ro0gr: I've tried to improve docs but it's still not perfect. Would like any suggestion for improvement
stefanpenner: trying appveyor again ;)

Patch:
diff --git a/lib/cli/index.js b/lib/cli/index.js
index 01b2ef6fa..9edd455dc 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -1,5 +1,6 @@
 'use strict';
 
+const willInterruptProcess = require('../utilities/will-interrupt-process');
 const instrumentation = require('../utilities/instrumentation');
 
 let initInstrumentation;
@@ -12,10 +13,6 @@ if (instrumentation.instrumentationEnabled()) {
   };
 }
 
-// work around misbehaving libraries, so we can correctly cleanup before
-// actually exiting.
-require('capture-exit').captureExit();
-
 // Main entry point
 const requireAsHash = require('../utilities/require-as-hash');
 const packageConfig = require('../../package.json');
@@ -24,15 +21,6 @@ const merge = require('ember-cli-lodash-subset').merge;
 const path = require('path');
 const heimdall = require('heimdalljs');
 
-// ember-cli and user apps have many dependencies, many of which require
-// process.addListener('exit', ....) for cleanup, by default this limit for
-// such listeners is 10, recently users have been increasing this and not to
-// their fault, rather they are including large and more diverse sets of
-// node_modules.
-//
-// https://github.com/babel/ember-cli-babel/issues/76
-process.setMaxListeners(1000);
-
 let version = packageConfig.version;
 let name = packageConfig.name;
 let trackingCode = packageConfig.trackingCode;
@@ -75,8 +63,12 @@ function configureLogger(env) {
   }
 }
 
-// Options: Array cliArgs, Stream inputStream, Stream outputStream
+// Options: Array cliArgs, Stream inputStream, Stream outputStream, EventEmitter process
 module.exports = function(options) {
+  // `process` should be captured before we require any libraries which
+  // may use `process.exit` work arounds for async cleanup.
+  willInterruptProcess.capture(options.process || process);
+
   let UI = options.UI || require('console-ui');
   let Yam = options.Yam || require('yam');
   const CLI = require('./cli');
@@ -150,5 +142,5 @@ module.exports = function(options) {
     settings: merge(defaultUpdateCheckerOptions, config.getAll()),
   };
 
-  return cli.run(environment);
+  return cli.run(environment).finally(() => willInterruptProcess.release());
 };
diff --git a/lib/utilities/will-interrupt-process.js b/lib/utilities/will-interrupt-process.js
index d1714ae1c..ac7e35f59 100644
--- a/lib/utilities/will-interrupt-process.js
+++ b/lib/utilities/will-interrupt-process.js
@@ -1,17 +1,71 @@
 'use strict';
 
-// (un)Register process interruption signals(SIGINT, SIGTERM, process.kill()) handlers.
+// Allows to setup process interruption handlers.
+// The process can be interrupted when ges SIGINT, SIGTERM signal,
+// something called process.exit(exitCode) or CTRL+C was pressed.
 //
-// When the process interrupted("CTR+C") all the registered handlers are being invoked via `capture-exit`.
-// The process finishes after the last interruption handler is settled.
+// Node.js doesn't allow to perform async tasks when the process is exiting.
+// Also there are some work arounds for exit in the node.js ecosystem.
+//
+// In order to supply reliable process exit phase, `will-interrupt-process`
+// is tightly integrated with `capture-exit` which allows us to perform async cleanup
+// on `process.exit()` and control the final exit code.
+
+const captureExit = require('capture-exit');
+const EventEmitter = require('events');
 
-let exit = require('capture-exit');
-exit.captureExit();
+const handlers = [];
 
-let windowsCtrlCTrap, originalIsRaw;
-let handlers = [];
+let _process,
+    windowsCtrlCTrap,
+    originalIsRaw;
 
 module.exports = {
+  capture(outerProcess) {
+    if (_process) {
+      throw new Error('process already captured');
+    }
+
+    if (outerProcess instanceof EventEmitter === false) {
+      throw new Error('attempt to capture bad process instance');
+    }
+
+    _process = outerProcess;
+
+    // ember-cli and user apps have many dependencies, many of which require
+    // process.addListener('exit', ....) for cleanup, by default this limit for
+    // such listeners is 10, recently users have been increasing this and not to
+    // their fault, rather they are including large and more diverse sets of
+    // node_modules.
+    //
+    // https://github.com/babel/ember-cli-babel/issues/76
+    _process.setMaxListeners(1000);
+
+    // work around misbehaving libraries, so we can correctly cleanup before actually exiting.
+    captureExit.captureExit();
+  },
+
+  /**
+   * Drops all the interruption handlers and disables an ability to add new one
+   *
+   * Note: We don't call `captureExit.releaseExit() here.
+   * In some rare scenarios it can lead to the hard to debug issues.
+   * see: https://github.com/ember-cli/ember-cli/issues/6779#issuecomment-280940358
+   *
+   * We can more or less feel comfortable with a captured exit because it behaves very
+   * similar to the original `exit` except of cases when we need to do cleanup before exit.
+   *
+   * @private
+   * @method release
+   */
+  release() {
+    while (handlers.length > 0) {
+      this.removeHandler(handlers[0]);
+    }
+
+    _process = null;
+  },
+
   /**
    * Add process interruption handler
    *
@@ -23,6 +77,10 @@ module.exports = {
    * @param {function} cb   Callback to be called when process interruption fired
    */
   addHandler(cb) {
+    if (!_process) {
+      throw new Error('process is not captured');
+    }
+
     let index = handlers.indexOf(cb);
     if (index > -1) { return; }
 
@@ -31,7 +89,7 @@ module.exports = {
     }
 
     handlers.push(cb);
-    exit.onExit(cb);
+    captureExit.onExit(cb);
   },
 
   /**
@@ -49,24 +107,12 @@ module.exports = {
     if (index < 0) { return; }
 
     handlers.splice(index, 1);
-    exit.offExit(cb);
+    captureExit.offExit(cb);
 
     if (handlers.length === 0) {
       teardownSignalsTrap();
     }
   },
-
-  /**
-   * Tears down all the handlers and cleans up all signal listeners
-   *
-   * @private
-   * @method teardown
-   */
-  teardown() {
-    while (handlers.length > 0) {
-      this.removeHandler(handlers[0]);
-    }
-  },
 };
 
 /**
@@ -78,12 +124,12 @@ module.exports = {
  * @method setupSignalsTrap
  */
 function setupSignalsTrap() {
-  process.on('SIGINT', _exit);
-  process.on('SIGTERM', _exit);
-  process.on('message', onMessage);
+  _process.on('SIGINT', exit);
+  _process.on('SIGTERM', exit);
+  _process.on('message', onMessage);
 
-  if (isWindowsTTY()) {
-    trapWindowsSignals();
+  if (isWindowsTTY(_process)) {
+    trapWindowsSignals(_process);
   }
 }
 
@@ -93,12 +139,12 @@ function setupSignalsTrap() {
  * @method teardownSignalsTrap
  */
 function teardownSignalsTrap() {
-  process.removeListener('SIGINT', _exit);
-  process.removeListener('SIGTERM', _exit);
-  process.removeListener('message', onMessage);
+  _process.removeListener('SIGINT', exit);
+  _process.removeListener('SIGTERM', exit);
+  _process.removeListener('message', onMessage);
 
-  if (isWindowsTTY()) {
-    cleanupWindowsSignals();
+  if (isWindowsTTY(_process)) {
+    cleanupWindowsSignals(_process);
   }
 }
 
@@ -107,8 +153,8 @@ function teardownSignalsTrap() {
  *
  * @method trapWindowsSignals
  */
-function trapWindowsSignals() {
-  const stdin = process.stdin;
+function trapWindowsSignals(_process) {
+  const stdin = _process.stdin;
 
   originalIsRaw = stdin.isRaw;
 
@@ -117,37 +163,30 @@ function trapWindowsSignals() {
 
   windowsCtrlCTrap = function(data) {
     if (data.length === 1 && data[0] === 0x03) {
-      process.emit('SIGINT');
+      _process.emit('SIGINT');
     }
   };
   stdin.on('data', windowsCtrlCTrap);
 }
 
-function cleanupWindowsSignals() {
-  const stdin = process.stdin;
+function cleanupWindowsSignals(_process) {
+  const stdin = _process.stdin;
 
   stdin.setRawMode(originalIsRaw);
 
   stdin.removeListener('data', windowsCtrlCTrap);
 }
 
-function isWindowsTTY() {
-  return (/^win/).test(process.platform) && process.stdin && process.stdin.isTTY;
+function isWindowsTTY(_process) {
+  return (/^win/).test(_process.platform) && _process.stdin && _process.stdin.isTTY;
+}
+
+function exit() {
+  _process.exit();
 }
 
-/**
- * Handles the `message` event on the `process`.
- *
- * Calls `process.exit` if the `kill` property on the `message` is set.
- *
- * @method onMessage
- */
 function onMessage(message) {
   if (message.kill) {
-    _exit();
+    exit();
   }
 }
-
-function _exit() {
-  process.exit();
-}


Test patch:
diff --git a/tests/helpers/ember.js b/tests/helpers/ember.js
index 2450c0c9e..7c90f46e3 100644
--- a/tests/helpers/ember.js
+++ b/tests/helpers/ember.js
@@ -3,6 +3,7 @@
 const MockUI = require('console-ui/mock');
 const MockAnalytics = require('./mock-analytics');
 const cli = require('../../lib/cli');
+const MockProcess = require('./mock-process');
 const path = require('path');
 
 /*
@@ -65,6 +66,7 @@ module.exports = function ember(args, options) {
   }
 
   cliInstance = cli({
+    process: new MockProcess(),
     inputStream,
     outputStream,
     errorLog,
diff --git a/tests/helpers/mock-process.js b/tests/helpers/mock-process.js
new file mode 100644
index 000000000..5476c4013
--- /dev/null
+++ b/tests/helpers/mock-process.js
@@ -0,0 +1,36 @@
+'use strict';
+
+let td = require('testdouble');
+let EventEmitter = require('events');
+
+module.exports = class FakeProcess extends EventEmitter {
+  constructor(options) {
+    super();
+
+    options = options || {};
+
+    const stdin = Object.assign(new EventEmitter(), {
+      isRaw: process.stdin.isRaw,
+      setRawMode: td.function(),
+    }, options.stdin || {});
+
+    td.when(stdin.setRawMode(td.matchers.anything())).thenDo(function(flag) {
+      this.isRaw = flag;
+    });
+
+    const topLevelProps = Object.assign({
+      platform: 'MockOS',
+      exit: td.function(),
+    }, options);
+
+    Object.assign(this, topLevelProps, { stdin });
+  }
+
+  getSignalListenerCounts() {
+    return {
+      SIGINT: this.listenerCount('SIGINT'),
+      SIGTERM: this.listenerCount('SIGTERM'),
+      message: this.listenerCount('message'),
+    };
+  }
+};
diff --git a/tests/integration/tasks/build-test.js b/tests/integration/tasks/build-test.js
index 69e28b2db..c9ff31f24 100644
--- a/tests/integration/tasks/build-test.js
+++ b/tests/integration/tasks/build-test.js
@@ -10,16 +10,20 @@ const BuildTask = require('../../../lib/tasks/build');
 const RSVP = require('rsvp');
 const MockProject = require('../../helpers/mock-project');
 const MockAnalytics = require('../../helpers/mock-analytics');
+const MockProcess = require('../../helpers/mock-process');
 const copyFixtureFiles = require('../../helpers/copy-fixture-files');
 const mkTmpDirIn = require('../../../lib/utilities/mk-tmp-dir-in');
+const willInterruptProcess = require('../../../lib/utilities/will-interrupt-process');
 let remove = RSVP.denodeify(fs.remove);
 let root = process.cwd();
 let tmproot = path.join(root, 'tmp');
 
 describe('build task test', function() {
-  let project, ui;
+  let project, ui, _process;
 
   beforeEach(function() {
+    _process = new MockProcess();
+    willInterruptProcess.capture(_process);
     return mkTmpDirIn(tmproot)
       .then(function(tmpdir) {
         process.chdir(tmpdir);
@@ -34,6 +38,7 @@ describe('build task test', function() {
   });
 
   afterEach(function() {
+    willInterruptProcess.release();
     process.chdir(root);
     delete process.env.BROCCOLI_VIZ;
 
diff --git a/tests/unit/utilities/will-interrupt-process-test.js b/tests/unit/utilities/will-interrupt-process-test.js
index 330425510..601062fad 100644
--- a/tests/unit/utilities/will-interrupt-process-test.js
+++ b/tests/unit/utilities/will-interrupt-process-test.js
@@ -1,88 +1,132 @@
 'use strict';
 
-let willInterruptProcess;
+let willInterruptProcess = require('../../../lib/utilities/will-interrupt-process');
+let MockProcess = require('../../helpers/mock-process');
 let captureExit;
 
 let td = require('testdouble');
 let chai = require('../../chai');
 let expect = chai.expect;
-let EventEmitter = require('events');
-
-function getSignalListenersCount(emitter, event) {
-  if (emitter.listenerCount) { // Present in Node >= 4.0
-    return emitter.listenerCount(event);
-  } else {
-    // deprecated in Node 4.0
-    return EventEmitter.listenerCount(emitter, event);
-  }
-}
-
-function getSignalListenerCounts() {
-  return {
-    SIGINT: getSignalListenersCount(process, 'SIGINT'),
-    SIGTERM: getSignalListenersCount(process, 'SIGTERM'),
-    message: getSignalListenersCount(process, 'message'),
-  };
-}
 
 describe('will interrupt process', function() {
   let cb;
   beforeEach(function() {
-    willInterruptProcess = require('../../../lib/utilities/will-interrupt-process');
     captureExit = require('capture-exit');
     cb = td.function();
   });
 
   afterEach(function() {
-    willInterruptProcess.teardown();
+    willInterruptProcess.release();
   });
 
-  describe('capture-exit integration', function() {
-    let originalExitHandlersCount;
+  describe('capture', function() {
+    it('throws if already captured', function() {
+      const mockProcess = new MockProcess();
+
+      willInterruptProcess.capture(mockProcess);
+      try {
+        willInterruptProcess.capture(mockProcess);
+        expect(true).to.equal(false);
+      } catch (e) {
+        expect(e.message).to.equal('process already captured');
+      }
+    });
 
-    beforeEach(function() {
-      originalExitHandlersCount = captureExit.listenerCount();
+    it('throws if the process is not an EventEmitter instance', function() {
+      const dissallowedArgs = [null, true, '', [], {}];
+
+      dissallowedArgs.forEach(arg => {
+        try {
+          willInterruptProcess.capture(arg);
+          expect(true).to.equal(false);
+        } catch (e) {
+          expect(e.message).to.equal('attempt to capture bad process instance');
+        }
+      });
+    });
+
+    it('sets process maxListeners count', function() {
+      const mockProcess = new MockProcess();
+
+      willInterruptProcess.capture(mockProcess);
+
+      expect(mockProcess.getMaxListeners()).to.equal(1000);
+    });
+  });
+
+  describe('addHandler', function() {
+    it('throws if process is not captured', function() {
+      try {
+        willInterruptProcess.addHandler(() => {});
+        expect(true).to.equal(false);
+      } catch (e) {
+        expect(e.message).to.equal('process is not captured');
+      }
+
+      const mockProcess = new MockProcess();
+
+      willInterruptProcess.capture(mockProcess);
+      willInterruptProcess.release();
     });
 
+    it('throws if process is released', function() {
+      willInterruptProcess.capture(new MockProcess());
+      willInterruptProcess.release();
+
+      try {
+        willInterruptProcess.addHandler(() => {});
+        expect(true).to.equal(false);
+      } catch (e) {
+        expect(e.message).to.equal('process is not captured');
+      }
+    });
+  });
+
+  describe('capture-exit', function() {
     it('adds exit handler', function() {
+      willInterruptProcess.capture(new MockProcess());
       willInterruptProcess.addHandler(cb);
 
-      expect(captureExit.listenerCount()).to.equal(originalExitHandlersCount + 1);
+      expect(captureExit.listenerCount()).to.equal(1);
     });
 
     it('removes exit handler', function() {
+      willInterruptProcess.capture(new MockProcess());
       willInterruptProcess.addHandler(cb);
       willInterruptProcess.addHandler(function() {});
 
       willInterruptProcess.removeHandler(cb);
 
-      expect(captureExit.listenerCount()).to.equal(originalExitHandlersCount + 1);
+      expect(captureExit.listenerCount()).to.equal(1);
     });
 
     it('removes all exit handlers', function() {
+      willInterruptProcess.capture(new MockProcess());
+
       willInterruptProcess.addHandler(cb);
       willInterruptProcess.addHandler(function() {});
 
-      willInterruptProcess.teardown();
+      willInterruptProcess.release();
 
-      expect(captureExit.listenerCount()).to.equal(originalExitHandlersCount);
+      expect(captureExit.listenerCount()).to.equal(0);
     });
   });
 
   describe('process interruption signal listeners', function() {
-    let originalSignalListenersCounts;
+    let process;
 
     beforeEach(function() {
-      originalSignalListenersCounts = getSignalListenerCounts();
+      process = new MockProcess();
+      willInterruptProcess.capture(process);
     });
 
-    it('sets up interruption signal listeners when then first handler added', function() {
+    it('sets up interruption signal listeners when the first handler added', function() {
       willInterruptProcess.addHandler(cb);
 
-      expect(getSignalListenerCounts()).to.eql({
-        SIGINT: originalSignalListenersCounts.SIGINT + 1,
-        SIGTERM: originalSignalListenersCounts.SIGTERM + 1,
-        message: originalSignalListenersCounts.message + 1,
+      expect(process.getSignalListenerCounts()).to.eql({
+        SIGINT: 1,
+        SIGTERM: 1,
+        message: 1,
       });
     });
 
@@ -90,22 +134,25 @@ describe('will interrupt process', function() {
       willInterruptProcess.addHandler(cb);
       willInterruptProcess.addHandler(function() {});
 
-      expect(getSignalListenerCounts()).to.eql({
-        SIGINT: originalSignalListenersCounts.SIGINT + 1,
-        SIGTERM: originalSignalListenersCounts.SIGTERM + 1,
-        message: originalSignalListenersCounts.message + 1,
+      expect(process.getSignalListenerCounts()).to.eql({
+        SIGINT: 1,
+        SIGTERM: 1,
+        message: 1,
       });
     });
 
-    it('cleans up interruption signal listeners', function() {
-      willInterruptProcess.addHandler(cb);
+    it('cleans up interruption signal listener', function() {
       // will-interrupt-process doesn't have any public API to get actual handlers count
       // so here we make a side test to ensure that we don't add the same callback twice
       willInterruptProcess.addHandler(cb);
 
       willInterruptProcess.removeHandler(cb);
 
-      expect(getSignalListenerCounts()).to.eql(originalSignalListenersCounts);
+      expect(process.getSignalListenerCounts()).to.eql({
+        SIGINT: 0,
+        SIGTERM: 0,
+        message: 0,
+      });
     });
 
     it(`doesn't clean up interruption signal listeners if there are remaining handlers`, function() {
@@ -114,10 +161,10 @@ describe('will interrupt process', function() {
 
       willInterruptProcess.removeHandler(cb);
 
-      expect(getSignalListenerCounts()).to.eql({
-        SIGINT: originalSignalListenersCounts.SIGINT + 1,
-        SIGTERM: originalSignalListenersCounts.SIGTERM + 1,
-        message: originalSignalListenersCounts.message + 1,
+      expect(process.getSignalListenerCounts()).to.eql({
+        SIGINT: 1,
+        SIGTERM: 1,
+        message: 1,
       });
     });
 
@@ -126,102 +173,85 @@ describe('will interrupt process', function() {
       willInterruptProcess.addHandler(function() {});
       willInterruptProcess.addHandler(() => cb);
 
-      willInterruptProcess.teardown();
+      willInterruptProcess.release();
 
-      expect(getSignalListenerCounts()).to.eql(originalSignalListenersCounts);
+      expect(process.getSignalListenerCounts()).to.eql({
+        SIGINT: 0,
+        SIGTERM: 0,
+        message: 0,
+      });
     });
   });
 
   describe('Windows CTRL + C Capture', function() {
-    let originalPlatform, originalStdin;
-
-    before(function() {
-      originalPlatform = process.platform;
-      originalStdin = process.platform;
-    });
-
-    after(function() {
-      Object.defineProperty(process, 'platform', {
-        value: originalPlatform,
-      });
-
-      Object.defineProperty(process, 'stdin', {
-        value: originalStdin,
-      });
-    });
-
-    it('enables raw capture on Windows', function() {
-      Object.defineProperty(process, 'platform', {
-        value: 'win',
-      });
-
-      Object.defineProperty(process, 'stdin', {
-        value: {
+    it('exits on CTRL+C when TTY', function() {
+      let process = new MockProcess({
+        platform: 'win',
+        stdin: {
           isTTY: true,
-          on: td.function(),
-          setRawMode: td.function(),
-          removeListener: td.function(),
         },
       });
 
-      let windowsCtrlCTrap = td.matchers.isA(Function);
-
+      willInterruptProcess.capture(process);
       willInterruptProcess.addHandler(cb);
-      td.verify(process.stdin.on('data', windowsCtrlCTrap));
+
+      process.stdin.emit('data', [0x03]);
+
+      td.verify(process.exit());
     });
 
     it('adds and reverts rawMode on Windows', function() {
-      Object.defineProperty(process, 'platform', {
-        value: 'win',
-      });
-
-      Object.defineProperty(process, 'stdin', {
-        value: {
+      const process = new MockProcess({
+        platform: 'win',
+        stdin: {
           isRaw: false,
           isTTY: true,
-          on: td.function(),
-          setRawMode: td.function(),
-          removeListener: td.function(),
         },
       });
 
+      willInterruptProcess.capture(process);
+
       willInterruptProcess.addHandler(cb);
-      td.verify(process.stdin.setRawMode(true));
+      expect(process.stdin.isRaw).to.equal(true);
 
       willInterruptProcess.removeHandler(cb);
-      td.verify(process.stdin.setRawMode(false));
-
-      td.verify(process.stdin.setRawMode(), {
-        ignoreExtraArgs: true,
-        times: 2,
-      });
+      expect(process.stdin.isRaw).to.equal(false);
     });
 
-    it('does not enable raw capture on non-Windows', function() {
-      Object.defineProperty(process, 'platform', {
-        value: 'mockOS',
-      });
-
-      Object.defineProperty(process, 'stdin', {
-        value: {
+    it('does not enable raw capture when not a Windows', function() {
+      const process = new MockProcess({
+        stdin: {
           isTTY: true,
-          on: td.function(),
-          setRawMode: td.function(),
-          removeListener: td.function(),
         },
       });
 
-      let windowsCtrlCTrap = td.matchers.isA(Function);
-
+      willInterruptProcess.capture(process);
       willInterruptProcess.addHandler(cb);
 
       td.verify(process.stdin.setRawMode(true), {
         times: 0,
       });
 
-      td.verify(process.stdin.on('data', windowsCtrlCTrap), {
+      process.stdin.emit('data', [0x03]);
+
+      td.verify(process.exit(), { times: 0 });
+    });
+
+    it('does not enable raw capture when not a TTY', function() {
+      const process = new MockProcess({
+        platform: 'win',
+      });
+
+      willInterruptProcess.capture(process);
+      willInterruptProcess.addHandler(cb);
+
+      td.verify(process.stdin.setRawMode(true), {
         times: 0,
       });
+
+      process.stdin.emit('data', [0x03]);
+
+      td.verify(process.exit(), { times: 0 });
     });
   });
 });