Repository: ember-cli/ember-cli
Pull request #6520 (2016-12-03 22:07:28)

Improve Windows Symlink Related messaging
* refactor to be more testable (but maintain same external API)
* improve tests
* don’t warn if canSymlink is already true (may have: https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/ setup, or some other mechanism)
* make sure the URL is formatted correctly

- [x] fix YUIDoc lint issue
- [x] get reviews

--- 


- [x] issue to update website: https://github.com/ember-cli/ember-cli.github.io/issues/113

stefanpenner: also tested this manually in windows, all appears well.
felixrieseberg: Jesus, @stefanpenner is a machine. Well done, sir.

Patch:
diff --git a/blueprints/app/files/.jshintrc b/blueprints/app/files/.jshintrc
new file mode 100644
index 000000000..d421faa30
--- /dev/null
+++ b/blueprints/app/files/.jshintrc
@@ -0,0 +1,32 @@
+{
+  "predef": [
+    "document",
+    "window",
+    "-Promise"
+  ],
+  "browser": true,
+  "boss": true,
+  "curly": true,
+  "debug": false,
+  "devel": true,
+  "eqeqeq": true,
+  "evil": true,
+  "forin": false,
+  "immed": false,
+  "laxbreak": false,
+  "newcap": true,
+  "noarg": true,
+  "noempty": false,
+  "nonew": false,
+  "nomen": false,
+  "onevar": false,
+  "plusplus": false,
+  "regexp": false,
+  "undef": true,
+  "sub": true,
+  "strict": false,
+  "white": false,
+  "eqnull": true,
+  "esversion": 6,
+  "unused": true
+}
diff --git a/blueprints/app/files/package.json b/blueprints/app/files/package.json
index 73f6ddb5c..e19394da0 100644
--- a/blueprints/app/files/package.json
+++ b/blueprints/app/files/package.json
@@ -24,6 +24,7 @@
     "ember-cli-htmlbars": "^1.1.1",
     "ember-cli-htmlbars-inline-precompile": "^0.3.3",
     "ember-cli-inject-live-reload": "^1.4.1",
+    "ember-cli-jshint": "^2.0.1",
     "ember-cli-qunit": "^3.0.1",
     "ember-cli-sri": "^2.1.0",
     "ember-cli-test-loader": "^1.1.0",
