Repository: ember-cli/ember-cli
Pull request #6096 (2016-07-21 16:15:19)

Fix and improve Watcher.detectWatcher
- [x] add tests to detectWatcher
- [x] improve fallback scenarios (not all systems node works on support fs.watch)
- [x]  make sure we consistently do the right thing, regardless of which branches we take.
- [x] windows CI seems to be failing for unknown reasons....

Ok, so this refactors the watcher detection code (which was quite complicated, untestable, buggy, and nearly impossible to extend without causing more issues). The goal here is, a separate model which can be tested, extended and improved with confidence. 

I feel This has festered long enough, a refactor was long overdue.

This PR also adds the ability to detect if `node` watcher extremely broken (we could extend this to detect if change events are even possible, but that can come later). If it is extremely broken, we log it and fall back to `polling`. This hopefully will both provide useful information to the user, and attempt a grace-full fallback.



rwjblue: This looks great! I left a few comments, but mostly just things I was thinking as I reviewed.  Only one that seems like it may be wrong (though you may correct me here).
stefanpenner: > This looks great! I left a few comments, but mostly just things I was thinking as I reviewed. Only one that seems like it may be wrong (though you may correct me here).

https://en.wikipedia.org/wiki/If_and_only_if apparently its MATHs people

rwjblue: HAHA, MATHS! 🍇 
stefanpenner: the following appears to be possible, but should not.
```
was unable to use: "events", fell back to: "watchman"
```

