Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Configuration is optional. It should be put in a file at `config/coverage.js` (`

- `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`).

- `includeTranspiledSources`: Defaults to `[]`. Should include a list of transpiled JavaScript source extensions to be included in the coverage instrumentation. However, the compiled output is what will be instrumented so this will only be a close approximation of the source coverage.

#### Example
```js
module.exports = {
Expand Down
62 changes: 46 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = {
return undefined;
},

preprocessTree: function(type, tree) {
postprocessTree: function(type, tree) {
var useBabelInstrumenter = this._getConfig().useBabelInstrumenter === true;
var babelPlugins = this._getConfig().babelPlugins;

Expand All @@ -53,11 +53,8 @@ module.exports = {
annotation: 'Instrumenting for code coverage',
appName: this._parentName(),
appRoot: this.parent.root,
babelOptions: this.app.options.babel,
isAddon: this.project.isEmberCLIAddon(),
useBabelInstrumenter: useBabelInstrumenter,
babelPlugins: babelPlugins,
templateExtensions: this.registry.extensionsForType('template')
preCompiledExtensions: this.registry.extensionsForType('template').concat(this._getTranspiledSourceExtensions())
});

return new BroccoliMergeTrees([tree, instrumentedNode], { overwrite: true });
Expand Down Expand Up @@ -96,7 +93,18 @@ module.exports = {
return true;
}

return this._doesTemplateFileExist(relativePath);
return this._doesTemplateFileExist(relativePath) || this._doesFileExistAsTranspilationSource(relativePath);
},

/**
* Checks if a file exists as a transpiled source specified in the addon configuration
* @param {String} relativePath path to file within current app
* @returns {Boolean} whether or not the file exists within the current app
* @private
*/
_doesFileExistAsTranspilationSource: function(relativePath) {
var sourceExtensions = this._getTranspiledSourceExtensions();
return this._doesPrecompiledFileExist(relativePath, sourceExtensions);
},

/**
Expand Down Expand Up @@ -146,18 +154,19 @@ module.exports = {
},

/**
* Check if a template file exists within the current app/addon
* Note: Template files are already compiled into JavaScript files so we must
* check for the pre-compiled .hbs file
* @param {String} relativePath - path to file within current app/addon
* @returns {Boolean} whether or not the file exists within the current app/addon
* Checks if a file exists as a precompilation source
* @param {String} relativePath path to the file within the current app/addon
* @param {String[]} extensions list of precompilation extensions that the file may exist as
* @returns {boolean} Flag indicating whether the file exists with any of the precompilation extensions
* @private
*/
_doesTemplateFileExist: function(relativePath) {
var templateExtensions = this.registry.extensionsForType('template');
_doesPrecompiledFileExist: function(relativePath, extensions) {
var sourceExtensions = Array.isArray(extensions) ? extensions : [];
var extension, extensionPath;

for (var i = 0, len = templateExtensions.length; i < len; i++) {
var extension = templateExtensions[i];
var extensionPath = relativePath.replace('.js', '.' + extension);
for (var i = 0, len = sourceExtensions.length; i < len; i++) {
extension = sourceExtensions[i];
extensionPath = relativePath.replace('.js', '.' + extension);

if (this._existsSync(extensionPath)) {
return true;
Expand All @@ -167,6 +176,18 @@ module.exports = {
return false;
},

/**
* Check if a template file exists within the current app/addon
* Note: Template files are already compiled into JavaScript files so we must
* check for the pre-compiled .hbs file
* @param {String} relativePath - path to file within current app/addon
* @returns {Boolean} whether or not the file exists within the current app/addon
*/
_doesTemplateFileExist: function(relativePath) {
var templateExtensions = this.registry.extensionsForType('template');
return this._doesPrecompiledFileExist(relativePath, templateExtensions);
},

/**
* Thin wrapper around exists-sync that allows easy stubbing in tests
* @param {String} path - path to check existence of
Expand Down Expand Up @@ -209,6 +230,15 @@ module.exports = {
return config(this.project.configPath());
},

/**
* Gets the list of transpiled source extensions from the host configuration options
* @returns {String[]} list of transpilation source extensions if provided
* @private
*/
_getTranspiledSourceExtensions: function() {
return this._getConfig().includeTranspiledSources || [];
},

/**
* Get paths to exclude from coverage
* @returns {Array<String>} exclude paths
Expand Down
3 changes: 2 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ function getDefaultConfig() {
reporters: [
'html',
'lcov'
]
],
includeTranspiledSources: []
};
}

Expand Down
61 changes: 18 additions & 43 deletions lib/coverage-instrumenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
require('string.prototype.startswith');
var existsSync = require('exists-sync');
var Filter = require('broccoli-filter');
var BabelInstrumenter = require('./babel-istanbul-instrumenter');
var Instrumenter = require('istanbul').Instrumenter;
var EmberInstrumenter = require('./ember-instrumenter');
var path = require('path');

function getPathForRealFile(relativePath, root, templateExtensions) {

function getPathForRealFile(relativePath, root, extensions) {
if (existsSync(path.join(root, relativePath))) {
return relativePath
}

for (var i = 0, len = templateExtensions.length; i < len; i++) {
var extension = templateExtensions[i];
for (var i = 0, len = extensions.length; i < len; i++) {
var extension = extensions[i];
var templatePath = relativePath.replace('.js', '.' + extension);

if (existsSync(templatePath)) {
Expand All @@ -24,16 +24,16 @@ function getPathForRealFile(relativePath, root, templateExtensions) {
return null;
}

function fixPath(relativePath, name, root, templateExtensions, isAddon) {
function fixPath(relativePath, name, root, extensions, isAddon) {
// Handle addons
if (isAddon) {
// Handle addons (served from dummy app)
if (relativePath.startsWith('dummy')) {
relativePath = relativePath.replace('dummy', 'app');
var dummyPath = path.join('tests', 'dummy', relativePath);
return (
getPathForRealFile(dummyPath, root, templateExtensions) ||
getPathForRealFile(relativePath, root, templateExtensions) ||
getPathForRealFile(dummyPath, root, extensions) ||
getPathForRealFile(relativePath, root, extensions) ||
relativePath
);
}
Expand All @@ -43,13 +43,13 @@ function fixPath(relativePath, name, root, templateExtensions, isAddon) {
if (regex.test(relativePath)) {
relativePath = relativePath.replace(regex, 'addon/');
return (
getPathForRealFile(relativePath, root, templateExtensions) ||
getPathForRealFile(relativePath, root, extensions) ||
relativePath
);
}
} else {
relativePath = relativePath.replace(name, 'app');
return getPathForRealFile(relativePath, root, templateExtensions) || relativePath;
return getPathForRealFile(relativePath, root, extensions) || relativePath;
}

return relativePath;
Expand All @@ -63,22 +63,8 @@ function CoverageInstrumenter(inputNode, options) {
this._appName = options.appName;
this._appRoot = options.appRoot;
this._isAddon = options.isAddon;
this._useBabelInstrumenter = options.useBabelInstrumenter;
this._babelPlugins = options.babelPlugins;

this._babelOptions = options.babelOptions || {};

// The presence of the following babel options cause tests to fail so let's
// simply remove them from the babel config
[
'compileModules',
'resolveModuleSource',
'includePolyfill'
].forEach(function(key) {
delete this._babelOptions[key];
}.bind(this));

this._templateExtensions = options.templateExtensions;
this._preCompiledExtensions = options.preCompiledExtensions;

Filter.call(this, inputNode, {
annotation: options.annotation
Expand All @@ -89,29 +75,18 @@ CoverageInstrumenter.prototype.extensions = ['js'];
CoverageInstrumenter.prototype.targetExtension = 'js';

CoverageInstrumenter.prototype.processString = function(content, relativePath) {
var instrumenter

if (this._useBabelInstrumenter) {
instrumenter = new BabelInstrumenter({
babel: this._babelOptions,
plugins: this._babelPlugins,
embedSource: true,
noAutoWrap: true
});
} else {
instrumenter = new Instrumenter({
embedSource: true,
esModules: true,
noAutoWrap: true
});
}
var instrumenter;
instrumenter = new EmberInstrumenter({
embedSource: true,
noAutoWrap: true
});

relativePath = fixPath(relativePath, this._appName, this._appRoot, this._templateExtensions, this._isAddon);
relativePath = fixPath(relativePath, this._appName, this._appRoot, this._preCompiledExtensions, this._isAddon);

try {
return instrumenter.instrumentSync(content, relativePath);
} catch (e) {
console.error('Unable to cover:', relativePath, '. Newer JS features may need Babel instrumentation to work. Try setting useBabelInstrumenter to true in your config/coverage.js.\n', e.stack);
console.error('Unable to cover:', relativePath, '. Please try to enable source maps "inline" on babel conf.\n', e.stack);
}
};

Expand Down
39 changes: 21 additions & 18 deletions lib/babel-istanbul-instrumenter.js → lib/ember-instrumenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
/**
* This is a modified copy of the isparta instrumenter
* @reference: https://github.com/douglasduteil/isparta/blob/master/src/instrumenter.js
*
* Modified again by @igbopie to make it generic (it wont transpile any code)
*/

var extend = require('extend');
var istanbul = require('istanbul');
var babelTransform = require('babel-core').transform;
var esprima = require('esprima');
var escodegen = require('escodegen');
var SourceMapConsumer = require('source-map').SourceMapConsumer;
Expand All @@ -21,43 +22,45 @@ function Instrumenter(options) {

istanbul.Instrumenter.call(this, options); // Call super constructor

this.babelOptions = extend({
sourceMap: true
}, options && options.babel || {});
this.plugins = options.plugins;

return this;
}

// Make custom instrumenter extend istanbul instrumenter
Instrumenter.prototype = Object.create(istanbul.Instrumenter.prototype);
Instrumenter.prototype.constructor = Instrumenter;

/**
* With the new modification, this code will be executed at the end of the build, so all the
* resources are available and transpiled already. That way, we will use source maps generated by
* babel/typescript to extract the original source code.
*
* This way our code won't be affected by the way we transpile our code (Only by the way the sourcemaps are generated).
*
* Babel/Typescript needs to be setup to ouput inline source maps.
*/
Instrumenter.prototype.instrumentSync = function(code, fileName) {
var plugins = this.babelOptions.plugins;
// If we're running in coverage, we're assuming that this is running in CI or in some
// form of test scenario and not being built for production. So it's fine to always
// force this plugin to work.
for (var plugin of this.plugins) {
plugins.push(plugin);
}
var result = this._r = (0, babelTransform)(code, extend({}, this.babelOptions, { filename: fileName }));
this._babelMap = new SourceMapConsumer(result.map);
// Source map base64 extraction from file. Only inline supported for now.
var reg = new RegExp('# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.*)$');
var base64 = reg.exec(code)[1];
var srcMapStr = new Buffer(base64, 'base64').toString('utf8');
var map = JSON.parse(srcMapStr);

// PARSE
var program = esprima.parse(result.code, {
var program = esprima.parse(code, {
loc: true,
range: true,
tokens: this.opts.preserveComments,
comment: true,
sourceType: 'module'
});

this._srcMap = new SourceMapConsumer(map);

if (this.opts.preserveComments) {
program = escodegen.attachComments(program, program.comments, program.tokens);
}

return this.instrumentASTSync(program, fileName, code);
return this.instrumentASTSync(program, fileName, map.sourcesContent[0]);
};

Instrumenter.prototype.getPreamble = function(sourceCode, emitUseStrict) {
Expand Down Expand Up @@ -177,7 +180,7 @@ Instrumenter.prototype._getOriginalPositionsFor = function(generatedPositions) {
function reducer(originalPositions, current) {
var generatedPosition = current[0];
var position = current[1];
var originalPosition = this._babelMap.originalPositionFor(generatedPosition);
var originalPosition = this._srcMap.originalPositionFor(generatedPosition);
// Remove extra keys
delete originalPosition.name;
delete originalPosition.source;
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ember-cli-code-coverage",
"version": "0.4.2",
"version": "0.5.0",
"description": "Code coverage for ember projects using Istanbul",
"directories": {
"doc": "doc",
Expand Down Expand Up @@ -55,8 +55,6 @@
"ember-addon"
],
"dependencies": {
"babel-core": "^6.24.1",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"body-parser": "^1.15.0",
"broccoli-filter": "^1.2.3",
"broccoli-funnel": "^1.0.1",
Expand Down
Loading