Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +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).
* 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
Expand All @@ -33,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 your app (See https://github.com/gotwarlost/istanbul#configuring)

#### Options

Expand All @@ -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 `_<random_string>` 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
Expand All @@ -58,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`.

Expand Down
3 changes: 3 additions & 0 deletions addon/utils/my-covered-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function myCoveredUtil() {
return true;
}
3 changes: 3 additions & 0 deletions addon/utils/my-uncovered-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function myUncoveredUtil() {
return true;
}
1 change: 1 addition & 0 deletions app/utils/my-covered-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-cli-code-coverage/utils/my-covered-util';
2 changes: 2 additions & 0 deletions app/utils/my-uncovered-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from 'ember-cli-code-coverage/utils/my-uncovered-util';

133 changes: 102 additions & 31 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
const walkSync = require('walk-sync');
const VersionChecker = require('ember-cli-version-checker');

function requireBabelPlugin(pluginName) {
Expand All @@ -21,46 +22,34 @@ function requireBabelPlugin(pluginName) {
return plugin;
}

// Regular expression to extract the file extension from a path.
const EXT_RE = /\.[^\.]+$/;

module.exports = {
name: 'ember-cli-code-coverage',

// Ember Methods

_getParentOptions: function() {
let options;

// 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<String, String>}
*/
fileLookup: null,

return options;
},
// Ember Methods

included: function() {
this._super.included.apply(this, arguments);

let parentOptions = this._getParentOptions();
this.fileLookup = {};

if (!this._registeredWithBabel && this._isCoverageEnabled()) {
let checker = new VersionChecker(this.parent).for('ember-cli-babel', 'npm');

if (checker.satisfies('>= 6.0.0')) {
const IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul');
this.IstanbulPlugin = requireBabelPlugin('babel-plugin-istanbul');

// Create babel options if they do not exist
parentOptions.babel = parentOptions.babel || {};

// Create and pull off babel plugins
let plugins = parentOptions.babel.plugins = parentOptions.babel.plugins || [];

plugins.push([IstanbulPlugin, { exclude: this._getExcludes() }]);
this._instrumentAppDirectory();
this._instrumentAddonDirectory();
this._instrumentInRepoAddonDirectories();
} else {
this.project.ui.writeWarnLine(
'ember-cli-code-coverage: You are using an unsupported ember-cli-babel version,' +
Expand All @@ -75,7 +64,7 @@ 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;
Expand All @@ -97,11 +86,78 @@ 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
});
},

treeFor() {
// Only include test fixtures when testing the addon.
if (this._parentName() === this.name) {
return this._super.treeFor.apply(this, arguments);
}
},

// 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
Expand All @@ -119,14 +175,29 @@ module.exports = {
return config(this.project.configPath());
},

/**
* 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<String>} include paths
*/
_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(dirname, file);
return module;
});
},

/**
* Get paths to exclude from coverage
* @returns {Array<String>} exclude paths
*/
_getExcludes: function() {
var excludes = this._getConfig().excludes || [];

return excludes;
return this._getConfig().excludes || [];
},

/**
Expand Down
17 changes: 10 additions & 7 deletions lib/attach-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
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);
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;
}

Expand All @@ -35,12 +34,16 @@ module.exports = function(app, options) {
}

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(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);
Expand Down
1 change: 0 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ function getDefaultConfig() {
excludes: [
'*/mirage/**/*'
],
useBabelInstrumenter: false,
reporters: [
'html',
'lcov'
Expand Down
1 change: 1 addition & 0 deletions lib/coverage-merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
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) {
Expand Down
18 changes: 4 additions & 14 deletions lib/templates/test-body-footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {});
Expand Down Expand Up @@ -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(' | '));
Expand Down
11 changes: 2 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -59,21 +58,15 @@
"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",
Expand Down
Loading