diff --git a/lib/commands/build.js b/lib/commands/build.js
index 2d4a12b14..a60471f1e 100644
--- a/lib/commands/build.js
+++ b/lib/commands/build.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var Command = require('../models/command');
-var win     = require('../utilities/windows-admin');
+var Win = require('../utilities/windows-admin');
 
 module.exports = Command.extend({
   name: 'build',
@@ -28,7 +28,7 @@ module.exports = Command.extend({
       ui: this.ui
     });
 
-    return win.checkWindowsElevation(this.ui).then(function () {
+    return Win.checkIfSymlinksNeedToBeEnabled(this.ui).then(function () {
       return buildTask.run(commandOptions)
         .then(function () {
           if (!commandOptions.suppressSizes && commandOptions.environment === 'production') {
diff --git a/lib/commands/init.js b/lib/commands/init.js
index 502534307..2fe77dfe0 100644
--- a/lib/commands/init.js
+++ b/lib/commands/init.js
@@ -65,14 +65,6 @@ module.exports = Command.extend({
         analytics: this.analytics,
         project: this.project
       });
-
-      var addonInstall = new this.tasks.AddonInstall({
-        ui:             this.ui,
-        analytics:      this.analytics,
-        project:        this.project,
-        NpmInstallTask: this.tasks.NpmInstall,
-        BlueprintTask:  this.tasks.GenerateFromBlueprint
-      });
     }
 
     if (!commandOptions.skipBower) {
@@ -110,12 +102,15 @@ module.exports = Command.extend({
     return installBlueprint.run(blueprintOpts)
       .then(function() {
         logger.info('after:installblueprint');
+        if (commandOptions.skipGit === false) {
+          return gitInit.run(commandOptions, rawArgs);
+        }
+      })
+      .then(function() {
         if (!commandOptions.skipNpm) {
           return npmInstall.run({
             verbose: commandOptions.verbose,
             optional: false
-          }).then(function() {
-            project.setupNodeModulesPath();
           });
         }
       })
@@ -125,20 +120,6 @@ module.exports = Command.extend({
             verbose: commandOptions.verbose
           });
         }
-      })
-      .then(function() {
-        // skip `ember generate ember-cli-jshint` if the plugin hasn't been installed
-        if (!commandOptions.skipNpm) {
-          return addonInstall.run({
-            verbose: commandOptions.verbose,
-            packages: ['ember-cli-jshint']
-          });
-        }
-      })
-      .then(function() {
-        if (commandOptions.skipGit === false) {
-          return gitInit.run(commandOptions, rawArgs);
-        }
       });
   }
 });
diff --git a/lib/commands/serve.js b/lib/commands/serve.js
index 4b0cf2ff0..bc2b16844 100644
--- a/lib/commands/serve.js
+++ b/lib/commands/serve.js
@@ -5,7 +5,7 @@ var Command     = require('../models/command');
 var Promise     = require('../ext/promise');
 var SilentError = require('silent-error');
 var PortFinder  = require('portfinder');
-var win         = require('../utilities/windows-admin');
+var Win         = require('../utilities/windows-admin');
 var EOL         = require('os').EOL;
 
 PortFinder.basePort = 49152;
@@ -72,7 +72,7 @@ module.exports = Command.extend({
           project: this.project
         });
 
-        return win.checkWindowsElevation(this.ui).then(function() {
+        return Win.checkIfSymlinksNeedToBeEnabled(this.ui).then(function() {
           return serve.run(commandOptions);
         });
       }.bind(this));
diff --git a/lib/models/addon.js b/lib/models/addon.js
index 0a9e45f20..ea11d9dd4 100644
--- a/lib/models/addon.js
+++ b/lib/models/addon.js
@@ -293,10 +293,6 @@ var Addon = CoreObject.extend({
     return tree;
   },
 
-  _treePathFor: function _treePathFor(treeName) {
-    return path.normalize(path.join(this.root, this.treePaths[treeName]));
-  },
-
   /* @private
    * @method _warn
    */
@@ -633,17 +629,18 @@ var Addon = CoreObject.extend({
   */
   shouldCompileTemplates: function() {
     var templateExtensions = this.registry.extensionsForType('template');
-    var addonTreePath = this._treePathFor('addon');
-    var addonTemplatesTreePath = this._treePathFor('addon-templates');
-    var addonTemplatesTreeInAddonTree = addonTemplatesTreePath.indexOf(addonTreePath) === 0;
+    var addonTreePath = path.join(this.root, this.treePaths['addon']);
+    var addonTemplatesTreePath = path.join(this.root, this.treePaths['addon-templates']);
 
-    var files = this._getAddonTreeFiles();
+    var files = [];
 
-    if (!addonTemplatesTreeInAddonTree) {
-      files = files.concat(this._getAddonTemplatesTreeFiles());
+    if (existsSync(addonTreePath)) {
+      files = files.concat(walkSync(addonTreePath));
     }
 
-    files = files.filter(Boolean);
+    if (existsSync(addonTemplatesTreePath)) {
+      files = files.concat(walkSync(addonTemplatesTreePath));
+    }
 
     var extensionMatcher = new RegExp('(' + templateExtensions.join('|') + ')$');
 
@@ -652,26 +649,6 @@ var Addon = CoreObject.extend({
     });
   },
 
-  _getAddonTreeFiles: function() {
-    var addonTreePath = this._treePathFor('addon');
-
-    if (existsSync(addonTreePath)) {
-      return walkSync(addonTreePath);
-    }
-
-    return [];
-  },
-
-  _getAddonTemplatesTreeFiles: function() {
-    var addonTemplatesTreePath = this._treePathFor('addon-templates');
-
-    if (existsSync(addonTemplatesTreePath)) {
-      return walkSync(addonTemplatesTreePath);
-    }
-
-    return [];
-  },
-
   _addonTemplateFiles: function addonTemplateFiles(tree) {
     this._requireBuildPackages();
 
@@ -773,7 +750,7 @@ var Addon = CoreObject.extend({
   jshintAddonTree: function() {
     this._requireBuildPackages();
 
-    var addonPath = this._treePathFor('addon');
+    var addonPath = path.join(this.root, this.treePaths['addon']);
 
     if (!existsSync(addonPath)) {
       return;
diff --git a/lib/utilities/windows-admin.js b/lib/utilities/windows-admin.js
index f5a6d69e5..510cfce5f 100644
--- a/lib/utilities/windows-admin.js
+++ b/lib/utilities/windows-admin.js
@@ -2,40 +2,134 @@
 
 var Promise = require('../ext/promise');
 var chalk   = require('chalk');
-var exec    = require('child_process').exec;
-
-module.exports = {
-  /**
-   * Uses the eon-old command NET SESSION to determine whether or not the
-   * current user has elevated rights (think sudo, but Windows).
-   *
-   * @method checkWindowsElevation
-   * @param  {Object} ui - ui object used to call writeLine();
-   * @return {Object} Object describing whether we're on windows and if admin rights exist
-   */
-  checkWindowsElevation: function (ui) {
-    return new Promise(function (resolve) {
-      if (/^win/.test(process.platform)) {
-        exec('NET SESSION', function (error, stdout, stderr) {
-          var elevated = (!stderr || stderr.length === 0);
-
-          if (!elevated && ui && ui.writeLine) {
-            ui.writeLine(chalk.yellow('\nRunning without elevated rights. ' +
-              'Running Ember CLI "as Administrator" increases performance significantly.'));
-            ui.writeLine('See ember-cli.com/user-guide/#windows for details.\n');
-          }
-
-          resolve({
-            windows: true,
-            elevated: elevated
-          });
-        });
-      } else {
-        resolve({
-          windows: false,
-          elevated: null
-        });
+
+module.exports = WindowsSymlinkChecker;
+
+/**
+ *
+ * On windows users will have a much better experience if symlinks are enabled
+ * an usable. This object when queried informs windows users, if they can
+ * improve there build performance, and how.
+ *
+ *  > Windows vista: nothing we can really do, so we fall back to junctions for folders + copying of files
+ *  <= Windows vista: symlinks are available but using them is somewhat tricky
+ *    * if the users is an admin, the process needed to have been started with elevated privs
+ *    * if the user is not an admin, a specific setting needs to be enabled
+ *  <= Windows 10 Insiders build 14972
+ *    * if developer mode is enabled, symlinks "just work"
+ *    * https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10
+ *
+ * ```js
+ * let checker = WindowsSymlinkChecker;
+ * let {
+ *   windows,
+ *   elevated
+ * } = await = checker.checkIfSymlinksNeedToBeEnabled(); // aslso emits helpful warnings
+ * ```
+ *
+ * @public
+ * @class WindowsSymlinkChecker
+ */
+function WindowsSymlinkChecker(ui, isWindows, canSymlink, exec) {
+  this.ui = ui;
+  this.isWindows = isWindows;
+  this.canSymlink = canSymlink;
+  this.exec = exec;
+}
+
+/**
+ *
+ * if not windows, will fulfill with:
+ *  `{ windows: false, elevated: null)`
+ *
+ * if windows, and elevated will fulfill with:
+ *  `{ windows: false, elevated: true)`
+ *
+ * if windows, and is NOT elevated will fulfill with:
+ *  `{ windows: false, elevated: false)`
+ *
+ *  will include heplful warning, so that users know (if possible) how to
+ *  achieve better windows build performance
+ *
+ * @public
+ * @method checkIfSymlinksNeedToBeEnabled
+ * @return {Promise<Object>} Object describing whether we're on windows and if admin rights exist
+ */
+WindowsSymlinkChecker.checkIfSymlinksNeedToBeEnabled = function(ui) {
+  return this._setup(ui).checkIfSymlinksNeedToBeEnabled();
+};
+
+/**
+ * sets up a WindowsSymlinkChecker
+ *
+ * providing it with defaults for:
+ *
+ * * if we are on windows
+ * * if we can symlink
+ * * a reference to exec
+ *
+ * @private
+ * @method _setup
+ * @param UI {UI}
+ * @return {WindowsSymlinkChecker}
+ */
+WindowsSymlinkChecker._setup = function(ui) {
+  var exec          = require('child_process').exec;
+  var symlinkOrCopy = require('symlink-or-copy');
+
+  return new WindowsSymlinkChecker(ui, /^win/.test(process.platform), symlinkOrCopy.canSymlink, exec)
+};
+
+/**
+ * @public
+ * @method checkIfSymlinksNeedToBeEnabled
+ * @return {Promise<Object>} Object describing whether we're on windows and if admin rights exist
+ */
+WindowsSymlinkChecker.prototype.checkIfSymlinksNeedToBeEnabled = function() {
+  return new Promise(function(resolve) {
+    if (!this.isWindows) {
+      resolve({
+        windows: false,
+        elevated: null
+      });
+    } else if (this.canSymlink) {
+      resolve({
+        windows: true,
+        elevated: null
+      });
+    } else {
+      resolve(this._checkForElevatedRights(this.ui));
+    }
+  }.bind(this));
+};
+
+/**
+ *
+ * Uses the eon-old command NET SESSION to determine whether or not the
+ * current user has elevated rights (think sudo, but Windows).
+ *
+ * @private
+ * @method _checkForElevatedRights
+ * @param  {Object} ui - ui object used to call writeLine();
+ * @return {Object} Object describing whether we're on windows and if admin rights exist
+ */
+WindowsSymlinkChecker.prototype._checkForElevatedRights = function () {
+  var ui = this.ui;
+  var exec = this.exec;
+
+  return new Promise(function (resolve) {
+    exec('NET SESSION', function (error, stdout, stderr) {
+      var elevated = (!stderr || stderr.length === 0);
+
+      if (!elevated) {
+        ui.writeLine(chalk.yellow('\nRunning without permission to symlink will degrade build peformance.'));
+        ui.writeLine('See http://ember-cli.com/user-guide/#windows for details.\n');
       }
+
+      resolve({
+        windows: true,
+        elevated: elevated
+      });
     });
-  }
+  });
 };
diff --git a/package.json b/package.json
index d310bd458..698b99e3a 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
     "semver": "^5.1.1",
     "silent-error": "^1.0.0",
     "sort-package-json": "^1.4.0",
-    "symlink-or-copy": "^1.0.1",
+    "symlink-or-copy": "^1.1.8",
     "temp": "0.8.3",
     "testem": "^1.8.1",
     "tiny-lr": "^1.0.3",


Test patch:
diff --git a/blueprints/app/files/tests/.jshintrc b/blueprints/app/files/tests/.jshintrc
new file mode 100644
index 000000000..d2bd11343
--- /dev/null
+++ b/blueprints/app/files/tests/.jshintrc
@@ -0,0 +1,52 @@
+{
+  "predef": [
+    "document",
+    "window",
+    "location",
+    "setTimeout",
+    "$",
+    "-Promise",
+    "define",
+    "console",
+    "visit",
+    "exists",
+    "fillIn",
+    "click",
+    "keyEvent",
+    "triggerEvent",
+    "find",
+    "findWithAssert",
+    "wait",
+    "DS",
+    "andThen",
+    "currentURL",
+    "currentPath",
+    "currentRouteName"
+  ],
+  "node": false,
+  "browser": false,
+  "boss": true,
+  "curly": true,
+  "debug": false,
+  "devel": false,
+  "eqeqeq": true,
+  "evil": true,
+  "forin": false,
+  "immed": false,
+  "laxbreak": false,
+  "newcap": true,
+  "noarg": true,
+  "noempty": false,
+  "nonew": false,
+  "nomen": false,
+  "onevar": false,
+  "plusplus": false,
+  "regexp": false,
+  "undef": true,
+  "sub": true,
+  "strict": false,
+  "white": false,
+  "eqnull": true,
+  "esversion": 6,
+  "unused": true
+}
diff --git a/lib/commands/test.js b/lib/commands/test.js
index 8cc84297c..9f97ac06a 100644
--- a/lib/commands/test.js
+++ b/lib/commands/test.js
@@ -5,7 +5,7 @@ var Watcher     = require('../models/watcher');
 var Builder     = require('../models/builder');
 var SilentError = require('silent-error');
 var path        = require('path');
-var win         = require('../utilities/windows-admin');
+var Win         = require('../utilities/windows-admin');
 var existsSync  = require('exists-sync');
 var assign      = require('ember-cli-lodash-subset').assign;
 var defaultPort = 7357;
@@ -128,7 +128,7 @@ module.exports = Command.extend({
       project: this.project
     };
 
-    return win.checkWindowsElevation(this.ui).then(function() {
+    return Win.checkIfSymlinksNeedToBeEnabled(this.ui).then(function() {
       var session;
 
       if (commandOptions.server) {
diff --git a/tests/acceptance/addon-dummy-generate-test.js b/tests/acceptance/addon-dummy-generate-test.js
index 3ed4504cc..e34d52cee 100644
--- a/tests/acceptance/addon-dummy-generate-test.js
+++ b/tests/acceptance/addon-dummy-generate-test.js
@@ -43,13 +43,7 @@ describe('Acceptance: ember generate in-addon-dummy', function() {
       'my-addon',
       '--skip-npm',
       '--skip-bower'
-    ]).then(addJSHint);
-  }
-
-  function addJSHint() {
-    var pkg = fs.readJsonSync('package.json');
-    pkg.devDependencies['ember-cli-jshint'] = '*';
-    fs.writeJsonSync('package.json', pkg);
+    ]);
   }
 
   function generateInAddon(args) {
@@ -263,6 +257,7 @@ describe('Acceptance: ember generate in-addon-dummy', function() {
   it('dummy server', function() {
     return generateInAddon(['server', '--dummy']).then(function() {
       expect(file('server/index.js')).to.exist;
+      expect(file('server/.jshintrc')).to.exist;
     });
   });
 
diff --git a/tests/acceptance/addon-generate-test.js b/tests/acceptance/addon-generate-test.js
index 5f8c7ddd8..0658129d6 100644
--- a/tests/acceptance/addon-generate-test.js
+++ b/tests/acceptance/addon-generate-test.js
@@ -43,13 +43,7 @@ describe('Acceptance: ember generate in-addon', function() {
       name,
       '--skip-npm',
       '--skip-bower'
-    ]).then(addJSHint);
-  }
-
-  function addJSHint() {
-    var pkg = fs.readJsonSync('package.json');
-    pkg.devDependencies['ember-cli-jshint'] = '*';
-    fs.writeJsonSync('package.json', pkg);
+    ]);
   }
 
   function generateInAddon(args) {
@@ -292,6 +286,7 @@ describe('Acceptance: ember generate in-addon', function() {
   it('in-addon server', function() {
     return generateInAddon(['server']).then(function() {
       expect(file('server/index.js')).to.exist;
+      expect(file('server/.jshintrc')).to.exist;
     });
   });
 
diff --git a/tests/acceptance/destroy-test.js b/tests/acceptance/destroy-test.js
index 6bab9c81e..8859c80d2 100644
--- a/tests/acceptance/destroy-test.js
+++ b/tests/acceptance/destroy-test.js
@@ -305,6 +305,7 @@ describe('Acceptance: ember destroy', function() {
       .then(function() { return destroy(['http-mock', 'foo']); })
       .then(function() {
         expect(file('server/index.js')).to.exist;
+        expect(file('server/.jshintrc')).to.exist;
       });
   });
 
diff --git a/tests/acceptance/generate-test.js b/tests/acceptance/generate-test.js
index 1bceb5898..373b052b9 100755
--- a/tests/acceptance/generate-test.js
+++ b/tests/acceptance/generate-test.js
@@ -16,7 +16,6 @@ var mkTmpDirIn       = require('../../lib/utilities/mk-tmp-dir-in');
 var chai = require('../chai');
 var expect = chai.expect;
 var file = chai.file;
-var dir = chai.dir;
 
 describe('Acceptance: ember generate', function() {
   this.timeout(20000);
@@ -49,13 +48,7 @@ describe('Acceptance: ember generate', function() {
       '--name=my-app',
       '--skip-npm',
       '--skip-bower'
-    ]).then(addJSHint);
-  }
-
-  function addJSHint() {
-    var pkg = fs.readJsonSync('package.json');
-    pkg.devDependencies['ember-cli-jshint'] = '*';
-    fs.writeJsonSync('package.json', pkg);
+    ]);
   }
 
   function generate(args) {
@@ -403,6 +396,7 @@ describe('Acceptance: ember generate', function() {
   it('server', function() {
     return generate(['server']).then(function() {
       expect(file('server/index.js')).to.exist;
+      expect(file('server/.jshintrc')).to.exist;
     });
   });
 
@@ -414,7 +408,7 @@ describe('Acceptance: ember generate', function() {
 
   it('lib', function() {
     return generate(['lib']).then(function() {
-      expect(dir('lib')).to.exist;
+      expect(file('lib/.jshintrc')).to.exist;
     });
   });
 
diff --git a/tests/acceptance/pods-generate-test.js b/tests/acceptance/pods-generate-test.js
index de10a4ee1..cea1f6893 100644
--- a/tests/acceptance/pods-generate-test.js
+++ b/tests/acceptance/pods-generate-test.js
@@ -49,7 +49,7 @@ describe('Acceptance: ember generate pod', function() {
       '--name=my-app',
       '--skip-npm',
       '--skip-bower'
-    ]).then(addJSHint);
+    ]);
   }
 
   function initAddon() {
@@ -58,13 +58,7 @@ describe('Acceptance: ember generate pod', function() {
       'my-addon',
       '--skip-npm',
       '--skip-bower'
-    ]).then(addJSHint);
-  }
-
-  function addJSHint() {
-    var pkg = fs.readJsonSync('package.json');
-    pkg.devDependencies['ember-cli-jshint'] = '*';
-    fs.writeJsonSync('package.json', pkg);
+    ]);
   }
 
   function initInRepoAddon() {
diff --git a/tests/acceptance/smoke-test-slow.js b/tests/acceptance/smoke-test-slow.js
index 17f0641f3..4d10d5ec8 100644
--- a/tests/acceptance/smoke-test-slow.js
+++ b/tests/acceptance/smoke-test-slow.js
@@ -171,8 +171,9 @@ describe('Acceptance: smoke-test', function() {
 
         output = output.join(EOL);
 
+        expect(output).to.match(/JSHint/, 'JSHint should be run');
         expect(output).to.match(/fail\s+0/, 'no failures');
-        expect(output).to.match(/pass\s+2/, '2 passing');
+        expect(output).to.match(/pass\s+12/, '1 passing');
       });
   });
 
diff --git a/tests/unit/blueprints/lib-test.js b/tests/unit/blueprints/lib-test.js
index bb675c521..85bb1340b 100644
--- a/tests/unit/blueprints/lib-test.js
+++ b/tests/unit/blueprints/lib-test.js
@@ -22,24 +22,24 @@ describe('Acceptance: ember generate and destroy lib', function() {
       .then(function() {
         return emberGenerateDestroy(args, function(file) {
           expect(dir('lib')).to.exist;
-          expect(file('lib/.jshintrc')).to.not.exist;
+          expect(file('lib/.jshintrc')).to.exist;
         });
       });
   });
 
-  it('lib foo with ember-cli-jshint', function() {
+  it('lib foo without ember-cli-jshint', function() {
     var args = ['lib', 'foo'];
 
     return emberNew()
       .then(function() {
         return modifyPackages([
-          {name: 'ember-cli-jshint', dev: true},
+          {name: 'ember-cli-jshint', delete: true},
         ]);
       })
       .then(function() {
         return emberGenerateDestroy(args, function(file) {
           expect(dir('lib')).to.exist;
-          expect(file('lib/.jshintrc')).to.exist;
+          expect(file('lib/.jshintrc')).to.not.exist;
         });
       });
   });
diff --git a/tests/unit/blueprints/server-test.js b/tests/unit/blueprints/server-test.js
index 16dd72fa9..915bdabff 100644
--- a/tests/unit/blueprints/server-test.js
+++ b/tests/unit/blueprints/server-test.js
@@ -26,26 +26,26 @@ describe('Acceptance: ember generate and destroy server', function() {
       })
       .then(function() {
         expect(file('server/index.js')).to.contain('module.exports = function(app) {');
-        expect(file('server/.jshintrc')).to.not.exist;
+        expect(file('server/.jshintrc')).to.exist;
 
         // TODO: assert that `morgan` and `glob` dependencies were installed
       });
   });
 
-  it('server with ember-cli-jshint', function() {
+  it('server without ember-cli-jshint', function() {
     var args = ['server'];
 
     return emberNew()
       .then(function() {
         return modifyPackages([
-          {name: 'ember-cli-jshint', dev: true},
+          {name: 'ember-cli-jshint', delete: true},
         ]);
       })
       .then(function() {
         return emberGenerate(args);
       })
       .then(function() {
-        expect(file('server/.jshintrc')).to.exist;
+        expect(file('server/.jshintrc')).to.not.exist;
       });
   });
 });
diff --git a/tests/unit/commands/init-test.js b/tests/unit/commands/init-test.js
index 4511d874d..42226d89d 100644
--- a/tests/unit/commands/init-test.js
+++ b/tests/unit/commands/init-test.js
@@ -21,7 +21,6 @@ describe('init command', function() {
     ui = new MockUI();
     analytics = new MockAnalytics();
     tasks = {
-      AddonInstall: Task.extend({}),
       InstallBlueprint: Task.extend({}),
       NpmInstall: Task.extend({}),
       BowerInstall: Task.extend({})
diff --git a/tests/unit/models/addon-test.js b/tests/unit/models/addon-test.js
index 658ba0ec5..9d0a90908 100644
--- a/tests/unit/models/addon-test.js
+++ b/tests/unit/models/addon-test.js
@@ -619,33 +619,6 @@ describe('models/addon.js', function() {
         addon.compileTemplates();
       }).not.to.throw();
     });
-
-    it('should not call _getAddonTemplatesTreeFiles when default treePath is used', function() {
-      addon.root = path.join(fixturePath, 'with-empty-addon-templates');
-      var wasCalled = false;
-      addon._getAddonTemplatesTreeFiles = function() {
-        wasCalled = true;
-        return [];
-      };
-
-      addon.compileTemplates();
-
-      expect(wasCalled).to.not.be.ok;
-    });
-
-    it('should call _getAddonTemplatesTreeFiles when custom treePaths[\'addon-templates\'] is used', function() {
-      addon.root = path.join(fixturePath, 'with-empty-addon-templates');
-      addon.treePaths['addon-templates'] = 'foo';
-      var wasCalled = false;
-      addon._getAddonTemplatesTreeFiles = function() {
-        wasCalled = true;
-        return [];
-      };
-
-      addon.compileTemplates();
-
-      expect(wasCalled).to.be.ok;
-    });
   });
 
   describe('addonDiscovery', function() {
diff --git a/tests/unit/utilities/windows-admin-test.js b/tests/unit/utilities/windows-admin-test.js
index 1adf63c37..ea9a455c0 100644
--- a/tests/unit/utilities/windows-admin-test.js
+++ b/tests/unit/utilities/windows-admin-test.js
@@ -1,41 +1,106 @@
 'use strict';
 
-var win = require('../../../lib/utilities/windows-admin');
+var WindowsSymlinkChecker = require('../../../lib/utilities/windows-admin');
 var expect = require('chai').expect;
 var MockUI = require('console-ui/mock');
+var td = require('testdouble');
+var symlinkOrCopy = require('symlink-or-copy');
 
 describe('windows-admin', function() {
-  before(function() {
-    this.originalPlatform = process.platform;
-  });
+  var ui;
 
-  after(function () {
-    Object.defineProperty(process, 'platform', {
-      value: this.originalPlatform
-    });
+  beforeEach(function() {
+    ui = new MockUI();
   });
 
-  it('attempts to determine admin rights if Windows', function(done) {
-    Object.defineProperty(process, 'platform', {
-      value: 'win'
+  describe('on windows', function() {
+    var isWindows = true;
+
+    describe('can symlink', function() {
+      var canSymlink = true;
+
+      it('attempts to determine admin rights if Windows', function() {
+        var exec = td.function('exec');
+
+        var checker = new WindowsSymlinkChecker(ui, isWindows, canSymlink, exec);
+
+        return checker.checkIfSymlinksNeedToBeEnabled().then(function(result) {
+          expect(result).to.be.ok;
+          expect(result.windows).to.be.eql(true);
+          expect(result.elevated).to.be.eql(null);
+          td.verify(exec(), { times: 0 });
+          expect(ui.output).to.eql('');
+        });
+      });
     });
 
-    win.checkWindowsElevation(new MockUI()).then(function (result) {
-      expect(result).to.be.ok;
-      expect(result.windows).to.be.true;
-      done();
+    describe('cannot symlink', function() {
+      var canSymlink = false;
+
+      describe('attempts to determine admin rights', function() {
+        it('gets STDERR during NET SESSION exec', function() {
+          var exec = td.function('exec');
+          td.when(exec('NET SESSION', td.callback(null, null, 'error'))).thenReturn();
+
+          var checker = new WindowsSymlinkChecker(ui, isWindows, canSymlink, exec);
+
+          return checker.checkIfSymlinksNeedToBeEnabled().then(function(result) {
+            expect(result.windows, 'is windows').to.be.eql(true)
+            expect(result.elevated, 'is not elevated').to.be.eql(false);
+            td.verify(exec(td.matchers.contains('NET SESSION'), td.matchers.anything()), { times: 1 });
+            expect(ui.output).to.match(/Running without permission to symlink will degrade build peformance/);
+            expect(ui.output).to.match(/See http\:\/\/ember-cli\.com\/user-guide\/#windows for details./);
+          });
+        });
+
+        it('gets no stdrrduring NET  SESSION exec', function() {
+          var exec = td.function('exec');
+          td.when(exec('NET SESSION', td.callback(null, null, null))).thenReturn();
+
+          var checker = new WindowsSymlinkChecker(ui, isWindows, canSymlink, exec);
+
+          return checker.checkIfSymlinksNeedToBeEnabled().then(function(result) {
+            expect(result.windows, 'is windows').to.be.eql(true)
+            expect(result.elevated, 'is elevated').to.be.eql(true);
+            td.verify(exec(td.matchers.contains('NET SESSION'), td.matchers.anything()), { times: 1 });
+            expect(ui.output).to.eql('');
+          });
+        });
+      });
     });
   });
 
-  it('does not attempt to determine admin rights if not on Windows', function(done) {
-    Object.defineProperty(process, 'platform', {
-      value: 'MockOS'
+  describe('on linux', function() {
+    var isWindows = false;
+    var canSymlink = true;
+
+    it('does not attempt to determine admin', function() {
+      var exec = td.function('exec');
+      var checker = new WindowsSymlinkChecker(ui, isWindows, canSymlink, exec);
+
+      return checker.checkIfSymlinksNeedToBeEnabled().then(function(result) {
+        expect(result.windows).to.be.eql(false);
+        expect(result.elevated).to.be.eql(null);
+        expect(td.explain(exec).callCount).to.eql(0);
+        expect(ui.output).to.eql('');
+      });
     });
+  });
+
+  describe('on darwin', function() {
+    var isWindows = false;
+    var canSymlink = true;
+
+    it('does not attempt to determine admin', function() {
+      var checker = new WindowsSymlinkChecker(ui, isWindows, canSymlink /* exec spy */);
+      var exec = td.function('exec');
 
-    win.checkWindowsElevation(new MockUI()).then(function (result) {
-      expect(result).to.be.ok;
-      expect(result.windows).to.be.false;
-      done();
+      return checker.checkIfSymlinksNeedToBeEnabled().then(function(result) {
+        expect(result.windows).to.be.eql(false);
+        expect(result.elevated).to.be.eql(null);
+        expect(td.explain(exec).callCount).to.eql(0);
+        expect(ui.output).to.eql('');
+      });
     });
   });
 });