- [ ] fix this
homu: :umbrella: The latest upstream changes (presumably #6129) made this pull request unmergeable. Please resolve the merge conflicts.
stefanpenner: crap forgot about this, must ship soon.

Patch:
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index bc8f63caa..6bf1b052b 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -1263,11 +1263,11 @@ EmberApp.prototype.javascript = function() {
 
   var vendorFiles = [];
   for (var outputFile in this._scriptOutputFiles) {
-    var headerFiles = this._scriptOutputFiles[outputFile];
+    var inputFiles = this._scriptOutputFiles[outputFile];
 
     vendorFiles.push(
       this._concatFiles(applicationJs, {
-        headerFiles: headerFiles,
+        inputFiles: inputFiles,
         outputFile: outputFile,
         separator: EOL + ';',
         annotation: 'Concat: Vendor ' + outputFile
@@ -1328,10 +1328,10 @@ EmberApp.prototype.styles = function() {
 
   var vendorStyles = [];
   for (var outputFile in this._styleOutputFiles) {
-    var headerFiles = this._styleOutputFiles[outputFile];
+    var inputFiles = this._styleOutputFiles[outputFile];
 
     vendorStyles.push(this._concatFiles(stylesAndVendor, {
-      headerFiles: headerFiles,
+      inputFiles: inputFiles,
       outputFile: outputFile,
       annotation: 'Concat: Vendor Styles' + outputFile
     }));
@@ -1441,7 +1441,7 @@ EmberApp.prototype.testFiles = function(coreTestTree) {
   if (this.vendorTestStaticStyles.length > 0) {
     sourceTrees.push(
       this._concatFiles(external, {
-        headerFiles: this.vendorTestStaticStyles,
+        inputFiles: this.vendorTestStaticStyles,
         outputFile: this.options.outputPaths.testSupport.css,
         annotation: 'Concat: Test Support CSS'
       })
@@ -1538,7 +1538,6 @@ EmberApp.prototype.import = function(asset, options) {
   @param {String} extension
  */
 EmberApp.prototype._import = function(assetPath, options, directory, subdirectory, extension) {
-  // TODO: refactor, this has gotten very messy. Relevant tests: tests/unit/broccoli/ember-app-test.js
   var basename = path.basename(assetPath);
 
   if (isType(assetPath, 'js', {registry: this.registry})) {
@@ -1578,11 +1577,7 @@ EmberApp.prototype._import = function(assetPath, options, directory, subdirector
       options.outputFile = options.outputFile || this.options.outputPaths.vendor.css;
       addOutputFile(this._styleOutputFiles, assetPath, options);
     } else {
-      if (options.prepend) {
-        this.vendorTestStaticStyles.unshift(assetPath);
-      } else {
-        this.vendorTestStaticStyles.push(assetPath);
-      }
+      this.vendorTestStaticStyles.push(assetPath);
     }
   } else {
     var destDir = options.destDir;
diff --git a/lib/models/command.js b/lib/models/command.js
index 79edc7861..c22268374 100644
--- a/lib/models/command.js
+++ b/lib/models/command.js
@@ -13,8 +13,12 @@ var _                   = require('ember-cli-lodash-subset');
 var EOL                 = require('os').EOL;
 var CoreObject          = require('core-object');
 var logger              = require('heimdalljs-logger')('ember-cli:command');
-var Watcher             = require('../models/watcher');
+var WatchDetector       = require('../models/watch-detector');
 var SilentError         = require('silent-error');
+var exec                = Promise.denodeify(require('child_process').exec)
+var fs                  = require('fs')
+
+var cache = {};
 
 var allowedWorkOptions = {
   insideProject: true,
@@ -216,11 +220,21 @@ var Command = CoreObject.extend({
       }
     }
 
-    return Watcher.detectWatcher(this.ui, commandOptions.options).then(function(options) {
-      if (options._watchmanInfo) {
-        this.project._watchmanInfo = options._watchmanInfo;
-      }
+    var detector = new WatchDetector({
+      ui: this.ui,
+      childProcess: { exec: exec },
+      fs: fs,
+      watchmanSupportsPlatform: /^win/.test(process.platform),
+      cache: cache,
+      root: this.project.root
+    });
+
+    var options = commandOptions.options;
 
+    // do stuff to try and provide a good experience when it comes to file watching
+    return detector.findBestWatcherOption(options).then(function(watchPreference) {
+      this.project._watchmanInfo = watchPreference.watchmanInfo;
+      options.watcher = watchPreference.watcher;
       return this.run(options, commandOptions.args);
     }.bind(this));
   },
diff --git a/lib/models/watch-detector.js b/lib/models/watch-detector.js
new file mode 100644
index 000000000..b1dcc01b6
--- /dev/null
+++ b/lib/models/watch-detector.js
@@ -0,0 +1,256 @@
+'use strict';
+
+var quickTemp = require('quick-temp');
+var Promise = require('../ext/promise');
+var WATCHMAN_INFO = 'Visit http://ember-cli.com/user-guide/#watchman for more info.';
+var semver = require('semver');
+var SilentError = require('silent-error');
+
+var POLLING = 'polling';
+var WATCHMAN = 'watchman';
+var NODE = 'node';
+var EVENTS = 'events';
+var POSSIBLE_WATCHERS = [
+  POLLING,
+  WATCHMAN,
+  NODE,
+  EVENTS
+];
+
+var debug = require('heimdalljs-logger')('ember-cli:watcher');
+debugger;
+
+function WatchPreference(watcher) {
+  this.watcher = watcher || null;
+  this._watchmanInfo = {
+    enabled: false,
+    version: null,
+    canNestRoots: false
+  };
+}
+
+WatchPreference.prototype.watchmanWorks = function(details) {
+  this._watchmanInfo.enabled = true;
+  this._watchmanInfo.version = details.version;
+  this._watchmanInfo.canNestRoots = semver.satisfies(details.version, '>= 3.7.0');
+};
+
+Object.defineProperty(WatchPreference.prototype, 'watchmanInfo', {
+  get: function() {
+    return this._watchmanInfo;
+  }
+});
+
+module.exports = WatchDetector;
+/*
+ * @public
+ *
+ * A testable class that encapsulates which watch technique to use.
+ *
+ */
+function WatchDetector(options) {
+  this.childProcess = options.childProcess;
+  this.watchmanSupportsPlatform = options.watchmanSupportsPlatform;
+  this.fs = options.fs;
+  this.root = options.root;
+
+  this.ui = options.ui;
+  this.tmp = undefined;
+
+  // This exists, because during tests repeadelty testing the same directories
+  this._doesNodeWorkCache = options.cache || {};
+}
+
+/*
+ * @private
+ *
+ * implements input options parsing and fallback.
+ *
+ * @method extractPreferenceFromOptions
+ * @returns {WatchDetector)
+*/
+WatchDetector.prototype.extractPreferenceFromOptions = function(options) {
+  if (options.watcher === POLLING) {
+    debug.info('skip detecting watchman, "polling" was selected.');
+    return new WatchPreference(POLLING);
+  } else if (options.watcher === NODE) {
+    debug.info('skip detecting watchman, "node" was selected.');
+    return new WatchPreference(NODE);
+  } else {
+    debug.info('no watcher preference set, lets attempt watchman');
+    return new WatchPreference(WATCHMAN);
+  }
+};
+
+/*
+ * @public
+ *
+ * Detect if `fs.watch` provided by node is capable of observing a directory
+ * within `process.cwd()`. Although `fs.watch` is provided as a part of the
+ * node stdlib, their exists platforms it ships to that do not implement the
+ * required sub-system. For example, there exists `posix` targets, without both
+ * inotify and FSEvents
+ *
+ * @method testIfNodeWatcherAppearsToWork
+ * @returns {Boolean) outcome
+*/
+WatchDetector.prototype.testIfNodeWatcherAppearsToWork  = function() {
+  var root = this.root;
+  return new Promise(function(resolve) {
+    if (root in this._doesNodeWorkCache) {
+      resolve(this._doesNodeWorkCache[root]);
+      return;
+    }
+
+    this._doesNodeWorkCache[root] = false;
+    try {
+      // builds a tmp directory at process.cwd() + '/tmp/' + 'something-unique';
+      var tmpDir = quickTemp.makeOrRemake(this, 'tmp');
+
+      var watcher = this.fs.watch(tmpDir, { persistent: false, recursive: false });
+      watcher.close();
+    } catch (e) {
+      debug.info('testing if node watcher failed with: %o', e)
+      resolve(false);
+      return;
+    } finally {
+      try {
+        quickTemp.remove(this, 'tmp');
+        // cleanup dir
+      } catch (e) {
+        // not much we can do, lets debug log.
+        debug.info('cleaning up dir failed with: %o', e)
+      }
+    }
+
+    // it seems we where able to at least watch and unwatch, this should catch
+    // systems that have a very broken NodeWatcher.
+    //
+    // NOTE: we could also add a more advance chance, that triggers a change
+    // and expects to be informed of the change. This can be added in a future
+    // iteration.
+    //
+
+    this._doesNodeWorkCache[root] = true;
+    resolve(true);
+  }.bind(this));
+};
+
+/*
+ * @public
+ *
+ * Selecting the best watcher is complicated, this method (given a preference)
+ * will test and provide the best possible watcher available to the current
+ * system and project.
+ *
+ *
+ *  @method findBestWatcherOption
+ *  @input {Object} options
+ *  @returns {Object} watch preference
+*/
+WatchDetector.prototype.findBestWatcherOption = function(options) {
+  var preference = this.extractPreferenceFromOptions(options);
+  var original = options.watcher;
+
+  if (original && POSSIBLE_WATCHERS.indexOf(original) === -1) {
+    return Promise.reject(new SilentError('Unknown Watcher: `' + original + '` supported watchers: [' + POSSIBLE_WATCHERS.join(', ') + ']'));
+  }
+
+  if (preference.watcher === WATCHMAN) {
+    preference = this.checkWatchman();
+  }
+
+  return Promise.resolve(preference).then(function(bestOption) {
+    if (bestOption.watcher === NODE) {
+      // although up to this point, we may believe Node is the best watcher
+      // this may not be true because:
+      // * not all platforms that run node, support node.watch
+      // * not all file systems support node watch
+      //
+      return this.testIfNodeWatcherAppearsToWork().then(function(appearsToWork) {
+        return new WatchPreference(appearsToWork ? NODE : POLLING);
+      });
+    } else {
+      return bestOption;
+    }
+  }.bind(this)).then(function(actual) {
+    debug.info('foundBestWatcherOption, preference was: %o, actual: %o', options, actual);
+    if (actual /* if no preference was initial set, don't bother informing the user */ &&
+      original /* if no original was set, the fallback is expected */ &&
+      actual.watcher !== original &&
+
+      // events represents either watcherman or node, but no preference
+      !(original === EVENTS && (actual.watcher === WATCHMAN || actual.watcher === NODE))) {
+      // if there was an initial preference, but we had to fall back inform the user.
+      this.ui.writeLine('was unable to use: "' + original + '", fell back to: "' + actual.watcher + '"')
+    }
+    return actual;
+  }.bind(this));
+};
+
+/*
+ * @public
+ *
+ * Although watchman may be selected, it may not work due to:
+ *
+ *  * invalid version
+ *  * it may be broken in detectable ways
+ *  * watchman executable may be something unexpected
+ *  * ???
+ *
+ *  @method checkWatchman
+ *  @returns {Object} watch preference + _watchmanInfo
+*/
+WatchDetector.prototype.checkWatchman = function() {
+  var result = new WatchPreference(null);
+
+  debug.info('exev("watchman version")');
+  return this.childProcess.exec('watchman version').then(function(output) {
+    debug.info('watchman version STDOUT: %s', output);
+    var version;
+    try {
+      version = JSON.parse(output).version;
+    } catch (e) {
+      this.ui.writeLine('Looks like you have a different program called watchman.');
+      this.ui.writeLine(WATCHMAN_INFO);
+
+      result.watcher = NODE;
+      return result;
+    }
+
+    debug.info('detected watchman: %s', version);
+
+    if (semver.satisfies(version, '>= 3.0.0')) {
+      debug.info('watchman %s does satisfy: %s', version, '>= 3.0.0');
+
+      result.watcher = WATCHMAN;
+
+      result.watchmanWorks({
+        version: version,
+        canNestRoots: semver.satisfies(version, '>= 3.7.0')
+      });
+    } else {
+      debug.info('watchman %s does NOT satisfy: %s', version, '>= 3.0.0');
+
+      this.ui.writeLine('Invalid watchman found, version: [' + version + '] did not satisfy [>= 3.0.0].');
+      this.ui.writeLine(WATCHMAN_INFO);
+
+      result.watcher = NODE;
+    }
+
+    return result;
+  }.bind(this), function(reason) {
+    debug.info('detecting watchman failed %o', reason);
+
+    if (this.watchmanSupportsPlatform) {
+      // don't bother telling windows users watchman detection failed, that is
+      // until watchman is legit on windows.
+    } else {
+      this.ui.writeLine('Could not start watchman');
+      this.ui.writeLine(WATCHMAN_INFO);
+    }
+
+    result.watcher = NODE;
+    return result;
+  }.bind(this));
+};
diff --git a/lib/models/watcher.js b/lib/models/watcher.js
index 0cd43cd44..e08406814 100644
--- a/lib/models/watcher.js
+++ b/lib/models/watcher.js
@@ -3,9 +3,6 @@
 var chalk   = require('chalk');
 var Task    = require('./task');
 var logger  = require('heimdalljs-logger')('ember-cli:watcher');
-var Promise = require('../ext/promise');
-var exec    = Promise.denodeify(require('child_process').exec);
-var isWin = /^win/.test(process.platform);
 
 var Watcher = Task.extend({
   verbose: true,
@@ -84,59 +81,4 @@ var Watcher = Task.extend({
   }
 });
 
-Watcher.detectWatcher = function(ui, _options) {
-  var options = _options || {};
-  var watchmanInfo = 'Visit http://ember-cli.com/user-guide/#watchman for more info.';
-
-  if (options.watcher === 'polling') {
-    logger.info('skip detecting watchman, poll instead');
-    return Promise.resolve(options);
-  } else if (options.watcher === 'node') {
-    logger.info('skip detecting watchman, node instead');
-    return Promise.resolve(options);
-  } else if (isWin) {
-    logger.info('watchman isn\'t supported on windows, node instead');
-    options.watcher = 'node';
-    return Promise.resolve(options);
-  } else {
-    logger.info('detecting watchman');
-    return exec('watchman version').then(function(output) {
-      var version;
-      try {
-        version = JSON.parse(output).version;
-      } catch (e) {
-        options.watcher = 'node';
-        ui.writeLine('Looks like you have a different program called watchman, falling back to NodeWatcher.');
-        ui.writeLine(watchmanInfo);
-        return options;
-      }
-      logger.info('detected watchman: %s', version);
-
-      var semver = require('semver');
-      if (semver.satisfies(version, '>= 3.0.0')) {
-        logger.info('watchman %s does satisfy: %s', version, '>= 3.0.0');
-        options.watcher = 'watchman';
-        options._watchmanInfo = {
-          enabled: true,
-          version: version,
-          canNestRoots: semver.satisfies(version, '>= 3.7.0')
-        };
-      } else {
-        logger.info('watchman %s does NOT satisfy: %s', version, '>= 3.0.0');
-        ui.writeLine('Invalid watchman found, version: [' + version + '] did not satisfy [>= 3.0.0], falling back to NodeWatcher.');
-        ui.writeLine(watchmanInfo);
-        options.watcher = 'node';
-      }
-
-      return options;
-    }, function(reason) {
-      logger.info('detecting watchman failed %o', reason);
-      ui.writeLine('Could not start watchman; falling back to NodeWatcher for file system events.');
-      ui.writeLine(watchmanInfo);
-      options.watcher = 'node';
-      return options;
-    });
-  }
-};
-
 module.exports = Watcher;
diff --git a/package.json b/package.json
index 2bc97f1d4..a49355e94 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
     "broccoli-babel-transpiler": "^5.4.5",
     "broccoli-brocfile-loader": "^0.18.0",
     "broccoli-builder": "^0.18.0",
-    "broccoli-concat": "^3.0.2",
+    "broccoli-concat": "^2.0.4",
     "broccoli-config-loader": "^1.0.0",
     "broccoli-config-replace": "^1.1.2",
     "broccoli-funnel": "^1.0.6",


Test patch:
diff --git a/tests/acceptance/init-test.js b/tests/acceptance/init-test.js
index daa649738..1076c43eb 100644
--- a/tests/acceptance/init-test.js
+++ b/tests/acceptance/init-test.js
@@ -61,6 +61,9 @@ describe('Acceptance: ember init', function() {
     removeIgnored(expected);
     removeIgnored(actual);
 
+    removeTmp(expected);
+    removeTmp(actual);
+
     expected.sort();
 
     expect(expected).to.deep.equal(actual, EOL + ' expected: ' +  util.inspect(expected) +
@@ -75,6 +78,9 @@ describe('Acceptance: ember init', function() {
     removeIgnored(expected);
     removeIgnored(actual);
 
+    removeTmp(expected);
+    removeTmp(actual);
+
     expected.sort();
 
     expect(expected).to.deep.equal(actual, EOL + ' expected: ' +  util.inspect(expected) +
@@ -89,6 +95,12 @@ describe('Acceptance: ember init', function() {
       strict: true
     }).sort();
   }
+
+  function removeTmp(array) {
+    remove(array, function(entry) {
+      return /^tmp[\\\/]$/.test(entry);
+    });
+  }
   function removeIgnored(array) {
     remove(array, function(fn) {
       return any(Blueprint.ignoredFiles, function(ignoredFile) {
diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js
index b8d320636..d1293af06 100644
--- a/tests/unit/broccoli/ember-app-test.js
+++ b/tests/unit/broccoli/ember-app-test.js
@@ -8,7 +8,6 @@ var Project    = require('../../../lib/models/project');
 var expect     = require('chai').expect;
 var proxyquire = require('proxyquire');
 var td = require('testdouble');
-var EOL = require('os').EOL;
 
 var MockUI = require('../../helpers/mock-ui');
 
@@ -939,7 +938,7 @@ describe('broccoli/ember-app', function() {
     });
 
     it('shows deprecation message if called directly', function() {
-      app.concatFiles('input-path-somewhere', {
+      app.concatFiles(null, {
         outputFile: 'foo.js'
       });
 
@@ -947,180 +946,12 @@ describe('broccoli/ember-app', function() {
     });
 
     it('ignores deprecation message if called through _concatFiles()', function() {
-      app._concatFiles('input-path-somewhere', {
+      app._concatFiles(null, {
         outputFile: 'foo.js'
       });
 
       expect(project.ui.output).to.not.contain('EmberApp.concatFiles() is deprecated');
     });
-
-    describe('concat order', function() {
-      var count = 0;
-      var args = [];
-
-      beforeEach(function() {
-        count = 0;
-        args = [];
-
-        // we are "spying and mocking here" so to ensure we are testing our own
-        // "wiring" not the implementation of our dependencies e.g. broccoli-concat
-        app._concatFiles = function(tree, options) {
-          count++
-          args.push(options);
-          return tree;
-        };
-
-        app.appAndDependencies = function() {
-          return 'app-and-dependencies-tree';
-        };
-      });
-
-      it('correctly orders concats from app.styles()', function() {
-        app.import('files/c.css');
-        app.import('files/b.css');
-        app.import('files/a.css', { prepend: true });
-        app.import('files/a.css');
-
-        app.styles(); // run
-
-        expect(count).to.eql(3);
-
-        expect(args[0]).to.deep.eql({
-          allowNone: true,
-          annotation: 'Concat: Addon CSS',
-          inputFiles: [
-            '**/*.css'
-          ],
-          outputFile: '/addons.css'
-        });
-
-        expect(args[1]).to.deep.eql({
-          allowNone: true,
-          annotation: 'Concat: Addon JS',
-          inputFiles: [
-            '**/*.js'
-          ],
-          outputFile: '/addons.js'
-        });
-
-        expect(args[2]).to.deep.eql({
-          annotation: 'Concat: Vendor Styles/assets/vendor.css',
-          headerFiles: [
-            'files/a.css',
-            'files/c.css',
-            'files/b.css',
-            'files/a.css',
-            'vendor/addons.css',
-          ],
-          outputFile: '/assets/vendor.css'
-        });
-      });
-      it('correct orders concats from app.javacsript()', function() {
-        app.import('files/c.js');
-        app.import('files/b.js');
-        app.import('files/a.js', { prepend: true });
-        app.import('files/a.js');
-
-        app.javascript(); // run
-
-        expect(count).to.eql(2);
-        // should be unrelated files
-        expect(args[0]).to.deep.eql({
-          annotation: "Concat: App",
-          footerFiles: [
-            "vendor/ember-cli/app-suffix.js",
-            "vendor/ember-cli/app-config.js",
-            "vendor/ember-cli/app-boot.js"
-          ],
-          headerFiles: [
-            "vendor/ember-cli/app-prefix.js"
-          ],
-          inputFiles: [
-            "test-project/**/*.js"
-          ],
-          outputFile: "/assets/test-project.js"
-        });
-
-        // should be: a,c,b,a in output
-        expect(args[1]).to.deep.eql({
-          annotation: "Concat: Vendor /assets/vendor.js",
-          headerFiles: [
-            "vendor/ember-cli/vendor-prefix.js",
-            "files/a.js",
-            "bower_components/jquery/dist/jquery.js",
-            "bower_components/ember/ember.js",
-            "bower_components/ember-cli-shims/app-shims.js",
-            "files/c.js",
-            "files/b.js",
-            "files/a.js",
-            "vendor/addons.js",
-            "vendor/ember-cli/vendor-suffix.js"
-          ],
-          outputFile: "/assets/vendor.js",
-          separator: EOL + ';',
-        });
-      });
-
-      it('correctly orders concats from app.testFiles()', function() {
-        app.import('files/c.js', { type: 'test'});
-        app.import('files/b.js', { type: 'test'});
-        app.import('files/a.js', { type: 'test', prepend: true });
-        app.import('files/a.js', { type: 'test' });
-
-        app.import('files/c.css', { type: 'test' })
-        app.import('files/b.css', { type: 'test' })
-        app.import('files/a.css', { type: 'test' })
-        app.import('files/a.css', { type: 'test', prepend: true });
-
-        app.testFiles('some-tree'); // run
-
-        expect(count).to.eql(4);
-
-        expect(args[0]).to.deep.eql({
-          allowNone: true,
-          annotation: 'Concat: Addon CSS',
-          inputFiles: ['**/*.css'],
-          outputFile: '/addons.css'
-        });
-
-        expect(args[1]).to.deep.eql({
-          allowNone: true,
-          annotation: 'Concat: Addon JS',
-          inputFiles: ['**/*.js'],
-          outputFile: '/addons.js',
-        });
-
-        expect(args[2]).to.deep.eql({
-          allowNone: true,
-          annotation: 'Concat: Test Support JS',
-          footerFiles: [
-            'vendor/ember-cli/test-support-suffix.js'
-          ],
-          headerFiles: [
-            'vendor/ember-cli/test-support-prefix.js',
-            'files/a.js',
-            'files/c.js',
-            'files/b.js',
-            'files/a.js'
-          ],
-          inputFiles: [
-            'addon-test-support/**/*.js'
-          ],
-          outputFile: '/assets/test-support.js'
-        });
-
-        expect(args[3]).to.deep.eql({
-          annotation: 'Concat: Test Support CSS',
-          headerFiles: [
-            'files/a.css',
-            'files/c.css',
-            'files/b.css',
-            'files/a.css',
-          ],
-          outputFile: '/assets/test-support.css'
-        });
-      });
-    });
   });
 });
 
diff --git a/tests/unit/cli/cli-test.js b/tests/unit/cli/cli-test.js
index 8e548fc9e..c70466023 100644
--- a/tests/unit/cli/cli-test.js
+++ b/tests/unit/cli/cli-test.js
@@ -242,7 +242,7 @@ describe('Unit: CLI', function() {
         return ember([command, '--watcher', 'events']).then(function() {
           var captor = td.matchers.captor();
           td.verify(server(captor.capture()), {ignoreExtraArgs: true, times: 1});
-          expect(captor.value.watcher, 'watcher').to.match(/node|events|watchman/);
+          expect(captor.value.watcher, 'watcher').to.match(/node|polling|watchman/);
         });
       });
 
@@ -262,7 +262,7 @@ describe('Unit: CLI', function() {
         return ember([command]).then(function() {
           var captor = td.matchers.captor();
           td.verify(server(captor.capture()), {ignoreExtraArgs: true, times: 1});
-          expect(captor.value.watcher, 'watcher').to.match(/node|events|watchman/);
+          expect(captor.value.watcher, 'watcher').to.match(/node|polling|watchman/);
         });
       });
 
diff --git a/tests/unit/commands/init-test.js b/tests/unit/commands/init-test.js
index bdfc8dea4..aa87fa1b0 100644
--- a/tests/unit/commands/init-test.js
+++ b/tests/unit/commands/init-test.js
@@ -1,6 +1,6 @@
 'use strict';
 
-var fs            = require('fs');
+var fs            = require('fs-extra');
 var os            = require('os');
 var path          = require('path');
 var expect        = require('chai').expect;
@@ -15,7 +15,7 @@ var InitCommand   = require('../../../lib/commands/init');
 var td = require('testdouble');
 
 describe('init command', function() {
-  var ui, analytics, tasks, command;
+  var ui, analytics, tasks, command, workingDir;
 
   beforeEach(function() {
     ui = new MockUI();
@@ -25,9 +25,14 @@ describe('init command', function() {
       NpmInstall: Task.extend({}),
       BowerInstall: Task.extend({})
     };
+
+    var tmpDir = os.tmpdir();
+    workingDir = tmpDir + '/ember-cli-test-project';
+    fs.mkdirSync(workingDir);
   });
 
   afterEach(function() {
+    fs.removeSync(workingDir);
     td.reset();
   });
 
@@ -102,11 +107,8 @@ describe('init command', function() {
     // change the working dir so `process.cwd` can't be an invalid path for base directories
     // named `ember-cli`.
 
-    var tmpDir = os.tmpdir();
-    var workingDir = tmpDir + '/ember-cli-test-project';
     var currentWorkingDir = process.cwd();
 
-    fs.mkdirSync(workingDir);
     process.chdir(workingDir);
 
     tasks.InstallBlueprint = Task.extend({
@@ -124,7 +126,6 @@ describe('init command', function() {
       })
       .then(function() {
         process.chdir(currentWorkingDir);
-        fs.rmdirSync(workingDir);
       });
   });
 
diff --git a/tests/unit/models/watch-detector-test.js b/tests/unit/models/watch-detector-test.js
new file mode 100644
index 000000000..5c11df203
--- /dev/null
+++ b/tests/unit/models/watch-detector-test.js
@@ -0,0 +1,263 @@
+'use strict';
+
+var expect = require('chai').expect;
+
+var MockUI = require('../../helpers/mock-ui');
+var WatchDetector = require('../../../lib/models/watch-detector');
+var EOL = require('os').EOL;
+var chalk = require('chalk');
+var Promise = require('../../../lib/ext/promise');
+
+describe('WatchDetector', function() {
+  var ui;
+  var subject;
+  var execArg, execValue;
+  var fs, childProcess;
+
+  beforeEach(function() {
+    ui = new MockUI();
+
+    fs =  {};
+    childProcess = {};
+
+    function exec(arg) {
+      execArg = arg;
+      return execValue;
+    }
+
+    subject = new WatchDetector({
+      ui: ui,
+      fs: fs,
+      childProcess: childProcess,
+      watchmanSupportsPlatform: false,
+      root: process.cwd()
+    });
+  });
+
+  it('#extractPreferenceFromOptions works', function() {
+    expect(subject.extractPreferenceFromOptions({})).to.have.property('watcher', 'watchman');
+    expect(subject.extractPreferenceFromOptions({watcher: 'polling'})).to.have.property('watcher', 'polling');
+    expect(subject.extractPreferenceFromOptions({watcher: 'node'})).to.have.property('watcher', 'node');
+    expect(subject.extractPreferenceFromOptions({watcher: 'nodsdfe'})).to.have.property('watcher', 'watchman');
+  });
+
+  describe('#testIfNodeWatcherAppearsToWork', function() {
+    it('reports NO if fs.watch throws', function() {
+      fs.watch = function() {
+        throw new Error('Something went wrong');
+      };
+
+      return subject.testIfNodeWatcherAppearsToWork().then(function(value) {
+        expect(value).to.be.false;
+      });
+    });
+
+    // we could extend this to test also if change events are triggered or not..
+    it('reports YES if nothing throws', function() {
+      fs.watch = function() { return { close: function() { }}};
+
+      return subject.testIfNodeWatcherAppearsToWork().then(function(value) {
+        expect(value).to.be.true;
+      });
+    });
+  });
+
+
+  describe('#findBestWatcherOption', function() {
+    describe('input preference.watcher === watchman', function() {
+      beforeEach(function() {
+        childProcess.exec = function() {
+          return Promise.resolve('{"version":"3.0.0"}');
+        };
+      });
+
+      it('chooses watchman', function() {
+        return subject.findBestWatcherOption({ watcher: 'watchman' }).then(function(option) {
+          expect(option).to.have.property('watcher', 'watchman');
+          expect(option.watchmanInfo).to.have.property('version');
+          expect(option.watchmanInfo).to.have.property('canNestRoots');
+          expect(option.watchmanInfo).to.have.property('enabled', true)
+          expect(ui.output).not.to.match(/Could not start watchman/);
+          expect(ui.output).not.to.match(/fell back to: "node"/);
+          expect(ui.output).not.to.match(/Visit http:\/\/ember-cli.com\/user-guide\/\#watchman/);
+        });
+      });
+
+      describe('watchman does not work', function() {
+        beforeEach(function() {
+          childProcess.exec = function() {
+            return Promise.reject();
+          };
+        });
+
+        it('false back to node if it can', function() {
+          fs.watch = function() { return { close: function() { }}};
+
+          return subject.findBestWatcherOption({ watcher: 'watchman' }).then(function(option) {
+            expect(option.watchmanInfo).to.have.property('enabled', false)
+            expect(option).to.have.property('watcher', 'node');
+            expect(option.watchmanInfo).to.have.property('version');
+            expect(option.watchmanInfo).to.have.property('canNestRoots');
+            expect(ui.output).to.match(/Could not start watchman/);
+            expect(ui.output).to.match(/fell back to: "node"/);
+            expect(ui.output).to.match(/Visit http:\/\/ember-cli.com\/user-guide\/\#watchman/);
+          });
+        });
+
+        it('false back to polling if node does not work', function() {
+          fs.watch = function() {
+            throw new Error('something went wrong');
+          };
+
+          return subject.findBestWatcherOption({ watcher: 'watchman' }).then(function(option) {
+            expect(option).to.have.property('watcher', 'polling');
+            expect(option.watchmanInfo).to.have.property('enabled', false)
+            expect(option.watchmanInfo).to.have.property('version');
+            expect(option.watchmanInfo).to.have.property('canNestRoots');
+            expect(ui.output).to.match(/Could not start watchman/);
+            expect(ui.output).to.match(/fell back to: "polling"/);
+            expect(ui.output).to.match(/Visit http:\/\/ember-cli.com\/user-guide\/\#watchman/);
+          });
+        });
+      });
+    });
+
+    describe('input preference.watcher === polling', function() {
+      it('simply works', function() {
+        // we assuming polling can never not work, if it doesn't sorry..
+        return subject.findBestWatcherOption({ watcher: 'polling' }).then(function(option) {
+          expect(option.watchmanInfo).to.have.property('enabled', false)
+          expect(option).to.have.property('watcher', 'polling');
+          expect(ui.output).to.eql('');
+        });
+      })
+    });
+
+    describe('input preference.watcher === node', function() {
+      it('chooses node, if everything  seems ok', function() {
+        fs.watch = function() { return { close: function() { }}};
+
+        // we assuming polling can never not work, if it doesn't sorry..
+        return subject.findBestWatcherOption({ watcher: 'node' }).then(function(option) {
+          expect(option).to.have.property('watcher', 'node');
+          expect(ui.output).to.eql('');
+        });
+      });
+
+      it('false back to polling if watch fails', function() {
+        fs.watch = function() {
+          throw new Error('OMG');
+        };
+        // we assuming polling can never not work, if it doesn't sorry..
+        return subject.findBestWatcherOption({ watcher: 'node' }).then(function(option) {
+          expect(option).to.have.property('watcher', 'polling');
+          expect(ui.output).to.eql('was unable to use: "node", fell back to: "polling"' + EOL);
+        });
+      });
+
+      it('falls back to polling if unwatch fails', function() {
+        fs.watch = function() {
+          // works
+        };
+        fs.unwatch = function() {
+          throw new Error('OMG');
+        };
+        // we assuming polling can never not work, if it doesn't sorry..
+        return subject.findBestWatcherOption({ watcher: 'node' }).then(function(option) {
+          expect(option).to.have.property('watcher', 'polling');
+          expect(ui.output).to.eql('was unable to use: "node", fell back to: "polling"' + EOL);
+        });
+      })
+    });
+
+  });
+
+  describe('#checkWatchman', function() {
+    describe('watchmanSupportsPlatform', function() {
+      it('true: hides the "watchman not found, falling back to XYZ message"', function() {
+        subject.watchmanSupportsPlatform = true;
+
+        childProcess.exec = function() {
+          return Promise.reject();
+        };
+        fs.watch = function() { return { close: function() { }}};
+
+        return subject.checkWatchman().then(function(result) {
+          expect(result).to.have.property('watcher', 'node');
+          expect(ui.output).to.eql('');
+        });
+      });
+
+      it('false: shows the "watchman not found, falling back to XYZ message"', function() {
+        subject.watchmanSupportsPlatform = false;
+        fs.watch = function() { return { close: function() { }}};
+
+        childProcess.exec = function() {
+          return Promise.reject();
+        };
+
+        return subject.checkWatchman().then(function(result) {
+          expect(result).to.have.property('watcher', 'node');
+          expect(ui.output).to.match(/Could not start watchman/);
+          expect(ui.output).to.match(/Visit http:\/\/ember-cli.com\/user-guide\/\#watchman/);
+        });
+      });
+    });
+    it('prefers watchman if everything appears to be good', function() {
+      childProcess.exec = function() {
+        return Promise.resolve('{"version":"3.0.0"}');
+      };
+
+      return subject.checkWatchman().then(function(preference) {
+        expect(preference).to.have.property('watcher', 'watchman');
+        expect(ui.output).to.not.match(/Could not start watchman/);
+        expect(ui.output).to.not.match(/falling back to NodeWatcher/);
+        expect(ui.output).to.not.match(/ember-cli\.com\/user-guide\/\#watchman/);
+        expect(ui.output).to.not.match(/Looks like you have a different program called watchman/);
+        expect(ui.output).to.not.match(/Invalid watchman found/);
+      });
+    });
+
+    describe('fallse back to NODE', function() {
+      var iff = it;
+
+      iff('the exec rejects', function() {
+        childProcess.exec = function() {
+          return Promise.reject();
+        };
+
+        return subject.checkWatchman().then(function(preference) {
+          expect(preference).to.have.property('watcher', 'node');
+          expect(ui.output).to.match(/Could not start watchman/);
+          expect(ui.output).to.match(/ember-cli\.com\/user-guide\/\#watchman/);
+        });
+      });
+
+      iff('the `watchman version` doesn\'t parse', function() {
+        childProcess.exec = function() {
+          return Promise.resolve("not json");
+        };
+
+        return subject.checkWatchman().then(function(preference) {
+          expect(preference).to.have.property('watcher', 'node');
+          expect(ui.output).to.match(/Looks like you have a different program called watchman/);
+          expect(ui.output).to.match(/ember-cli\.com\/user-guide\/\#watchman/);
+        });
+      });
+
+
+      iff('the `watchman version` doesn\'t satisfy => 3.0.0', function() {
+        childProcess.exec = function() {
+          return Promise.resolve('{"version":"2.9.9"}');
+        };
+
+        return subject.checkWatchman().then(function(preference) {
+          expect(preference).to.have.property('watcher', 'node');
+          expect(ui.output).to.match(/Invalid watchman found/);
+          expect(ui.output).to.match(/version: \[2\.9\.9\] did not satisfy/);
+          expect(ui.output).to.match(/ember-cli\.com\/user-guide\/\#watchman/);
+        });
+      });
+    });
+  });
+});
diff --git a/tests/unit/models/watcher-test.js b/tests/unit/models/watcher-test.js
index e7aa880a8..ef134ba9b 100644
--- a/tests/unit/models/watcher-test.js
+++ b/tests/unit/models/watcher-test.js
@@ -9,6 +9,7 @@ var Watcher = require('../../../lib/models/watcher');
 var EOL = require('os').EOL;
 var chalk = require('chalk');
 var BuildError = require('../../helpers/build-error');
+var Promise = require('../../../lib/ext/promise');
 
 describe('Watcher', function() {
   var ui;
diff --git a/tests/unit/tasks/test-test.js b/tests/unit/tasks/test-test.js
index d020c0e63..e254bc102 100644
--- a/tests/unit/tasks/test-test.js
+++ b/tests/unit/tasks/test-test.js
@@ -4,7 +4,7 @@ var expect      = require('chai').expect;
 var TestTask    = require('../../../lib/tasks/test');
 var MockProject = require('../../helpers/mock-project');
 
-describe('test', function() {
+describe('test task test', function() {
   var subject;
 
   it('transforms options for testem configuration', function() {