diff --git a/index.js b/index.js index a4585193..3ffe9f8f 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ var attachMiddleware = require('./lib/attach-middleware'); var config = require('./lib/config'); const walkSync = require('walk-sync'); const VersionChecker = require('ember-cli-version-checker'); +const concat = require('lodash.concat'); function requireBabelPlugin(pluginName) { let plugin = require(pluginName); @@ -22,6 +23,12 @@ function requireBabelPlugin(pluginName) { return plugin; } +function getPlugins(appOrAddon) { + let options = appOrAddon.options = appOrAddon.options || {}; + options.babel = options.babel || {}; + return options.babel.plugins = options.babel.plugins || []; +} + // Regular expression to extract the file extension from a path. const EXT_RE = /\.[^\.]+$/; @@ -45,11 +52,19 @@ module.exports = { let checker = new VersionChecker(this.parent).for('ember-cli-babel', 'npm'); if (checker.satisfies('>= 6.0.0')) { - this.IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul'); + const IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul'); + const exclude = this._getExcludes(); + const include = this._getIncludes(); + + concat( + this.app, + this._findCoveredAddon(), + this._findInRepoAddons() + ) + .filter(Boolean) + .map(getPlugins) + .forEach((plugins) => plugins.push([IstanbulPlugin, { exclude, include }])); - this._instrumentAppDirectory(); - this._instrumentAddonDirectory(); - this._instrumentInRepoAddonDirectories(); } else { this.project.ui.writeWarnLine( 'ember-cli-code-coverage: You are using an unsupported ember-cli-babel version,' + @@ -96,83 +111,71 @@ module.exports = { // Custom Methods /** - * Instrument the "app" directory. + * Thin wrapper around exists-sync that allows easy stubbing in tests + * @param {String} path - path to check existence of + * @returns {Boolean} whether or not path exists */ - _instrumentAppDirectory() { - const dir = path.join(this.project.root, 'app'); - let prefix = this.parent.isEmberCLIAddon() ? 'dummy' : this.parent.name(); - this._instrumentDirectory(this.app, dir, prefix); + _existsSync: function(path) { + return existsSync(path); }, /** - * Instrument the "addon" directory. + * Get project configuration + * @returns {Configuration} project configuration */ - _instrumentAddonDirectory() { - let addon = this._findCoveredAddon(); - if (addon) { - const dir = path.join(this.project.root, 'addon'); - this._instrumentDirectory(addon, dir, addon.name); - } + _getConfig: function() { + return config(this.project.configPath()); }, /** - * Instrument the in-repo-addon directories in "lib/*". + * Get paths to include for coverage + * @returns {Array} include paths */ - _instrumentInRepoAddonDirectories() { - const pkg = this.project.pkg; - if (pkg['ember-addon'] && pkg['ember-addon'].paths) { - pkg['ember-addon'].paths.forEach((addonPath) => { - let addonName = path.basename(addonPath); - let addonDir = path.join(this.project.root, addonPath); - let addon = this.project.findAddonByName(addonName); - let addonAppDir = path.join(addonDir, 'app'); - let addonAddonDir = path.join(addonDir, 'addon'); - this._instrumentDirectory(this.app, addonAppDir, this.parent.name()); - this._instrumentDirectory(addon, addonAddonDir, addonName); - }); - } + _getIncludes: function() { + return concat( + this._getIncludesForAppDirectory(), + this._getIncludesForAddonDirectory(), + this._getIncludesForInRepoAddonDirectories() + ).filter(Boolean); }, /** - * Instrument directory helper. - * @param {Object} appOrAddon The Ember app or addon config. - * @param {String} dir The path to the Ember app or addon. - * @param {String} modulePrefix The prefix to the ember module ('app', 'dummy' or the name of the addon). + * Get paths to include for covering the "app" directory. + * @returns {Array} include paths */ - _instrumentDirectory(appOrAddon, dir, modulePrefix) { - if (existsSync(dir)) { - let options = appOrAddon.options = appOrAddon.options || {}; - options.babel = options.babel || {}; - let plugins = options.babel.plugins = options.babel.plugins || []; - let include = this._getIncludes(dir, modulePrefix); - let plugin = plugins.find((plugin) => plugin[0] === this.IstanbulPlugin); - if (plugin) { // plugin already exists, so amend it rather than adding another one - plugin[1].include = plugin[1].include.concat(include); - } else { - plugins.push([this.IstanbulPlugin, { - exclude: this._getExcludes(), - include - }]); - } - - } + _getIncludesForAppDirectory: function() { + const dir = path.join(this.project.root, 'app'); + let prefix = this.parent.isEmberCLIAddon() ? 'dummy' : this.parent.name(); + return this._getIncludesForDir(dir, prefix); }, /** - * Thin wrapper around exists-sync that allows easy stubbing in tests - * @param {String} path - path to check existence of - * @returns {Boolean} whether or not path exists + * Get paths to include for covering the "addon" directory. + * @returns {Array} include paths */ - _existsSync: function(path) { - return existsSync(path); + _getIncludesForAddonDirectory: function() { + let addon = this._findCoveredAddon(); + if (addon) { + const dir = path.join(this.project.root, 'addon'); + return this._getIncludesForDir(dir, addon.name); + } }, /** - * Get project configuration - * @returns {Configuration} project configuration + * Get paths to include for covering the in-repo-addon directories in "lib/*". + * @returns {Array} include paths */ - _getConfig: function() { - return config(this.project.configPath()); + _getIncludesForInRepoAddonDirectories: function() { + return this._findInRepoAddons().reduce((acc, addon) => { + let addonDir = path.join(this.project.root, 'lib', addon.name); + let addonAppDir = path.join(addonDir, 'app'); + let addonAddonDir = path.join(addonDir, 'addon'); + return concat( + acc, + this._getIncludesForDir(addonAppDir, this.parent.name()), + this._getIncludesForDir(addonAddonDir, addon.name) + ); + }, []); }, /** @@ -181,7 +184,7 @@ module.exports = { * @param {String} prefix The prefix to the ember module ('app', 'dummy' or the name of the addon). * @returns {Array} include paths */ - _getIncludes: function(dir, prefix) { + _getIncludesForDir: function(dir, prefix) { let dirname = path.relative(this.project.root, dir); let globs = this.registry.extensionsForType('js').map((extension) => `**/*.${extension}`); @@ -236,5 +239,22 @@ module.exports = { } return this._coveredAddon; + }, + + /** + * Find the app's in-repo addons (if any). + * @returns {Array} the in-repo addons + */ + _findInRepoAddons: function() { + if (!this._inRepoAddons) { + const pkg = this.project.pkg; + const inRepoAddonPaths = pkg['ember-addon'] && pkg['ember-addon'].paths; + this._inRepoAddons = (inRepoAddonPaths || []).map((addonPath) => { + let addonName = path.basename(addonPath); + return this.project.findAddonByName(addonName); + }); + } + + return this._inRepoAddons; } }; diff --git a/package.json b/package.json index f613d2bf..f1521a8d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "extend": "^3.0.0", "fs-extra": "^5.0.0", "istanbul-api": "^1.1.14", + "lodash.concat": "^4.5.0", "node-dir": "^0.1.16", "rsvp": "^4.8.1", "walk-sync": "^0.3.2" diff --git a/test/integration/app-coverage-test.js b/test/integration/app-coverage-test.js index 87a29f9a..31fc20cd 100644 --- a/test/integration/app-coverage-test.js +++ b/test/integration/app-coverage-test.js @@ -18,7 +18,7 @@ let app; describe('app coverage generation', function() { this.timeout(10000000); beforeEach(function() { - app = new AddonTestApp(); + app = new AddonTestApp({ skipNpm: true }); return app.create('my-app', { emberVersion: '2.16.0' }).then(() => { diff --git a/test/integration/in-repo-addon-coverage-test.js b/test/integration/in-repo-addon-coverage-test.js index f238ef43..65724230 100644 --- a/test/integration/in-repo-addon-coverage-test.js +++ b/test/integration/in-repo-addon-coverage-test.js @@ -20,7 +20,7 @@ let app; describe('in-repo addon coverage generation', function() { this.timeout(10000000); beforeEach(function() { - app = new AddonTestApp(); + app = new AddonTestApp({ skipNpm: true }); return app.create('my-app-with-in-repo-addon', { emberVersion: '2.16.0' }).then(() => { @@ -50,7 +50,9 @@ describe('in-repo addon coverage generation', function() { expect(file(`${app.path}/coverage/lcov-report/index.html`)).to.not.be.empty; expect(file(`${app.path}/coverage/index.html`)).to.not.be.empty; var summary = fs.readJSONSync(`${app.path}/coverage/coverage-summary.json`); - expect(summary.total.lines.pct).to.equal(83.33); + expect(summary.total.lines.pct).to.equal(75); + expect(summary['app/utils/my-covered-util-app.js'].lines.total).to.equal(1); + expect(summary['lib/my-in-repo-addon/addon/utils/my-covered-util.js'].lines.total).to.equal(1); }); })); }); diff --git a/test/unit/index-test.js b/test/unit/index-test.js index d4f24c22..e6a3ca43 100644 --- a/test/unit/index-test.js +++ b/test/unit/index-test.js @@ -11,16 +11,17 @@ describe('index.js', function() { beforeEach(function() { sandbox = sinon.sandbox.create(); - Index.registry = { + Index.parent = Index.project = Index.app = Index.IstanbulPlugin = Index.registry = null; + sandbox.stub(Index, 'fileLookup').value({}); + sandbox.stub(Index, 'registry').value({ extensionsForType: function() { return ['js']; } - }; - Index.parent = Index.project = Index.app = Index.IstanbulPlugin = null; - sandbox.stub(Index, 'fileLookup').value({}); + }); }); afterEach(function() { + Index._coveredAddon = Index._inRepoAddons = null; sandbox.restore(); }); @@ -38,15 +39,15 @@ describe('index.js', function() { describe('with coverage enabled', function() { beforeEach(function() { sandbox.stub(Index, '_isCoverageEnabled').returns(true); - Index.fileLookup = { + sandbox.stub(Index, 'fileLookup').value({ 'some/module.js': 'some/file.js', 'some/other/module.js': 'some/other/file.js' - }; - Index.parent = { + }); + sandbox.stub(Index, 'parent').value({ isEmberCLIAddon: function() { return false; } - }; + }); }); it('does nothing if type is not test-body-footer', function() { @@ -84,10 +85,10 @@ describe('index.js', function() { post: sinon.spy() }; - Index.project = { + sandbox.stub(Index, 'project').value({ root: '/path/to/foo-bar', configPath: sinon.stub().returns('tests/dummy/config/environment.js') - }; + }); }); describe('when coverage is enabled', function() { @@ -113,13 +114,13 @@ describe('index.js', function() { }); }); - describe('_getIncludes', function() { + describe('_getIncludesForDir', function() { beforeEach(function() { sandbox.stub(Index, 'project').value({ root: 'test/fixtures/my-addon/' }); }); it('gets files to include from the app directory', function() { - Index._getIncludes('test/fixtures/my-addon/app', 'my-app'); + Index._getIncludesForDir('test/fixtures/my-addon/app', 'my-app'); expect(Index.fileLookup).to.deep.equal({ 'my-app/utils/my-covered-util.js': 'app/utils/my-covered-util.js', 'my-app/utils/my-uncovered-util.js': 'app/utils/my-uncovered-util.js' @@ -127,7 +128,7 @@ describe('index.js', function() { }); it('gets files to include from the addon directory', function() { - Index._getIncludes('test/fixtures/my-addon/addon', 'my-addon'); + Index._getIncludesForDir('test/fixtures/my-addon/addon', 'my-addon'); expect(Index.fileLookup).to.deep.equal({ 'my-addon/utils/my-covered-util.js': 'addon/utils/my-covered-util.js', 'my-addon/utils/my-uncovered-util.js': 'addon/utils/my-uncovered-util.js' @@ -137,14 +138,14 @@ describe('index.js', function() { describe('_getExcludes', function() { beforeEach(function() { - Index.parent = { + sandbox.stub(Index, 'parent').value({ isEmberCLIAddon: function() { return false; }, name: function() { return 'test'; } - }; + }); }); describe('when excludes not defined in config', function() { @@ -275,14 +276,14 @@ describe('index.js', function() { var isAddon; beforeEach(function() { - Index.parent = { + sandbox.stub(Index, 'parent').value({ name: function() { return 'parent-app'; }, isEmberCLIAddon: function() { return isAddon; } - }; + }); }); describe('when parent is an app', function() { @@ -311,13 +312,12 @@ describe('index.js', function() { var result; beforeEach(function() { - Index.project = { + sandbox.stub(Index, 'project').value({ findAddonByName: sinon.stub().returns({ name: 'my-addon' }), pkg: { name: '@scope/ember-cli-my-addon' } - }; - + }); result = Index._findCoveredAddon(); }); @@ -330,7 +330,7 @@ describe('index.js', function() { }); }); - describe('_instrumentDirectory', function() { + describe('_getIncludes', function() { beforeEach(function() { sandbox.stub(Index, 'IstanbulPlugin').value('istanbul'); sandbox.stub(Index, '_getExcludes').returns([]); @@ -341,7 +341,7 @@ describe('index.js', function() { sandbox.stub(Index, 'app').value({}); }); - describe('_instrumentAppDirectory', function() { + describe('_getIncludesForAppDirectory', function() { describe('for an app', function() { beforeEach(function() { @@ -351,30 +351,16 @@ describe('index.js', function() { }); }); - it('instruments the app directory', function() { - Index._instrumentAppDirectory(); + it('gets includes for the app directory', function() { + const includes = Index._getIncludesForAppDirectory(); + expect(includes).to.deep.equal([ + 'my-app/utils/my-covered-util.js', + 'my-app/utils/my-uncovered-util.js' + ]); expect(Index.fileLookup).to.deep.equal({ 'my-app/utils/my-covered-util.js': 'app/utils/my-covered-util.js', 'my-app/utils/my-uncovered-util.js': 'app/utils/my-uncovered-util.js' }); - expect(Index.app).to.deep.equal({ - options: { - babel: { - plugins: [ - [ - 'istanbul', - { - exclude: [], - include: [ - 'my-app/utils/my-covered-util.js', - 'my-app/utils/my-uncovered-util.js' - ] - } - ] - ] - } - } - }); }); }); @@ -386,46 +372,33 @@ describe('index.js', function() { }); }); - it('instruments the app directory', function() { - Index._instrumentAppDirectory(); + it('gets includes for the app directory', function() { + const includes = Index._getIncludesForAppDirectory(); + expect(includes).to.deep.equal([ + 'dummy/utils/my-covered-util.js', + 'dummy/utils/my-uncovered-util.js' + ]); expect(Index.fileLookup).to.deep.equal({ 'dummy/utils/my-covered-util.js': 'app/utils/my-covered-util.js', 'dummy/utils/my-uncovered-util.js': 'app/utils/my-uncovered-util.js' }); - expect(Index.app).to.deep.equal({ - options: { - babel: { - plugins: [ - [ - 'istanbul', - { - exclude: [], - include: [ - 'dummy/utils/my-covered-util.js', - 'dummy/utils/my-uncovered-util.js' - ] - } - ] - ] - } - } - }); }); }); }); - describe('_instrumentAddonDirectory', function() { + describe('_getIncludesForAddonDirectory', function() { describe('for an app', function() { beforeEach(function() { sandbox.stub(Index, '_findCoveredAddon').returns(null); - sandbox.spy(Index, '_instrumentDirectory'); + sandbox.spy(Index, '_getIncludesForDir'); }); - it('does not instrument the addon directory', function() { - Index._instrumentAddonDirectory(); - sinon.assert.notCalled(Index._instrumentDirectory); + it('does not get includes for the addon directory', function() { + const includes = Index._getIncludesForAddonDirectory(); + expect(includes).to.be.undefined; + sinon.assert.notCalled(Index._getIncludesForDir); }); }); @@ -442,55 +415,41 @@ describe('index.js', function() { addon = null; }); - it('instruments the addon directory', function() { - Index._instrumentAddonDirectory(); + it('gets includes for the addon directory', function() { + const includes = Index._getIncludesForAddonDirectory(); + expect(includes).to.deep.equal([ + 'my-addon/utils/my-covered-util.js', + 'my-addon/utils/my-uncovered-util.js' + ]); expect(Index.fileLookup).to.deep.equal({ 'my-addon/utils/my-covered-util.js': 'addon/utils/my-covered-util.js', 'my-addon/utils/my-uncovered-util.js': 'addon/utils/my-uncovered-util.js' }); - expect(addon).to.deep.equal({ - name: 'my-addon', - options: { - babel: { - plugins: [ - [ - 'istanbul', - { - exclude: [], - include: [ - 'my-addon/utils/my-covered-util.js', - 'my-addon/utils/my-uncovered-util.js' - ] - } - ] - ] - } - } - }); }); }); }); - describe('_instrumentInRepoAddonDirectories', function() { + describe('_getIncludesForInRepoAddonDirectories', function() { describe('for an app with no inrepo addons', function() { beforeEach(function() { sandbox.stub(Index, 'project').value({ pkg: { } }); - sandbox.spy(Index, '_instrumentDirectory'); + sandbox.spy(Index, '_getIncludesForDir'); }); it('does not instrument any inrepo addon directories', function() { - Index._instrumentInRepoAddonDirectories(); - sinon.assert.notCalled(Index._instrumentDirectory); + const includes = Index._getIncludesForInRepoAddonDirectories(); + expect(includes).to.deep.equal([]); + sinon.assert.notCalled(Index._getIncludesForDir); }); }); describe('for an app with an inrepo addon', function() { - let addon = {}; + let addon = { name: 'my-in-repo-addon' }; beforeEach(function() { - sandbox.stub(path, 'basename').returns('my-inrepo-addon'); + sandbox.stub(path, 'basename').returns('my-in-repo-addon'); sandbox.stub(Index, 'project').value({ pkg: { 'ember-addon': { @@ -499,7 +458,7 @@ describe('index.js', function() { ] } }, - root: 'test/fixtures/my-addon/', + root: 'test/fixtures/my-app-with-in-repo-addon/', findAddonByName() { return addon; } }); }); @@ -509,30 +468,18 @@ describe('index.js', function() { }); it('instruments the inrepo addon', function() { - Index._instrumentInRepoAddonDirectories(); + const includes = Index._getIncludesForInRepoAddonDirectories(); + expect(includes).to.deep.equal([ + 'my-app/utils/my-covered-util.js', + 'my-app/utils/my-uncovered-util.js', + 'my-in-repo-addon/utils/my-covered-util.js', + 'my-in-repo-addon/utils/my-uncovered-util.js' + ]); expect(Index.fileLookup).to.deep.equal({ - 'my-app/utils/my-covered-util.js': 'app/utils/my-covered-util.js', - 'my-app/utils/my-uncovered-util.js': 'app/utils/my-uncovered-util.js', - 'my-inrepo-addon/utils/my-covered-util.js': 'addon/utils/my-covered-util.js', - 'my-inrepo-addon/utils/my-uncovered-util.js': 'addon/utils/my-uncovered-util.js', - }); - expect(addon).to.deep.equal({ - options: { - babel: { - plugins: [ - [ - 'istanbul', - { - exclude: [], - include: [ - 'my-inrepo-addon/utils/my-covered-util.js', - 'my-inrepo-addon/utils/my-uncovered-util.js' - ] - } - ] - ] - } - } + 'my-app/utils/my-covered-util.js': 'lib/my-in-repo-addon/app/utils/my-covered-util.js', + 'my-app/utils/my-uncovered-util.js': 'lib/my-in-repo-addon/app/utils/my-uncovered-util.js', + 'my-in-repo-addon/utils/my-covered-util.js': 'lib/my-in-repo-addon/addon/utils/my-covered-util.js', + 'my-in-repo-addon/utils/my-uncovered-util.js': 'lib/my-in-repo-addon/addon/utils/my-uncovered-util.js', }); }); }); diff --git a/yarn.lock b/yarn.lock index 2de85208..a9d49b8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4450,6 +4450,10 @@ lodash.clonedeep@^4.4.1: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" +lodash.concat@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.concat/-/lodash.concat-4.5.0.tgz#b053ae02e4a8008582e7256b9d02bda6d0380395" + lodash.debounce@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-3.1.1.tgz#812211c378a94cc29d5aa4e3346cf0bfce3a7df5"