From 4095a97244f0c7529ec8678a2cd3dc932aab4fa1 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Sun, 19 Nov 2017 19:24:24 +0000 Subject: [PATCH 01/10] Fix paths for istanbul report and remove parallel logic for impicit parallel support --- addon/components/my-covered-component.js | 9 + addon/components/my-uncovered-component.js | 9 + .../components/my-covered-component.hbs | 1 + .../components/my-uncovered-component.hbs | 1 + addon/utils/my-covered-util.js | 3 + addon/utils/my-uncovered-util.js | 3 + app/components/my-covered-component.js | 1 + app/components/my-uncovered-component.js | 1 + app/utils/my-covered-util.js | 1 + app/utils/my-uncovered-util.js | 2 + index.js | 93 +++++---- lib/attach-middleware.js | 25 +-- lib/config.js | 2 - lib/templates/test-body-footer.html | 18 +- package.json | 12 +- test/integration/coverage-test.js | 70 +------ test/unit/index-test.js | 189 +----------------- tests/dummy/app/app.js | 2 - .../components/my-covered-component-test.js | 12 ++ tests/unit/utils/my-covered-util-test.js | 10 + yarn.lock | 39 +--- 21 files changed, 147 insertions(+), 356 deletions(-) create mode 100644 addon/components/my-covered-component.js create mode 100644 addon/components/my-uncovered-component.js create mode 100644 addon/templates/components/my-covered-component.hbs create mode 100644 addon/templates/components/my-uncovered-component.hbs create mode 100644 addon/utils/my-covered-util.js create mode 100644 addon/utils/my-uncovered-util.js create mode 100644 app/components/my-covered-component.js create mode 100644 app/components/my-uncovered-component.js create mode 100644 app/utils/my-covered-util.js create mode 100644 app/utils/my-uncovered-util.js create mode 100644 tests/integration/components/my-covered-component-test.js create mode 100644 tests/unit/utils/my-covered-util-test.js diff --git a/addon/components/my-covered-component.js b/addon/components/my-covered-component.js new file mode 100644 index 00000000..4f41cb4e --- /dev/null +++ b/addon/components/my-covered-component.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; +import layout from '../templates/components/my-covered-component'; + +export default Ember.Component.extend({ + layout, + foo: Ember.computed(function() { + return 'foo'; + }) +}); diff --git a/addon/components/my-uncovered-component.js b/addon/components/my-uncovered-component.js new file mode 100644 index 00000000..cc2fb333 --- /dev/null +++ b/addon/components/my-uncovered-component.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; +import layout from '../templates/components/my-uncovered-component'; + +export default Ember.Component.extend({ + layout, + foo: Ember.computed(function() { + return 'foo'; + }) +}); diff --git a/addon/templates/components/my-covered-component.hbs b/addon/templates/components/my-covered-component.hbs new file mode 100644 index 00000000..24369f73 --- /dev/null +++ b/addon/templates/components/my-covered-component.hbs @@ -0,0 +1 @@ +{{foo}} \ No newline at end of file diff --git a/addon/templates/components/my-uncovered-component.hbs b/addon/templates/components/my-uncovered-component.hbs new file mode 100644 index 00000000..054e96cb --- /dev/null +++ b/addon/templates/components/my-uncovered-component.hbs @@ -0,0 +1 @@ +{{foo}} diff --git a/addon/utils/my-covered-util.js b/addon/utils/my-covered-util.js new file mode 100644 index 00000000..50867b0d --- /dev/null +++ b/addon/utils/my-covered-util.js @@ -0,0 +1,3 @@ +export default function myCoveredUtil() { + return true; +} diff --git a/addon/utils/my-uncovered-util.js b/addon/utils/my-uncovered-util.js new file mode 100644 index 00000000..8f531cba --- /dev/null +++ b/addon/utils/my-uncovered-util.js @@ -0,0 +1,3 @@ +export default function myUncoveredUtil() { + return true; +} diff --git a/app/components/my-covered-component.js b/app/components/my-covered-component.js new file mode 100644 index 00000000..c77ce448 --- /dev/null +++ b/app/components/my-covered-component.js @@ -0,0 +1 @@ +export { default } from 'ember-cli-code-coverage/components/my-covered-component'; \ No newline at end of file diff --git a/app/components/my-uncovered-component.js b/app/components/my-uncovered-component.js new file mode 100644 index 00000000..40263cf4 --- /dev/null +++ b/app/components/my-uncovered-component.js @@ -0,0 +1 @@ +export { default } from 'ember-cli-code-coverage/components/my-uncovered-component'; \ No newline at end of file diff --git a/app/utils/my-covered-util.js b/app/utils/my-covered-util.js new file mode 100644 index 00000000..4c33e85b --- /dev/null +++ b/app/utils/my-covered-util.js @@ -0,0 +1 @@ +export { default } from 'ember-cli-code-coverage/utils/my-covered-util'; diff --git a/app/utils/my-uncovered-util.js b/app/utils/my-uncovered-util.js new file mode 100644 index 00000000..5ff69ce4 --- /dev/null +++ b/app/utils/my-uncovered-util.js @@ -0,0 +1,2 @@ +export { default } from 'ember-cli-code-coverage/utils/my-uncovered-util'; + diff --git a/index.js b/index.js index 5bfe4faf..8a5d2dc9 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var existsSync = require('exists-sync'); var fs = require('fs-extra'); var attachMiddleware = require('./lib/attach-middleware'); var config = require('./lib/config'); +var walkSync = require('walk-sync'); const VersionChecker = require('ember-cli-version-checker'); function requireBabelPlugin(pluginName) { @@ -21,32 +22,29 @@ function requireBabelPlugin(pluginName) { return plugin; } -module.exports = { - name: 'ember-cli-code-coverage', +function getPlugins(options) { + options.babel = options.babel || {}; + return options.babel.plugins = options.babel.plugins || []; +} - // Ember Methods +const EXT_RE = /\.[^\.]+$/; - _getParentOptions: function() { - let options; +module.exports = { + name: 'ember-cli-code-coverage', - // The parent can either be an Addon or a Project. If it's the project, - // we want to use the app instead. - if (this.parent !== this.project) { - // the parent is an addon, so use its options directly - options = this.parent.options = this.parent.options || {}; - } else { - // the parent is the project, and therefore we need to use - // the app.options instead - options = this.app.options = this.app.options || {}; - } + /** + * Look up the file path from an ember module path. + * @type {Object} + */ + fileLookup: null, - return options; - }, + // Ember Methods included: function() { this._super.included.apply(this, arguments); - let parentOptions = this._getParentOptions(); + let options; + this.fileLookup = {}; if (!this._registeredWithBabel && this._isCoverageEnabled()) { let checker = new VersionChecker(this.parent).for('ember-cli-babel', 'npm'); @@ -54,13 +52,28 @@ module.exports = { if (checker.satisfies('>= 6.0.0')) { const IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul'); - // Create babel options if they do not exist - parentOptions.babel = parentOptions.babel || {}; + const appDir = path.join(this.project.root, 'app'); + if (fs.existsSync(appDir)) { + // Instrument the app directory. + let prefix = this.parent.isEmberCLIAddon() ? 'dummy' : this.parent.name(); + options = this.app.options = this.app.options || {}; + getPlugins(options).push([IstanbulPlugin, { + exclude: this._getExcludes(), + include: this._getIncludes(appDir, 'app', prefix) + }]); + } + + const addonDir = path.join(this.project.root, 'addon'); + if (fs.existsSync(addonDir)) { + // Instrument the addon directory. + let addon = this._findCoveredAddon(); + options = addon.options = addon.options || {}; + getPlugins(options).push([IstanbulPlugin, { + exclude: this._getExcludes(), + include: this._getIncludes(addonDir, 'addon', addon.name) + }]); + } - // Create and pull off babel plugins - let plugins = parentOptions.babel.plugins = parentOptions.babel.plugins || []; - - plugins.push([IstanbulPlugin, { exclude: this._getExcludes() }]); } else { this.project.ui.writeWarnLine( 'ember-cli-code-coverage: You are using an unsupported ember-cli-babel version,' + @@ -75,18 +88,12 @@ module.exports = { contentFor: function(type) { if (type === 'test-body-footer' && this._isCoverageEnabled()) { var template = fs.readFileSync(path.join(__dirname, 'lib', 'templates', 'test-body-footer.html')).toString(); - return template.replace('{%PROJECT_NAME%}', this._parentName()); + return template.replace('{%ENTRIES%}', JSON.stringify(Object.keys(this.fileLookup).map(file => file.replace(EXT_RE, '')))); } return undefined; }, - includedCommands: function () { - return { - 'coverage-merge': require('./lib/coverage-merge') - }; - }, - /** * If coverage is enabled attach coverage middleware to the express server run by ember-cli * @param {Object} startOptions - Express server start options @@ -97,7 +104,11 @@ module.exports = { testemMiddleware: function(app) { if (!this._isCoverageEnabled()) { return; } - attachMiddleware(app, { configPath: this.project.configPath(), root: this.project.root }); + attachMiddleware(app, { + configPath: this.project.configPath(), + root: this.project.root, + fileLookup: this.fileLookup + }); }, // Custom Methods @@ -119,14 +130,26 @@ module.exports = { return config(this.project.configPath()); }, + /** + * Get paths to include for coverage + * @returns {Array} include paths + */ + _getIncludes: function(dir, folder, prefix) { + let globs = this.registry.extensionsForType('js').map((extension) => `**/*.${extension}`); + + return walkSync(dir, { directories: false, globs }).map(file => { + let module = prefix + '/' + file.replace(EXT_RE, '.js'); + this.fileLookup[module] = path.join(folder, file); + return module; + }); + }, + /** * Get paths to exclude from coverage * @returns {Array} exclude paths */ _getExcludes: function() { - var excludes = this._getConfig().excludes || []; - - return excludes; + return this._getConfig().excludes || []; }, /** diff --git a/lib/attach-middleware.js b/lib/attach-middleware.js index 7159b331..ce030649 100644 --- a/lib/attach-middleware.js +++ b/lib/attach-middleware.js @@ -3,44 +3,37 @@ var bodyParser = require('body-parser'); var istanbul = require('istanbul-api'); var getConfig = require('./config'); -var crypto = require('crypto'); function logError(err, req, res, next) { console.error(err.stack); next(err); } -function fixFilePaths(coverageData) { - // TODO: munge `coverageData.path` to the "real" on disk paths - // so that tools like code-climate (and other coverage related things) - // work properly +function fixFilePaths(coverageData, fileLookup) { + coverageData.path = fileLookup[coverageData.path]; return coverageData; } module.exports = function(app, options) { + + let map = istanbul.libCoverage.createCoverageMap(); + app.post('/write-coverage', bodyParser.json({ limit: '50mb' }), function(req, res) { var config = getConfig(options.configPath); - if (config.parallel) { - config.coverageFolder = config.coverageFolder + '_' + crypto.randomBytes(4).toString('hex'); - if (config.reporters.indexOf('json') === -1) { - config.reporters.push('json'); - } - } - if (config.reporters.indexOf('json-summary') === -1) { config.reporters.push('json-summary'); } let reporter = istanbul.createReporter(); - let map = istanbul.libCoverage.createCoverageMap(); let coverage = req.body; - Object.keys(coverage).forEach(filename => - map.addFileCoverage(fixFilePaths(coverage[filename])) - ); + Object.keys(options.fileLookup).forEach(filename => { + let fileCoverage = coverage[filename] || istanbul.libCoverage.createFileCoverage(filename).data; + map.addFileCoverage(fixFilePaths(fileCoverage, options.fileLookup)); + }); reporter.addAll(config.reporters); reporter.write(map); diff --git a/lib/config.js b/lib/config.js index 8704bb1c..ab2c6064 100644 --- a/lib/config.js +++ b/lib/config.js @@ -38,11 +38,9 @@ function config(configPath) { function getDefaultConfig() { return { coverageEnvVar: 'COVERAGE', - coverageFolder: 'coverage', excludes: [ '*/mirage/**/*' ], - useBabelInstrumenter: false, reporters: [ 'html', 'lcov' diff --git a/lib/templates/test-body-footer.html b/lib/templates/test-body-footer.html index fb8badc7..a5ab37b6 100644 --- a/lib/templates/test-body-footer.html +++ b/lib/templates/test-body-footer.html @@ -3,17 +3,7 @@ var REQUEST_ASYNC = !/PhantomJS/.test(window.navigator.userAgent); function sendCoverage(callback) { - Object.keys(require.entries).forEach(function(file) { - // Only load non-test files from the project - var parts = file.split('/'); - if (parts[0] === '{%PROJECT_NAME%}' && parts[1] !== 'tests') { - try { - require(file); - } catch(error) { - console.warn('Error occurred while evaluating `' + file + '`: ' + error.message + '\n' + error.stack); - } - } - }); + {%ENTRIES%}.forEach(require); var coverageData = window.__coverage__; var data = JSON.stringify(coverageData || {}); @@ -62,16 +52,16 @@ data = JSON.parse(data); } - if (!data || !data.total) { + if (!data) { return; } var results = ['Lines', 'Branches', 'Functions', 'Statements'] .filter(function (name) { - return data.total[name.toLowerCase()] + return name.toLowerCase(); }) .map(function (name) { - return name + ' ' + data.total[name.toLowerCase()].pct + '%' + return name + ' ' + data[name.toLowerCase()].pct + '%' }); var resultsText = document.createTextNode(results.join(' | ')); diff --git a/package.json b/package.json index 5910df4a..f9711c99 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "ember-cli-app-version": "^1.0.0", "ember-cli-dependency-checker": "^1.3.0", "ember-cli-eslint": "^3.0.0", - "ember-cli-htmlbars": "^1.1.1", "ember-cli-htmlbars-inline-precompile": "^0.4.0", "ember-cli-inject-live-reload": "^1.4.1", "ember-cli-qunit": "^4.0.0", @@ -59,21 +58,14 @@ "babel-plugin-istanbul": "^4.1.5", "babel-plugin-transform-async-to-generator": "^6.24.1", "body-parser": "^1.15.0", - "broccoli-filter": "^1.2.3", - "broccoli-funnel": "^1.0.1", - "broccoli-merge-trees": "^1.1.1", "ember-cli-babel": "^6.8.2", + "ember-cli-htmlbars": "^1.1.1", "ember-cli-version-checker": "^2.0.0", - "escodegen": "^1.8.0", - "esprima": "^3.1.3", "exists-sync": "0.0.3", "extend": "^3.0.0", "fs-extra": "^0.26.7", "istanbul-api": "^1.1.14", - "node-dir": "^0.1.16", - "rsvp": "^3.2.1", - "source-map": "0.5.6", - "string.prototype.startswith": "^0.2.0" + "rsvp": "^3.2.1" }, "ember-addon": { "configPath": "tests/dummy/config", diff --git a/test/integration/coverage-test.js b/test/integration/coverage-test.js index 72cb7a4f..baa7ced5 100644 --- a/test/integration/coverage-test.js +++ b/test/integration/coverage-test.js @@ -31,7 +31,7 @@ describe('`ember test`', function() { expect(file('coverage/lcov-report/index.html')).to.not.be.empty; expect(file('coverage/index.html')).to.not.be.empty; var summary = fs.readJSONSync('coverage/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(100); + expect(summary.total.lines.pct).to.equal(50); }); }); @@ -43,74 +43,18 @@ describe('`ember test`', function() { }); }); - it('uses the babel instrumenter when the configuration is set', function() { - this.timeout(100000); - fs.copySync('tests/dummy/config/coverage-babel.js', 'tests/dummy/config/coverage.js'); - return runCommand('ember', ['test'], {env: {COVERAGE: true}}).then(function() { - expect(file('coverage/lcov-report/index.html')).to.not.be.empty; - expect(file('coverage/index.html')).to.not.be.empty; - var summary = fs.readJSONSync('coverage/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(100); - }); - }); - - it('works with the babel instrumenter and ES2017 async functions', function() { - this.timeout(100000); - fs.copySync('tests/dummy/config/coverage-babel.js', 'tests/dummy/config/coverage.js'); - // We want something that will always be evaluated whenever the app starts. - fs.writeFileSync('tests/dummy/app/routes/index.js', ` -import Ember from 'ember'; -export default Ember.Route.extend({ - async model() { - return {}; - } -}); -`); - return runCommand('ember', ['test'], {env: {COVERAGE: true}}).then(function() { - expect(file('coverage/lcov-report/index.html')).to.not.be.empty; - expect(file('coverage/index.html')).to.not.be.empty; - var summary = fs.readJSONSync('coverage/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(100); - }); - }); - - it('uses parallel configuration and merges coverage when merge-coverage command is issued', function() { + it('merges coverage when tests are run in parallel', function() { this.timeout(100000); expect(dir('coverage')).to.not.exist; - fs.copySync('tests/dummy/config/coverage-parallel.js', 'tests/dummy/config/coverage.js'); return runCommand('ember', ['exam', '--split=2', '--parallel=true'], {env: {COVERAGE: true}}).then(function() { - expect(dir('coverage')).to.not.exist; - return runCommand('ember', ['coverage-merge']); - }).then(function() { expect(file('coverage/lcov-report/index.html')).to.not.be.empty; expect(file('coverage/index.html')).to.not.be.empty; var summary = fs.readJSONSync('coverage/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(100); - expect(summary['tests/dummy/app/resolver.js'].lines.pct).to.equal(100); - expect(summary['tests/dummy/app/app.js'].lines.pct).to.equal(100); - expect(summary['tests/dummy/app/router.js'].lines.pct).to.equal(100); - expect(summary['tests/dummy/app/templates/application.hbs'].lines.pct).to.equal(100); - }); - }); - - it('uses nested coverageFolder and parallel configuration and run merge-coverage', function() { - this.timeout(100000); - var coverageFolder = 'coverage/abc/easy-as/123'; - - expect(dir(coverageFolder)).to.not.exist; - fs.copySync('tests/dummy/config/coverage-nested-folder.js', 'tests/dummy/config/coverage.js'); - return runCommand('ember', ['exam', '--split=2', '--parallel=true'], {env: {COVERAGE: true}}).then(function() { - expect(dir(coverageFolder)).to.not.exist; - return runCommand('ember', ['coverage-merge']); - }).then(function() { - expect(file(coverageFolder + '/lcov-report/index.html')).to.not.be.empty; - expect(file(coverageFolder + '/index.html')).to.not.be.empty; - var summary = fs.readJSONSync(coverageFolder + '/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(100); - expect(summary['tests/dummy/app/resolver.js'].lines.pct).to.equal(100); - expect(summary['tests/dummy/app/app.js'].lines.pct).to.equal(100); - expect(summary['tests/dummy/app/router.js'].lines.pct).to.equal(100); - expect(summary['tests/dummy/app/templates/application.hbs'].lines.pct).to.equal(100); + expect(summary.total.lines.pct).to.equal(50); + expect(summary['addon/components/my-covered-component.js'].lines.pct).to.equal(100); + expect(summary['addon/components/my-uncovered-component.js'].lines.pct).to.equal(0); + expect(summary['addon/utils/my-covered-util.js'].lines.pct).to.equal(100); + expect(summary['addon/utils/my-uncovered-util.js'].lines.pct).to.equal(0); }); }); }); diff --git a/test/unit/index-test.js b/test/unit/index-test.js index 4a5cbb20..850903f6 100644 --- a/test/unit/index-test.js +++ b/test/unit/index-test.js @@ -34,12 +34,13 @@ describe('index.js', function() { describe('with coverage enabled', function() { beforeEach(function() { sandbox.stub(Index, '_isCoverageEnabled').returns(true); + Index.fileLookup = { + 'some/module.js': 'some/file.js', + 'some/other/module.js': 'some/other/file.js' + }; Index.parent = { isEmberCLIAddon: function() { return false; - }, - name: function() { - return 'fake-app'; } }; }); @@ -53,7 +54,7 @@ describe('index.js', function() { }); it('includes the project name in the template for test-body-footer', function() { - expect(Index.contentFor('test-body-footer')).to.match(/fake-app/); + expect(Index.contentFor('test-body-footer')).to.match(/`["some/module", "some/other/module"`]/); }); }); }); @@ -108,164 +109,8 @@ describe('index.js', function() { }); }); - describe('_doesFileExistInCurrentProjectApp', function() { - describe('when file exists', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(true); - result = Index._doesFileExistInCurrentProjectApp('adapters/application.js'); - }); - - it('uses path to file in app', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/adapters/application.js']); - }); - - it('returns true', function() { - expect(result).to.be.true; - }); - }); - - describe('when file does not exist', function() { - beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(false); - }); - - describe('when template file exists', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_doesTemplateFileExist').returns(true); - result = Index._doesFileExistInCurrentProjectApp('templates/application.js'); - }); - - it('uses path to file in app', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/templates/application.js']); - }); - - it('returns true', function() { - expect(result).to.be.true; - }); - }); - - describe('when template file does not exist', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_doesTemplateFileExist').returns(false); - result = Index._doesFileExistInCurrentProjectApp('templates/application.js'); - }); - - it('uses path to file in app', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/templates/application.js']); - }); - - it('returns false', function() { - expect(result).to.be.false; - }); - }); - }); - }); - - describe('_doesFileExistInDummyApp', function() { - describe('when file exists', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(true); - result = Index._doesFileExistInDummyApp('adapters/application.js'); - }); - - it('uses path to file in dummy app', function() { - expect(Index._existsSync.lastCall.args).to.eql(['tests/dummy/app/adapters/application.js']); - }); - - it('returns true', function() { - expect(result).to.be.true; - }); - }); - - describe('when file does not exist', function() { - beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(false); - }); - - describe('when template file exists', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_doesTemplateFileExist').returns(true); - result = Index._doesFileExistInDummyApp('templates/application.js'); - }); - - it('uses path to file in dummy app', function() { - expect(Index._existsSync.lastCall.args).to.eql(['tests/dummy/app/templates/application.js']); - }); - - it('returns true', function() { - expect(result).to.be.true; - }); - }); - - describe('when template file does not exist', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_doesTemplateFileExist').returns(false); - result = Index._doesFileExistInDummyApp('templates/application.js'); - }); - - it('uses path to file in dummy app', function() { - expect(Index._existsSync.lastCall.args).to.eql(['tests/dummy/app/templates/application.js']); - }); - - it('returns false', function() { - expect(result).to.be.false; - }); - }); - }); - }); - - describe('_doesTemplateFileExist', function() { - describe('when file exists', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(true); - result = Index._doesTemplateFileExist('app/templates/application.js'); - }); - - it('uses path to hbs file', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/templates/application.hbs']); - }); - - it('returns true', function() { - expect(result).to.be.true; - }); - }); - - describe('when file does not exist', function() { - var result; - - beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(false); - result = Index._doesTemplateFileExist('app/templates/application.js'); - }); - - it('uses path to hbs file', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/templates/application.hbs']); - }); - - it('returns false', function() { - expect(result).to.be.false; - }); - }); - }); - describe('_getExcludes', function() { beforeEach(function() { - sandbox.stub(Index, '_filterOutAddonFiles').returns('test'); - Index.parent = { isEmberCLIAddon: function() { return false; @@ -284,17 +129,10 @@ describe('index.js', function() { results = Index._getExcludes(); }); - it('returns one exclude', function() { - expect(results.length).to.equal(1); - }); - - it('exclude is a function', function() { - expect(typeof results[0]).to.equal('function'); + it('returns no excludes', function() { + expect(results.length).to.equal(0); }); - it('exclude is expected function', function() { - expect(results[0]()).to.equal('test'); - }); }); describe('when excludes defined in config', function() { @@ -308,21 +146,14 @@ describe('index.js', function() { results = Index._getExcludes(); }); - it('returns two excludes', function() { - expect(results.length).to.equal(2); + it('returns one exclude', function() { + expect(results.length).to.equal(1); }); - it('first exclude is from config', function() { + it('exclude is from config', function() { expect(results[0]).to.eql('*/mirage/**/*'); }); - it('second exclude is a function', function() { - expect(typeof results[1]).to.equal('function'); - }); - - it('second exclude is expected function', function() { - expect(results[1]()).to.equal('test'); - }); }); }); diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js index 831ad610..f796e79d 100644 --- a/tests/dummy/app/app.js +++ b/tests/dummy/app/app.js @@ -5,8 +5,6 @@ import config from './config/environment'; let App; -Ember.MODEL_FACTORY_INJECTIONS = true; - App = Ember.Application.extend({ modulePrefix: config.modulePrefix, podModulePrefix: config.podModulePrefix, diff --git a/tests/integration/components/my-covered-component-test.js b/tests/integration/components/my-covered-component-test.js new file mode 100644 index 00000000..9fce79b7 --- /dev/null +++ b/tests/integration/components/my-covered-component-test.js @@ -0,0 +1,12 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('my-covered-component', 'Integration | Component | my covered component', { + integration: true +}); + +test('it renders', function(assert) { + this.render(hbs`{{my-covered-component}}`); + + assert.equal(this.$().text().trim(), 'foo'); +}); diff --git a/tests/unit/utils/my-covered-util-test.js b/tests/unit/utils/my-covered-util-test.js new file mode 100644 index 00000000..ba25a3ae --- /dev/null +++ b/tests/unit/utils/my-covered-util-test.js @@ -0,0 +1,10 @@ +import myCoveredUtil from 'dummy/utils/my-covered-util'; +import { module, test } from 'qunit'; + +module('Unit | Utility | my covered util'); + +// Replace this with your real tests. +test('it works', function(assert) { + let result = myCoveredUtil(); + assert.ok(result); +}); diff --git a/yarn.lock b/yarn.lock index 36e771c4..671bb9ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1466,7 +1466,7 @@ broccoli-lint-eslint@^3.3.0: lodash.defaultsdeep "^4.6.0" md5-hex "^2.0.0" -broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.4: +broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.4: version "1.2.4" resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.4.tgz#a001519bb5067f06589d91afa2942445a2d0fdb5" dependencies: @@ -2867,17 +2867,6 @@ escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^ version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escodegen@^1.8.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - escope@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" @@ -2980,11 +2969,11 @@ esprima-fb@~15001.1001.0-dev-harmony-fb: version "15001.1001.0-dev-harmony-fb" resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" -esprima@^2.6.0, esprima@^2.7.1: +esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@^3.1.1, esprima@^3.1.3, esprima@~3.1.0: +esprima@^3.1.1, esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -3005,10 +2994,6 @@ esrecurse@^4.1.0: estraverse "~4.1.0" object-assign "^4.0.1" -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -4955,12 +4940,6 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" -node-dir@^0.1.16: - version "0.1.16" - resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.16.tgz#d2ef583aa50b90d93db8cdd26fcea58353957fe4" - dependencies: - minimatch "^3.0.2" - node-fetch@^1.3.3: version "1.6.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" @@ -6260,7 +6239,7 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@0.5.6, source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1: +source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -6268,12 +6247,6 @@ source-map@^0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - dependencies: - amdefine ">=0.0.4" - spawn-args@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/spawn-args/-/spawn-args-0.2.0.tgz#fb7d0bd1d70fd4316bd9e3dec389e65f9d6361bb" @@ -6353,10 +6326,6 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" -string.prototype.startswith@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/string.prototype.startswith/-/string.prototype.startswith-0.2.0.tgz#da68982e353a4e9ac4a43b450a2045d1c445ae7b" - string_decoder@0.10, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" From 3b3a2fc5ec76148623f07296cba4a57df214f681 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Sun, 19 Nov 2017 19:27:54 +0000 Subject: [PATCH 02/10] Delete uneeded files --- lib/coverage-merge.js | 59 -------------------- tests/dummy/config/coverage-babel.js | 5 -- tests/dummy/config/coverage-nested-folder.js | 7 --- tests/dummy/config/coverage-parallel.js | 5 -- 4 files changed, 76 deletions(-) delete mode 100644 lib/coverage-merge.js delete mode 100644 tests/dummy/config/coverage-babel.js delete mode 100644 tests/dummy/config/coverage-nested-folder.js delete mode 100644 tests/dummy/config/coverage-parallel.js diff --git a/lib/coverage-merge.js b/lib/coverage-merge.js deleted file mode 100644 index e79893e3..00000000 --- a/lib/coverage-merge.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -var path = require('path'); -var getConfig = require('./config'); -var dir = require('node-dir'); -var Promise = require('rsvp').Promise; - -/** - * Merge together coverage files created when running in multiple threads, - * for example when being used with ember exam and parallel runs. - */ -module.exports = { - name: 'coverage-merge', - description: 'Merge multiple coverage files together.', - run: function () { - var istanbul = require('istanbul-api'); - var config = this._getConfig(); - - var coverageFolderSplit = config.coverageFolder.split('/'); - var coverageFolder = coverageFolderSplit.pop(); - var coverageRoot = this.project.root + '/' + coverageFolderSplit.join('/'); - var coverageDirRegex = new RegExp(coverageFolder + '_.*'); - - let reporter = istanbul.createReporter(); - let map = istanbul.libCoverage.createCoverageMap(); - - return new Promise(function (resolve, reject) { - dir.readFiles(coverageRoot, { matchDir: coverageDirRegex, match: /coverage-final\.json/ }, - function (err, coverageSummary, next) { - if (err) { - reject(err); - } - map.merge(JSON.parse(coverageSummary)); - next(); - }, - function (err) { - if (err) { - reject(err); - } - - if (config.reporters.indexOf('json-summary') === -1) { - config.reporters.push('json-summary'); - } - - reporter.addAll(config.reporters); - reporter.write(map); - resolve(); - }); - }); - }, - - /** - * Get project configuration - * @returns {Configuration} project configuration - */ - _getConfig: function () { - return getConfig(this.project.configPath()); - } -}; diff --git a/tests/dummy/config/coverage-babel.js b/tests/dummy/config/coverage-babel.js deleted file mode 100644 index 9b29422a..00000000 --- a/tests/dummy/config/coverage-babel.js +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-env node */ - -module.exports = { - useBabelInstrumenter: true -}; diff --git a/tests/dummy/config/coverage-nested-folder.js b/tests/dummy/config/coverage-nested-folder.js deleted file mode 100644 index ae5fac98..00000000 --- a/tests/dummy/config/coverage-nested-folder.js +++ /dev/null @@ -1,7 +0,0 @@ -/*jshint node:true*/ -'use strict'; - -module.exports = { - coverageFolder: 'coverage/abc/easy-as/123', - parallel: true -}; diff --git a/tests/dummy/config/coverage-parallel.js b/tests/dummy/config/coverage-parallel.js deleted file mode 100644 index a33514b6..00000000 --- a/tests/dummy/config/coverage-parallel.js +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-env node */ - -module.exports = { - parallel: true -}; From cc12db23539b29ca032c500c62c6af2ced8d2183 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Sat, 16 Dec 2017 09:03:31 +0000 Subject: [PATCH 03/10] Remove component fixtures and add tests for 'excludes' config --- addon/components/my-covered-component.js | 9 --------- addon/components/my-uncovered-component.js | 9 --------- .../components/my-covered-component.hbs | 1 - .../components/my-uncovered-component.hbs | 1 - app/components/my-covered-component.js | 1 - app/components/my-uncovered-component.js | 1 - index.js | 18 +++++++++--------- test/integration/coverage-test.js | 15 +++++++++++++-- tests/dummy/config/coverage-excludes.js | 7 +++++++ .../components/my-covered-component-test.js | 12 ------------ 10 files changed, 29 insertions(+), 45 deletions(-) delete mode 100644 addon/components/my-covered-component.js delete mode 100644 addon/components/my-uncovered-component.js delete mode 100644 addon/templates/components/my-covered-component.hbs delete mode 100644 addon/templates/components/my-uncovered-component.hbs delete mode 100644 app/components/my-covered-component.js delete mode 100644 app/components/my-uncovered-component.js create mode 100644 tests/dummy/config/coverage-excludes.js delete mode 100644 tests/integration/components/my-covered-component-test.js diff --git a/addon/components/my-covered-component.js b/addon/components/my-covered-component.js deleted file mode 100644 index 4f41cb4e..00000000 --- a/addon/components/my-covered-component.js +++ /dev/null @@ -1,9 +0,0 @@ -import Ember from 'ember'; -import layout from '../templates/components/my-covered-component'; - -export default Ember.Component.extend({ - layout, - foo: Ember.computed(function() { - return 'foo'; - }) -}); diff --git a/addon/components/my-uncovered-component.js b/addon/components/my-uncovered-component.js deleted file mode 100644 index cc2fb333..00000000 --- a/addon/components/my-uncovered-component.js +++ /dev/null @@ -1,9 +0,0 @@ -import Ember from 'ember'; -import layout from '../templates/components/my-uncovered-component'; - -export default Ember.Component.extend({ - layout, - foo: Ember.computed(function() { - return 'foo'; - }) -}); diff --git a/addon/templates/components/my-covered-component.hbs b/addon/templates/components/my-covered-component.hbs deleted file mode 100644 index 24369f73..00000000 --- a/addon/templates/components/my-covered-component.hbs +++ /dev/null @@ -1 +0,0 @@ -{{foo}} \ No newline at end of file diff --git a/addon/templates/components/my-uncovered-component.hbs b/addon/templates/components/my-uncovered-component.hbs deleted file mode 100644 index 054e96cb..00000000 --- a/addon/templates/components/my-uncovered-component.hbs +++ /dev/null @@ -1 +0,0 @@ -{{foo}} diff --git a/app/components/my-covered-component.js b/app/components/my-covered-component.js deleted file mode 100644 index c77ce448..00000000 --- a/app/components/my-covered-component.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-cli-code-coverage/components/my-covered-component'; \ No newline at end of file diff --git a/app/components/my-uncovered-component.js b/app/components/my-uncovered-component.js deleted file mode 100644 index 40263cf4..00000000 --- a/app/components/my-uncovered-component.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-cli-code-coverage/components/my-uncovered-component'; \ No newline at end of file diff --git a/index.js b/index.js index 8a5d2dc9..ff06ce83 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ var existsSync = require('exists-sync'); var fs = require('fs-extra'); var attachMiddleware = require('./lib/attach-middleware'); var config = require('./lib/config'); -var walkSync = require('walk-sync'); +const walkSync = require('walk-sync'); const VersionChecker = require('ember-cli-version-checker'); function requireBabelPlugin(pluginName) { @@ -22,7 +22,8 @@ function requireBabelPlugin(pluginName) { return plugin; } -function getPlugins(options) { +function getPlugins(appOrAddon) { + let options = appOrAddon.options = appOrAddon.options || {}; options.babel = options.babel || {}; return options.babel.plugins = options.babel.plugins || []; } @@ -43,7 +44,6 @@ module.exports = { included: function() { this._super.included.apply(this, arguments); - let options; this.fileLookup = {}; if (!this._registeredWithBabel && this._isCoverageEnabled()) { @@ -51,14 +51,15 @@ module.exports = { if (checker.satisfies('>= 6.0.0')) { const IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul'); + const excludes = this._getExcludes(); const appDir = path.join(this.project.root, 'app'); if (fs.existsSync(appDir)) { // Instrument the app directory. let prefix = this.parent.isEmberCLIAddon() ? 'dummy' : this.parent.name(); - options = this.app.options = this.app.options || {}; - getPlugins(options).push([IstanbulPlugin, { - exclude: this._getExcludes(), + + getPlugins(this.app).push([IstanbulPlugin, { + exclude: excludes, include: this._getIncludes(appDir, 'app', prefix) }]); } @@ -67,9 +68,8 @@ module.exports = { if (fs.existsSync(addonDir)) { // Instrument the addon directory. let addon = this._findCoveredAddon(); - options = addon.options = addon.options || {}; - getPlugins(options).push([IstanbulPlugin, { - exclude: this._getExcludes(), + getPlugins(addon).push([IstanbulPlugin, { + exclude: excludes, include: this._getIncludes(addonDir, 'addon', addon.name) }]); } diff --git a/test/integration/coverage-test.js b/test/integration/coverage-test.js index baa7ced5..84c3344c 100644 --- a/test/integration/coverage-test.js +++ b/test/integration/coverage-test.js @@ -51,10 +51,21 @@ describe('`ember test`', function() { expect(file('coverage/index.html')).to.not.be.empty; var summary = fs.readJSONSync('coverage/coverage-summary.json'); expect(summary.total.lines.pct).to.equal(50); - expect(summary['addon/components/my-covered-component.js'].lines.pct).to.equal(100); - expect(summary['addon/components/my-uncovered-component.js'].lines.pct).to.equal(0); expect(summary['addon/utils/my-covered-util.js'].lines.pct).to.equal(100); expect(summary['addon/utils/my-uncovered-util.js'].lines.pct).to.equal(0); }); }); + + it('excludes files when the configuration is set', function() { + this.timeout(100000); + fs.copySync('tests/dummy/config/coverage-excludes.js', 'tests/dummy/config/coverage.js'); + return runCommand('ember', ['test'], {env: {COVERAGE: true}}).then(function() { + expect(file('coverage/lcov-report/index.html')).to.not.be.empty; + expect(file('coverage/index.html')).to.not.be.empty; + var summary = fs.readJSONSync('coverage/coverage-summary.json'); + console.log(summary); + expect(summary.total.lines.pct).to.equal(100); + }); + }); + }); diff --git a/tests/dummy/config/coverage-excludes.js b/tests/dummy/config/coverage-excludes.js new file mode 100644 index 00000000..e84a75d7 --- /dev/null +++ b/tests/dummy/config/coverage-excludes.js @@ -0,0 +1,7 @@ +/* eslint-env node */ + +module.exports = { + excludes: [ + '**/utils/my-uncovered-util.js' + ] +}; diff --git a/tests/integration/components/my-covered-component-test.js b/tests/integration/components/my-covered-component-test.js deleted file mode 100644 index 9fce79b7..00000000 --- a/tests/integration/components/my-covered-component-test.js +++ /dev/null @@ -1,12 +0,0 @@ -import { moduleForComponent, test } from 'ember-qunit'; -import hbs from 'htmlbars-inline-precompile'; - -moduleForComponent('my-covered-component', 'Integration | Component | my covered component', { - integration: true -}); - -test('it renders', function(assert) { - this.render(hbs`{{my-covered-component}}`); - - assert.equal(this.$().text().trim(), 'foo'); -}); From 2b1c9fa2528001d222b35d59e843557b400149b3 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Sat, 16 Dec 2017 12:44:46 +0000 Subject: [PATCH 04/10] Add support for in-repo-addons #120 --- index.js | 96 +++++++++++++++++++++---------- test/integration/coverage-test.js | 1 - 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/index.js b/index.js index ff06ce83..d05ea663 100644 --- a/index.js +++ b/index.js @@ -22,12 +22,6 @@ 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 || []; -} - const EXT_RE = /\.[^\.]+$/; module.exports = { @@ -50,30 +44,11 @@ module.exports = { let checker = new VersionChecker(this.parent).for('ember-cli-babel', 'npm'); if (checker.satisfies('>= 6.0.0')) { - const IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul'); - const excludes = this._getExcludes(); - - const appDir = path.join(this.project.root, 'app'); - if (fs.existsSync(appDir)) { - // Instrument the app directory. - let prefix = this.parent.isEmberCLIAddon() ? 'dummy' : this.parent.name(); - - getPlugins(this.app).push([IstanbulPlugin, { - exclude: excludes, - include: this._getIncludes(appDir, 'app', prefix) - }]); - } - - const addonDir = path.join(this.project.root, 'addon'); - if (fs.existsSync(addonDir)) { - // Instrument the addon directory. - let addon = this._findCoveredAddon(); - getPlugins(addon).push([IstanbulPlugin, { - exclude: excludes, - include: this._getIncludes(addonDir, 'addon', addon.name) - }]); - } + this.IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul'); + this._instrumentAppDirectory(); + this._instrumentAddonDirectory(); + this._instrumentInRepoAddonDirectories(); } else { this.project.ui.writeWarnLine( 'ember-cli-code-coverage: You are using an unsupported ember-cli-babel version,' + @@ -113,6 +88,62 @@ module.exports = { // Custom Methods + /** + * Instrument the "app" directory. + */ + _instrumentAppDirectory() { + const dir = path.join(this.project.root, 'app'); + let prefix = this.parent.isEmberCLIAddon() ? 'dummy' : this.parent.name(); + this._instrumentDirectory(this.app, dir, prefix); + }, + + /** + * Instrument the "addon" directory. + */ + _instrumentAddonDirectory() { + let addon = this._findCoveredAddon(); + if (addon) { + const dir = path.join(this.project.root, 'addon'); + this._instrumentDirectory(addon, dir, addon.name); + } + }, + + /** + * Instrument the in-repo-addon directories in "lib/*". + */ + _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); + }); + } + }, + + /** + * 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). + */ + _instrumentDirectory(appOrAddon, dir, modulePrefix) { + if (fs.existsSync(dir)) { + let options = appOrAddon.options = appOrAddon.options || {}; + options.babel = options.babel || {}; + let plugins = options.babel.plugins = options.babel.plugins || []; + plugins.push([this.IstanbulPlugin, { + exclude: this._getExcludes(), + include: this._getIncludes(dir, modulePrefix) + }]); + } + }, + /** * Thin wrapper around exists-sync that allows easy stubbing in tests * @param {String} path - path to check existence of @@ -132,14 +163,17 @@ module.exports = { /** * Get paths to include for coverage + * @param {String} dir Include all js files under this directory. + * @param {String} prefix The prefix to the ember module ('app', 'dummy' or the name of the addon). * @returns {Array} include paths */ - _getIncludes: function(dir, folder, prefix) { + _getIncludes: function(dir, prefix) { + let dirname = path.relative(this.project.root, dir); let globs = this.registry.extensionsForType('js').map((extension) => `**/*.${extension}`); return walkSync(dir, { directories: false, globs }).map(file => { let module = prefix + '/' + file.replace(EXT_RE, '.js'); - this.fileLookup[module] = path.join(folder, file); + this.fileLookup[module] = path.join(dirname, file); return module; }); }, diff --git a/test/integration/coverage-test.js b/test/integration/coverage-test.js index 84c3344c..d364c746 100644 --- a/test/integration/coverage-test.js +++ b/test/integration/coverage-test.js @@ -63,7 +63,6 @@ describe('`ember test`', function() { expect(file('coverage/lcov-report/index.html')).to.not.be.empty; expect(file('coverage/index.html')).to.not.be.empty; var summary = fs.readJSONSync('coverage/coverage-summary.json'); - console.log(summary); expect(summary.total.lines.pct).to.equal(100); }); }); From fbad578c786e18c741b3c3f3a3388ad9e6dbcf60 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Mon, 15 Jan 2018 17:03:01 +0000 Subject: [PATCH 05/10] Revert parallel changes (TODO: move to another PR) --- index.js | 6 ++ lib/attach-middleware.js | 16 +++++- lib/config.js | 1 + lib/coverage-merge.js | 60 ++++++++++++++++++++ package.json | 1 + test/integration/coverage-test.js | 38 +++++++++---- tests/dummy/config/coverage-nested-folder.js | 7 +++ tests/dummy/config/coverage-parallel.js | 5 ++ 8 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 lib/coverage-merge.js create mode 100644 tests/dummy/config/coverage-nested-folder.js create mode 100644 tests/dummy/config/coverage-parallel.js diff --git a/index.js b/index.js index d05ea663..734b6540 100644 --- a/index.js +++ b/index.js @@ -69,6 +69,12 @@ module.exports = { return undefined; }, + includedCommands: function () { + return { + 'coverage-merge': require('./lib/coverage-merge') + }; + }, + /** * If coverage is enabled attach coverage middleware to the express server run by ember-cli * @param {Object} startOptions - Express server start options diff --git a/lib/attach-middleware.js b/lib/attach-middleware.js index ce030649..6f1dab01 100644 --- a/lib/attach-middleware.js +++ b/lib/attach-middleware.js @@ -3,6 +3,8 @@ var bodyParser = require('body-parser'); var istanbul = require('istanbul-api'); var getConfig = require('./config'); +var path = require('path'); +var crypto = require('crypto'); function logError(err, req, res, next) { console.error(err.stack); @@ -15,19 +17,27 @@ function fixFilePaths(coverageData, fileLookup) { } module.exports = function(app, options) { - - let map = istanbul.libCoverage.createCoverageMap(); - app.post('/write-coverage', bodyParser.json({ limit: '50mb' }), function(req, res) { var config = getConfig(options.configPath); + if (config.parallel) { + config.coverageFolder = config.coverageFolder + '_' + crypto.randomBytes(4).toString('hex'); + if (config.reporters.indexOf('json') === -1) { + config.reporters.push('json'); + } + } + if (config.reporters.indexOf('json-summary') === -1) { config.reporters.push('json-summary'); } let reporter = istanbul.createReporter(); + if (config.coverageFolder) { + reporter.dir = path.join(options.root, config.coverageFolder); + } + let map = istanbul.libCoverage.createCoverageMap(); let coverage = req.body; Object.keys(options.fileLookup).forEach(filename => { diff --git a/lib/config.js b/lib/config.js index ab2c6064..d139aff0 100644 --- a/lib/config.js +++ b/lib/config.js @@ -38,6 +38,7 @@ function config(configPath) { function getDefaultConfig() { return { coverageEnvVar: 'COVERAGE', + coverageFolder: 'coverage', excludes: [ '*/mirage/**/*' ], diff --git a/lib/coverage-merge.js b/lib/coverage-merge.js new file mode 100644 index 00000000..18186376 --- /dev/null +++ b/lib/coverage-merge.js @@ -0,0 +1,60 @@ +'use strict'; + +var path = require('path'); +var getConfig = require('./config'); +var dir = require('node-dir'); +var Promise = require('rsvp').Promise; + +/** + * Merge together coverage files created when running in multiple threads, + * for example when being used with ember exam and parallel runs. + */ +module.exports = { + name: 'coverage-merge', + description: 'Merge multiple coverage files together.', + run: function () { + var istanbul = require('istanbul-api'); + var config = this._getConfig(); + + var coverageFolderSplit = config.coverageFolder.split('/'); + var coverageFolder = coverageFolderSplit.pop(); + var coverageRoot = this.project.root + '/' + coverageFolderSplit.join('/'); + var coverageDirRegex = new RegExp(coverageFolder + '_.*'); + + let reporter = istanbul.createReporter(); + reporter.dir = path.join(coverageRoot, coverageFolder); + let map = istanbul.libCoverage.createCoverageMap(); + + return new Promise(function (resolve, reject) { + dir.readFiles(coverageRoot, { matchDir: coverageDirRegex, match: /coverage-final\.json/ }, + function (err, coverageSummary, next) { + if (err) { + reject(err); + } + map.merge(JSON.parse(coverageSummary)); + next(); + }, + function (err) { + if (err) { + reject(err); + } + + if (config.reporters.indexOf('json-summary') === -1) { + config.reporters.push('json-summary'); + } + + reporter.addAll(config.reporters); + reporter.write(map); + resolve(); + }); + }); + }, + + /** + * Get project configuration + * @returns {Configuration} project configuration + */ + _getConfig: function () { + return getConfig(this.project.configPath()); + } +}; diff --git a/package.json b/package.json index f9711c99..94bda93c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "extend": "^3.0.0", "fs-extra": "^0.26.7", "istanbul-api": "^1.1.14", + "node-dir": "^0.1.16", "rsvp": "^3.2.1" }, "ember-addon": { diff --git a/test/integration/coverage-test.js b/test/integration/coverage-test.js index d364c746..63da1b92 100644 --- a/test/integration/coverage-test.js +++ b/test/integration/coverage-test.js @@ -43,28 +43,46 @@ describe('`ember test`', function() { }); }); - it('merges coverage when tests are run in parallel', function() { + it('excludes files when the configuration is set', function() { this.timeout(100000); - expect(dir('coverage')).to.not.exist; - return runCommand('ember', ['exam', '--split=2', '--parallel=true'], {env: {COVERAGE: true}}).then(function() { + fs.copySync('tests/dummy/config/coverage-excludes.js', 'tests/dummy/config/coverage.js'); + return runCommand('ember', ['test'], {env: {COVERAGE: true}}).then(function() { expect(file('coverage/lcov-report/index.html')).to.not.be.empty; expect(file('coverage/index.html')).to.not.be.empty; var summary = fs.readJSONSync('coverage/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(50); - expect(summary['addon/utils/my-covered-util.js'].lines.pct).to.equal(100); - expect(summary['addon/utils/my-uncovered-util.js'].lines.pct).to.equal(0); + expect(summary.total.lines.pct).to.equal(100); }); }); - it('excludes files when the configuration is set', function() { + it('uses parallel configuration and merges coverage when merge-coverage command is issued', function() { this.timeout(100000); - fs.copySync('tests/dummy/config/coverage-excludes.js', 'tests/dummy/config/coverage.js'); - return runCommand('ember', ['test'], {env: {COVERAGE: true}}).then(function() { + expect(dir('coverage')).to.not.exist; + fs.copySync('tests/dummy/config/coverage-parallel.js', 'tests/dummy/config/coverage.js'); + return runCommand('ember', ['exam', '--split=2', '--parallel=true'], {env: {COVERAGE: true}}).then(function() { + expect(dir('coverage')).to.not.exist; + return runCommand('ember', ['coverage-merge']); + }).then(function() { expect(file('coverage/lcov-report/index.html')).to.not.be.empty; expect(file('coverage/index.html')).to.not.be.empty; var summary = fs.readJSONSync('coverage/coverage-summary.json'); - expect(summary.total.lines.pct).to.equal(100); + expect(summary.total.lines.pct).to.equal(50); }); }); + it('uses nested coverageFolder and parallel configuration and run merge-coverage', function() { + this.timeout(100000); + var coverageFolder = 'coverage/abc/easy-as/123'; + + expect(dir(coverageFolder)).to.not.exist; + fs.copySync('tests/dummy/config/coverage-nested-folder.js', 'tests/dummy/config/coverage.js'); + return runCommand('ember', ['exam', '--split=2', '--parallel=true'], {env: {COVERAGE: true}}).then(function() { + expect(dir(coverageFolder)).to.not.exist; + return runCommand('ember', ['coverage-merge']); + }).then(function() { + expect(file(coverageFolder + '/lcov-report/index.html')).to.not.be.empty; + expect(file(coverageFolder + '/index.html')).to.not.be.empty; + var summary = fs.readJSONSync(coverageFolder + '/coverage-summary.json'); + expect(summary.total.lines.pct).to.equal(50); + }); + }); }); diff --git a/tests/dummy/config/coverage-nested-folder.js b/tests/dummy/config/coverage-nested-folder.js new file mode 100644 index 00000000..ae5fac98 --- /dev/null +++ b/tests/dummy/config/coverage-nested-folder.js @@ -0,0 +1,7 @@ +/*jshint node:true*/ +'use strict'; + +module.exports = { + coverageFolder: 'coverage/abc/easy-as/123', + parallel: true +}; diff --git a/tests/dummy/config/coverage-parallel.js b/tests/dummy/config/coverage-parallel.js new file mode 100644 index 00000000..a33514b6 --- /dev/null +++ b/tests/dummy/config/coverage-parallel.js @@ -0,0 +1,5 @@ +/* eslint-env node */ + +module.exports = { + parallel: true +}; From 9ee9309265af03c0f5504e105aa8865f42cdd277 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Mon, 15 Jan 2018 17:27:01 +0000 Subject: [PATCH 06/10] Only include test fixtures when testing the addon. --- index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.js b/index.js index 734b6540..98fade07 100644 --- a/index.js +++ b/index.js @@ -92,6 +92,13 @@ module.exports = { }); }, + treeFor() { + // Only include test fixtures when testing the addon. + if (this.app.env === 'test' && this._parentName() === this.name) { + return this._super.treeFor.apply(this, arguments); + } + }, + // Custom Methods /** From 76ee728c08a2e73937b8ca1a102762a9af2789dd Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Mon, 15 Jan 2018 18:38:43 +0000 Subject: [PATCH 07/10] Add index.js unit tests --- test/unit/index-test.js | 240 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 1 deletion(-) diff --git a/test/unit/index-test.js b/test/unit/index-test.js index 850903f6..41630093 100644 --- a/test/unit/index-test.js +++ b/test/unit/index-test.js @@ -3,6 +3,8 @@ var expect = require('chai').expect; var sinon = require('sinon'); var Index = require('../../index.js'); +var path = require('path'); + describe('index.js', function() { var sandbox; @@ -11,9 +13,11 @@ describe('index.js', function() { Index.registry = { extensionsForType: function() { - return ['hbs']; + return ['js']; } }; + Index.parent = Index.project = Index.app = Index.IstanbulPlugin = null; + sandbox.stub(Index, 'fileLookup', {}); }); afterEach(function() { @@ -109,6 +113,28 @@ describe('index.js', function() { }); }); + describe('_getIncludes', function() { + beforeEach(function() { + sandbox.stub(Index, 'project', { root: process.cwd() }); + }); + + it('gets files to include from the app directory', function() { + Index._getIncludes('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' + }); + }); + + it('gets files to include from the addon directory', function() { + Index._getIncludes('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' + }); + }); + }); + describe('_getExcludes', function() { beforeEach(function() { Index.parent = { @@ -303,4 +329,216 @@ describe('index.js', function() { expect(result.name).to.equal('my-addon'); }); }); + + describe('_instrumentDirectory', function() { + beforeEach(function() { + sandbox.stub(Index, 'IstanbulPlugin', 'istanbul'); + sandbox.stub(Index, '_getExcludes').returns([]); + sandbox.stub(Index, 'project', { root: process.cwd() }); + sandbox.stub(Index, 'parent', { + name() { return 'my-app' }, + }); + sandbox.stub(Index, 'app', {}); + }); + + describe('_instrumentAppDirectory', function() { + + describe('for an app', function() { + beforeEach(function() { + sandbox.stub(Index, 'parent', { + name() { return 'my-app' }, + isEmberCLIAddon() { return false } + }); + }); + + it('instruments the app directory', function() { + Index._instrumentAppDirectory(); + 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' + ] + } + ] + ] + } + } + }); + }); + }); + + describe('for an addon', function() { + beforeEach(function() { + sandbox.stub(Index, 'parent', { + name() { return 'my-app' }, + isEmberCLIAddon() { return true } + }); + }); + + it('instruments the app directory', function() { + Index._instrumentAppDirectory(); + 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('for an app', function() { + beforeEach(function() { + sandbox.stub(Index, '_findCoveredAddon').returns(null); + sandbox.spy(Index, '_instrumentDirectory'); + }); + + it('does not instrument the addon directory', function() { + Index._instrumentAddonDirectory(); + sinon.assert.notCalled(Index._instrumentDirectory); + }); + }); + + describe('for an addon', function() { + let addon = { + name: 'my-addon' + }; + + beforeEach(function() { + sandbox.stub(Index, '_findCoveredAddon').returns(addon); + }); + + afterEach(function() { + addon = null; + }); + + it('instruments the addon directory', function() { + Index._instrumentAddonDirectory(); + 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('for an app with no inrepo addons', function() { + beforeEach(function() { + sandbox.stub(Index, 'project', { pkg: { } }); + sandbox.spy(Index, '_instrumentDirectory'); + }); + + it('does not instrument any inrepo addon directories', function() { + Index._instrumentInRepoAddonDirectories(); + sinon.assert.notCalled(Index._instrumentDirectory); + }); + }); + + describe('for an app with an inrepo addon', function() { + let addon = {}; + + beforeEach(function() { + sandbox.stub(path, 'basename').returns('my-inrepo-addon'); + sandbox.stub(Index, 'project', { + pkg: { + 'ember-addon': { + paths: [ + '' + ] + } + }, + root: process.cwd(), + findAddonByName() { return addon; } + }); + }); + + afterEach(function() { + addon = null; + }); + + it('instruments the inrepo addon', function() { + Index._instrumentInRepoAddonDirectories(); + 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' + ] + } + ] + ] + } + } + }); + }); + }); + + }); + + }); + }); From 9a9a79f2cb8e7b92ed2f759bd2c7e55d8fde86f9 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Mon, 15 Jan 2018 19:19:06 +0000 Subject: [PATCH 08/10] Update some docs --- README.md | 5 +---- index.js | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bfdb4a6e..d1399405 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Code coverage using [Istanbul](https://github.com/gotwarlost/istanbul) for Ember * If using Mirage you need `ember-cli-mirage >= 0.1.13` * If using Pretender (even as a dependency of Mirage) you need `pretender >= 0.11.0` * If using Mirage or Pretender, you need to [set up a passthrough for coverage to be written](#create-a-passthrough-when-intercepting-all-ajax-requests-in-tests). +* `ember-cli-babel >= 6.0.0` ## Installation @@ -45,10 +46,6 @@ Configuration is optional. It should be put in a file at `config/coverage.js` (` - `coverageFolder`: Defaults to `coverage`. A folder relative to the root of your project to store coverage results. -- `useBabelInstrumenter`: Defaults to `false`. Whether or not to use Babel instrumenter instead of default instrumenter. The Babel instrumenter is useful when you are using features of ESNext as it uses your Babel configuration defined in `ember-cli-build.js`. - -- `babelPlugins`: Defaults to `['babel-plugin-transform-async-to-generator']`. When using the Babel instrumenter, this specifies a set of additional plugins to pass to the parser. Use this to parse specific ESNext features you may be using in your app (decorators, for instance). - - `parallel`: Defaults to `false`. Should be set to true if parallel testing is being used, for example when using [ember-exam](https://github.com/trentmwillis/ember-exam) with the `--parallel` flag. This will generate the coverage reports in directories suffixed with `_` to avoid overwriting other threads reports. These reports can be joined by using the `ember coverage-merge` command (potentially as part of the [posttest hook](https://docs.npmjs.com/misc/scripts) in your `package.json`). #### Example diff --git a/index.js b/index.js index 98fade07..de34bb3f 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,7 @@ function requireBabelPlugin(pluginName) { return plugin; } +// Regular expression to extract the file extension from a path. const EXT_RE = /\.[^\.]+$/; module.exports = { @@ -94,7 +95,7 @@ module.exports = { treeFor() { // Only include test fixtures when testing the addon. - if (this.app.env === 'test' && this._parentName() === this.name) { + if (this._parentName() === this.name) { return this._super.treeFor.apply(this, arguments); } }, From 47ee106698f9b6f6ced71dc74d3401c13bf0e0ea Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Wed, 17 Jan 2018 21:52:38 +0000 Subject: [PATCH 09/10] Add comment about .istanbul.yml to README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d1399405..779fc665 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Code coverage using [Istanbul](https://github.com/gotwarlost/istanbul) for Ember * If using Mocha, Testem `>= 1.6.0` for which you need ember-cli `> 2.4.3` * If using Mirage you need `ember-cli-mirage >= 0.1.13` * If using Pretender (even as a dependency of Mirage) you need `pretender >= 0.11.0` -* If using Mirage or Pretender, you need to [set up a passthrough for coverage to be written](#create-a-passthrough-when-intercepting-all-ajax-requests-in-tests). -* `ember-cli-babel >= 6.0.0` +* If using Mirage or Pretender, you need to [set up a passthrough for coverage to be written](#create-a-passthrough-when-intercepting-all-ajax-requests-in-tests). +* `ember-cli-babel >= 6.0.0` ## Installation @@ -34,7 +34,7 @@ When running with `parallel` set to true, the final reports can be merged by usi ## Configuration -Configuration is optional. It should be put in a file at `config/coverage.js` (`configPath` configuration in package.json is honored) +Configuration is optional. It should be put in a file at `config/coverage.js` (`configPath` configuration in package.json is honored). In addition to this you can configure Istanbul by adding a `.istanbul.yml` file to the root directory of you app (See https://github.com/gotwarlost/istanbul#configuring) #### Options @@ -55,7 +55,7 @@ Configuration is optional. It should be put in a file at `config/coverage.js` (` } ``` -## Create a passthrough when intercepting all ajax requests in tests +## Create a passthrough when intercepting all ajax requests in tests To work, this addon has to post coverage results back to a middleware at `/write-coverage`. From 25704f6473d4c1edd13437039428f6ddfe32035a Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Wed, 17 Jan 2018 21:53:53 +0000 Subject: [PATCH 10/10] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 779fc665..e56fb5fa 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ When running with `parallel` set to true, the final reports can be merged by usi ## Configuration -Configuration is optional. It should be put in a file at `config/coverage.js` (`configPath` configuration in package.json is honored). In addition to this you can configure Istanbul by adding a `.istanbul.yml` file to the root directory of you app (See https://github.com/gotwarlost/istanbul#configuring) +Configuration is optional. It should be put in a file at `config/coverage.js` (`configPath` configuration in package.json is honored). In addition to this you can configure Istanbul by adding a `.istanbul.yml` file to the root directory of your app (See https://github.com/gotwarlost/istanbul#configuring) #### Options