Repository: ember-cli/ember-try
Pull request #120 (2017-03-22 05:04:35)

Add tests for using yarn when available
- Refactors as necessary for tests + small fixes
- Add to README about yarn usage


Patch:
diff --git a/README.md b/README.md
index cb12345..46ad86a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ the `--config-path` option.
   ember try:each --config-path="config/legacy-scenarios.js"
 ```
 
-If you need to know the scenario that is being ran (i.e. to customize a test output file name) you can use the `EMBER_TRY_CURRENT_SCENARIO`
+If you need to know the scenario that is being run (i.e. to customize a test output file name) you can use the `EMBER_TRY_CURRENT_SCENARIO`
 environment variable.
 
 #### `ember try:one <scenario> (...options) --- <command (Default: test)>`
@@ -100,7 +100,7 @@ To keep this from getting out of hand, `ember-try` will limit the versions of Em
 
 ##### Configuration Files
 
-Configuration will be read from a file in your ember app in `config/ember-try.js`. It should look like:
+Configuration will be read from a file in your ember app in `config/ember-try.js`. Here are the possible options:
 
 ```js
 /*jshint node:true*/
@@ -230,6 +230,16 @@ If no `config/ember-try.js` file is present, the default config will be used. Th
   ]
 }
 ```
+
+##### A note on npm scenarios / "What about yarn?"
+
+If `yarn` is available globally on the system the `ember-try` command is run on, all npm scenarios will use `yarn` for install with the `--no-lockfile` option. 
+If the project has a `yarn.lock`, it will be used *only* on cleanup to restore the project to a clean state. 
+ 
+###### But I use yarn, why `--no-lockfile`?
+
+When testing various scenarios, it's important to "float" dependencies so that the scenarios are run with the latest satisfying versions of dependencies a user of the project would get.
+
 ### Video
 [![How to use EmberTry](https://i.vimeocdn.com/video/559399937_500.jpg)](https://vimeo.com/157688157)
 
diff --git a/lib/dependency-manager-adapters/npm.js b/lib/dependency-manager-adapters/npm.js
index 59f48fa..848e549 100644
--- a/lib/dependency-manager-adapters/npm.js
+++ b/lib/dependency-manager-adapters/npm.js
@@ -13,21 +13,10 @@ module.exports = CoreObject.extend({
   init: function() {
     this._super.apply(this, arguments);
     this.run = this.run || require('../utils/run');
-    this.adapterInit();
+    this._setYarnAvailability();
   },
-  adapterInit: function() {
-    try {
-      execFileSync('yarn', [].concat(['--version']));
-      this.useYarn = true;
-      debug('Using yarn as dependency manager');
-    } catch (e) {
-      this.useYarn = false;
-      debug('Using npm as dependency manager');
-    }
-  },
-  useYarn: false,
+  useYarnCommand: false,
   yarnLock: 'yarn.lock',
-  yarnLockBackupFileName: 'yarn.lock.ember-try',
   configKey: 'npm',
   packageJSON: 'package.json',
   packageJSONBackupFileName: 'package.json.ember-try',
@@ -51,14 +40,14 @@ module.exports = CoreObject.extend({
     debug('Write package.json with: \n', JSON.stringify(newPackageJSON));
 
     fs.writeFileSync(packageJSONFile, JSON.stringify(newPackageJSON, null, 2));
-    return adapter._install(this.useYarn).then(function() {
+    return adapter._install().then(function() {
       var deps = extend({}, depSet.dependencies || {}, depSet.devDependencies || {});
       var currentDeps = Object.keys(deps).map(function(dep) {
         return {
           name: dep,
           versionExpected: deps[dep],
           versionSeen: adapter._findCurrentVersionOf(dep),
-          packageManager: adapter.useYarn ? 'yarn' : 'npm'
+          packageManager: adapter.useYarnCommand ? 'yarn' : 'npm'
         };
       });
 
@@ -69,25 +58,35 @@ module.exports = CoreObject.extend({
   },
   cleanup: function() {
     var adapter = this;
-    var backupYarnLockPresent = fs.existsSync(path.join(this.cwd, this.yarnLockBackupFileName));
-    return this._restoreOriginalDependencies(backupYarnLockPresent).then(function() {
+
+    return this._restoreOriginalDependencies().then(function() {
       debug('Remove backup package.json and node_modules');
 
       var cleanupTasks = [rimraf(path.join(adapter.cwd, adapter.packageJSONBackupFileName)),
         rimraf(path.join(adapter.cwd, adapter.nodeModulesBackupLocation))];
 
-      if (backupYarnLockPresent) {
-        cleanupTasks.push(rimraf(path.join(adapter.cwd, adapter.yarnLockBackupFileName)));
-      }
-
       return RSVP.all(cleanupTasks);
     }).catch(function(e) {
       console.log('Error cleaning up npm scenario:', e);
     })
     .then(function() {
-      return adapter._install(false);
+      var yarnLockPresent = fs.existsSync(path.join(adapter.cwd, adapter.yarnLock));
+      return adapter._install({ useYarnLock: yarnLockPresent });
     });
   },
+  _setYarnAvailability: function() {
+    try {
+      this._runYarnCheck();
+      this.useYarnCommand = true;
+      debug('Using yarn as dependency manager');
+    } catch (e) {
+      this.useYarnCommand = false;
+      debug('Using npm as dependency manager');
+    }
+  },
+  _runYarnCheck: function() {
+    execFileSync('yarn', [].concat(['--version']));
+  },
   _findCurrentVersionOf: function(packageName) {
     var filename = path.join(this.cwd, this.nodeModules, packageName, this.packageJSON);
     if (fs.existsSync(filename)) {
@@ -96,21 +95,22 @@ module.exports = CoreObject.extend({
       return null;
     }
   },
-  _install: function(noLock) {
+  _install: function(options) {
     var adapter = this;
-    var options = this.managerOptions || [];
+    var opts = extend({ useYarnLock: false }, options);
+    var mgrOptions = this.managerOptions || [];
 
-    debug('Run npm install with options %s', options);
+    debug('Run npm install with options %s', mgrOptions);
 
-    var cmd = this.useYarn ? 'yarn' : 'npm';
-    if (this.useYarn && noLock && options.indexOf('--no-lockfile') === -1) {
-      options = options.concat(['--no-lockfile']);
+    var cmd = this.useYarnCommand ? 'yarn' : 'npm';
+    if (this.useYarnCommand && !opts.useYarnLock && mgrOptions.indexOf('--no-lockfile') === -1) {
+      mgrOptions = mgrOptions.concat(['--no-lockfile']);
     }
 
-    return this.run(cmd, [].concat(['install'], options), { cwd: this.cwd }).then(function() {
-      if (!adapter.useYarn) {
+    return this.run(cmd, [].concat(['install'], mgrOptions), { cwd: this.cwd }).then(function() {
+      if (!adapter.useYarnCommand) {
         debug('Run npm prune');
-        return adapter.run(adapter.configKey, [].concat(['prune'], options), { cwd: adapter.cwd });
+        return adapter.run(adapter.configKey, [].concat(['prune'], mgrOptions), { cwd: adapter.cwd });
       }
     });
   },
@@ -139,7 +139,7 @@ module.exports = CoreObject.extend({
       }
     });
   },
-  _restoreOriginalDependencies: function(backupYarnLockPresent) {
+  _restoreOriginalDependencies: function() {
     var copy = RSVP.denodeify(fs.copy);
 
     debug('Restoring original package.json and node_modules');
@@ -151,11 +151,6 @@ module.exports = CoreObject.extend({
            path.join(this.cwd, this.nodeModules), { clobber: true })
     ];
 
-    if (backupYarnLockPresent) {
-      restoreTasks.push(copy(path.join(this.cwd, this.yarnLockBackupFileName),
-        path.join(this.cwd, this.yarnLock)));
-    }
-
     return RSVP.all(restoreTasks);
   },
   _backupOriginalDependencies: function() {
@@ -169,11 +164,6 @@ module.exports = CoreObject.extend({
       copy(path.join(this.cwd, this.nodeModules),
            path.join(this.cwd, this.nodeModulesBackupLocation), { clobber: true })];
 
-    if (fs.existsSync(path.join(this.cwd, this.yarnLock)) && this.useYarn) {
-      backupTasks.push(copy(path.join(this.cwd, this.yarnLock),
-        path.join(this.cwd, this.yarnLockBackupFileName)));
-    }
-
     return RSVP.all(backupTasks);
   }
 });


Test patch:
diff --git a/test/dependency-manager-adapters/npm-adapter-test.js b/test/dependency-manager-adapters/npm-adapter-test.js
index 2854c26..8fa1175 100644
--- a/test/dependency-manager-adapters/npm-adapter-test.js
+++ b/test/dependency-manager-adapters/npm-adapter-test.js
@@ -15,6 +15,12 @@ var root = process.cwd();
 var tmproot = path.join(root, 'tmp');
 var tmpdir;
 
+var failedYarnCheck = function() {
+  throw new Error('Yarn not available');
+};
+
+var passedYarnCheck = function() {};
+
 describe('npmAdapter', function() {
   beforeEach(function() {
     tmpdir = tmp.in(tmproot);
@@ -31,7 +37,10 @@ describe('npmAdapter', function() {
       fs.mkdirSync('node_modules');
       writeJSONFile('node_modules/prove-it.json', { originalNodeModules: true });
       writeJSONFile('package.json', { originalPackageJSON: true });
-      return new NpmAdapter({ cwd: tmpdir }).setup().then(function() {
+      return new NpmAdapter({
+        cwd: tmpdir,
+        _runYarnCheck: failedYarnCheck
+      }).setup().then(function() {
         assertFileContainsJSON('package.json.ember-try', { originalPackageJSON: true });
         assertFileContainsJSON('.node_modules.ember-try/prove-it.json', { originalNodeModules: true });
       }).catch(function(err) {
@@ -42,68 +51,129 @@ describe('npmAdapter', function() {
   });
 
   describe('#_install', function() {
-    it('runs npm prune and npm install', function() {
-      writeJSONFile('package.json', fixturePackage);
-      var runCount = 0;
-      var stubbedRun = generateMockRun([{
-        command: 'npm install',
-        callback: function(command, args, opts) {
-          runCount++;
-          expect(opts).to.have.property('cwd', tmpdir);
-          return RSVP.resolve();
-        }
-      }, {
-        command: 'npm prune',
-        callback: function(command, args, opts) {
-          runCount++;
-          expect(opts).to.have.property('cwd', tmpdir);
-          return RSVP.resolve();
-        }
-      }], { allowPassthrough: false });
+    describe('without yarn', function() {
+      it('runs npm prune and npm install', function() {
+        writeJSONFile('package.json', fixturePackage);
+        var runCount = 0;
+        var stubbedRun = generateMockRun([{
+          command: 'npm install',
+          callback: function(command, args, opts) {
+            runCount++;
+            expect(opts).to.have.property('cwd', tmpdir);
+            return RSVP.resolve();
+          }
+        }, {
+          command: 'npm prune',
+          callback: function(command, args, opts) {
+            runCount++;
+            expect(opts).to.have.property('cwd', tmpdir);
+            return RSVP.resolve();
+          }
+        }], { allowPassthrough: false });
 
-      return new NpmAdapter({
-        cwd: tmpdir,
-        run: stubbedRun,
-        adapterInit() {
-          this.configKey = 'npm';
-        }
-      })._install().then(function() {
-        expect(runCount).to.equal(2, 'Both commands should run');
-      }).catch(function(err) {
-        console.log(err);
-        expect(true).to.equal(false, 'Error should not happen');
+        return new NpmAdapter({
+          cwd: tmpdir,
+          run: stubbedRun,
+          _runYarnCheck: failedYarnCheck
+        })._install().then(function() {
+          expect(runCount).to.equal(2, 'Both commands should run');
+        }).catch(function(err) {
+          console.log(err);
+          expect(true).to.equal(false, 'Error should not happen');
+        });
+      });
+
+      it('uses managerOptions for npm commands', function() {
+        writeJSONFile('package.json', fixturePackage);
+        var runCount = 0;
+        var stubbedRun = generateMockRun([{
+          command: 'npm install --no-shrinkwrap=true',
+          callback: function() {
+            runCount++;
+            return RSVP.resolve();
+          }
+        }, {
+          command: 'npm prune --no-shrinkwrap=true',
+          callback: function() {
+            runCount++;
+            return RSVP.resolve();
+          }
+        }], { allowPassthrough: false });
+
+        return new NpmAdapter({
+          cwd: tmpdir,
+          run: stubbedRun,
+          _runYarnCheck: failedYarnCheck,
+          managerOptions: ['--no-shrinkwrap=true']
+        })._install().then(function() {
+          expect(runCount).to.equal(2, 'Both commands should run');
+        }).catch(function(err) {
+          console.log(err);
+          expect(true).to.equal(false, 'Error should not happen');
+        });
       });
     });
+    describe('with yarn', function() {
+      it('runs yarn install', function() {
+        writeJSONFile('package.json', fixturePackage);
+        var runCount = 0;
+        var stubbedRun = generateMockRun([{
+          command: 'yarn install --no-lockfile',
+          callback: function(command, args, opts) {
+            runCount++;
+            expect(opts).to.have.property('cwd', tmpdir);
+            return RSVP.resolve();
+          }
+        }], { allowPassthrough: false });
 
-    it('uses managerOptions for npm commands', function() {
-      writeJSONFile('package.json', fixturePackage);
-      var runCount = 0;
-      var stubbedRun = generateMockRun([{
-        command: 'npm install --no-shrinkwrap=true',
-        callback: function() {
-          runCount++;
-          return RSVP.resolve();
-        }
-      }, {
-        command: 'npm prune --no-shrinkwrap=true',
-        callback: function() {
-          runCount++;
-          return RSVP.resolve();
-        }
-      }], { allowPassthrough: false });
+        return new NpmAdapter({
+          cwd: tmpdir,
+          run: stubbedRun,
+          _runYarnCheck: passedYarnCheck
+        })._install().then(function() {
+          expect(runCount).to.equal(1, 'Only yarn install should run');
+        });
+      });
 
-      return new NpmAdapter({
-        cwd: tmpdir,
-        run: stubbedRun,
-        managerOptions: ['--no-shrinkwrap=true'],
-        adapterInit() {
-          this.configKey = 'npm';
-        }
-      })._install().then(function() {
-        expect(runCount).to.equal(2, 'Both commands should run');
-      }).catch(function(err) {
-        console.log(err);
-        expect(true).to.equal(false, 'Error should not happen');
+      it('uses managerOptions for yarn commands', function() {
+        writeJSONFile('package.json', fixturePackage);
+        var runCount = 0;
+        var stubbedRun = generateMockRun([{
+          command: 'yarn install --flat --no-lockfile',
+          callback: function() {
+            runCount++;
+            return RSVP.resolve();
+          }
+        }], { allowPassthrough: false });
+
+        return new NpmAdapter({
+          cwd: tmpdir,
+          run: stubbedRun,
+          _runYarnCheck: passedYarnCheck,
+          managerOptions: ['--flat']
+        })._install().then(function() {
+          expect(runCount).to.equal(1, 'Only yarn install should run with manager options and --no-lockfile');
+        });
+      });
+
+      it('runs yarn install with lockfile if given option', function() {
+        writeJSONFile('package.json', fixturePackage);
+        var runCount = 0;
+        var stubbedRun = generateMockRun([{
+          command: 'yarn install',
+          callback: function() {
+            runCount++;
+            return RSVP.resolve();
+          }
+        }], { allowPassthrough: false });
+
+        return new NpmAdapter({
+          cwd: tmpdir,
+          run: stubbedRun,
+          _runYarnCheck: passedYarnCheck
+        })._install({ useYarnLock: true }).then(function() {
+          expect(runCount).to.equal(1, 'Only yarn install should run without --no-lockfile');
+        });
       });
     });
   });
@@ -165,6 +235,52 @@ describe('npmAdapter', function() {
       expect(resultJSON.devDependencies).to.not.have.property('ember-feature-flags');
     });
   });
+
+  describe('#_setYarnAvailability', function() {
+    it('sets useYarnCommand to true if yarn check does not raise', function() {
+      var npmAdapter = new NpmAdapter({
+        cwd: tmpdir,
+        _runYarnCheck: function() {}
+      });
+
+      npmAdapter._setYarnAvailability();
+
+      expect(npmAdapter.useYarnCommand).to.equal(true);
+    });
+
+    it('sets useYarnCommand to false if yarn check raises', function() {
+      var npmAdapter = new NpmAdapter({
+        cwd: tmpdir,
+        _runYarnCheck: function() {
+          throw new Error();
+        }
+      });
+
+      npmAdapter._setYarnAvailability();
+
+      expect(npmAdapter.useYarnCommand).to.equal(false);
+    });
+  });
+
+  describe('#cleanup', function() {
+    it('runs _install using yarn lock if present', function() {
+      fs.writeFileSync('yarn.lock', 'stuff');
+      var installOptions = {};
+      function mockInstall(options) {
+        installOptions = options;
+      }
+
+      return new NpmAdapter({
+        cwd: tmpdir,
+        _restoreOriginalDependencies: function() {
+          return RSVP.resolve();
+        },
+        _install: mockInstall
+      }).cleanup().then(function() {
+        expect(installOptions).to.eql({ useYarnLock: true });
+      });
+    });
+  });
 });
 
 function assertFileContainsJSON(filename, expectedObj) {