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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@
/libpeerconnection.log
npm-debug.log*
testem.log

yalc.lock
.yalc
yarn-error.log
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# ember-cli-code-coverage [![npm version](https://badge.fury.io/js/ember-cli-code-coverage.svg)](http://badge.fury.io/js/ember-cli-code-coverage) [![Build Status](https://travis-ci.org/kategengler/ember-cli-code-coverage.svg?branch=master)](https://travis-ci.org/kategengler/ember-cli-code-coverage)
# ember-cli-nacho-coverage

Code coverage using [Istanbul](https://github.com/gotwarlost/istanbul) for Ember apps.
Code coverage using [Istanbul](https://github.com/gotwarlost/istanbul) for Ember apps that works with any transpiler.

## Requirements
* 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).


## Installation

* `ember install ember-cli-code-coverage`
* `ember install ember-cli-nacho-coverage`

## Usage

Expand All @@ -33,7 +33,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. Use `ember-cli-build.js`, as `ember-cli-nacho-coverage`.

#### Options

Expand All @@ -45,20 +45,18 @@ 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`).

- `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 = {
coverageEnvVar: 'COV'
}
```

## 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
142 changes: 100 additions & 42 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,36 @@ var attachMiddleware = require('./lib/attach-middleware');
var config = require('./lib/config');

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

// Ember Methods
config() {
if (this._isCoverageEnabled()) {
this.setRequiredConfig();
}
return {
'ember-cli-nacho-coverage': this._getConfig()
}
},

setRequiredConfig() {
let babelConf = this.app.options.babel;
if (!babelConf) {
this.app.options.babel = {};
babelConf = this.app.options.babel;
}

this.app.options.babel.sourceMaps = 'inline';
},

included: function() {
included() {
if (this._isCoverageEnabled()) {
this.setRequiredConfig();
}
if (this._isCoverageEnabled() && this.parent.isEmberCLIAddon()) {
var coveredAddon = this._findCoveredAddon();
var coverageAddonContext = this;

const coveredAddon = this._findCoveredAddon();
const coverageAddonContext = this;

coveredAddon.processedAddonJsFiles = function (tree){
var instrumentedTree = coverageAddonContext.preprocessTree('addon-js', this.addonJsFiles(tree));
Expand All @@ -30,40 +52,34 @@ 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();
const template = fs.readFileSync(path.join(__dirname, 'lib', 'templates', 'test-body-footer.html')).toString();
return template.replace('{%PROJECT_NAME%}', this._parentName());
}

return undefined;
},

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

postprocessTree: function(type, tree) {
if (!this._isCoverageEnabled() || (type !== 'js' && type !=='addon-js')) {
return tree;
}

var appFiles = new Funnel(tree, {
const appFiles = new Funnel(tree, {
exclude: this._getExcludes()
});

var instrumentedNode = new CoverageInstrumenter(appFiles, {
const instrumentedNode = new CoverageInstrumenter(appFiles, {
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 });
},

includedCommands: function () {
includedCommands() {
return {
'coverage-merge': require('./lib/coverage-merge')
};
Expand All @@ -73,13 +89,13 @@ module.exports = {
* If coverage is enabled attach coverage middleware to the express server run by ember-cli
* @param {Object} startOptions - Express server start options
*/
serverMiddleware: function(startOptions) {
serverMiddleware(startOptions) {
this.testemMiddleware(startOptions.app);
},

testemMiddleware: function(app) {
testemMiddleware(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, config: this._getConfig() });
},

// Custom Methods
Expand All @@ -89,22 +105,33 @@ module.exports = {
* @param {String} relativePath - path to file within current app
* @returns {Boolean} whether or not the file exists within the current app
*/
_doesFileExistInCurrentProjectApp: function(relativePath) {
_doesFileExistInCurrentProjectApp(relativePath) {
relativePath = path.join('app', relativePath);

if (this._existsSync(relativePath)) {
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(relativePath) {
var sourceExtensions = this._getTranspiledSourceExtensions();
return this._doesPrecompiledFileExist(relativePath, sourceExtensions);
},

/**
* Check if a file exists within the current addon directory
* @param {String} relativePath - path to file within current app
* @returns {Boolean} whether or not the file exists within the current app
*/
_doesFileExistInCurrentProjectAddon: function(relativePath) {
_doesFileExistInCurrentProjectAddon(relativePath) {
relativePath = path.join('addon', relativePath);

if (this._existsSync(relativePath)) {
Expand All @@ -119,7 +146,7 @@ module.exports = {
* @param {String} relativePath - path to file within current app
* @returns {Boolean} whether or not the file exists within the current app
*/
_doesFileExistInCurrentProjectAddonModule: function(relativePath) {
_doesFileExistInCurrentProjectAddonModule(relativePath) {
var relativePathWithoutProjectNamePrefix = relativePath.replace('modules' + '/' + this._parentName(), '');
var _relativePath = 'addon/' + relativePathWithoutProjectNamePrefix;

Expand All @@ -135,7 +162,7 @@ module.exports = {
* @param {String} relativePath - path to file within dummy app
* @returns {Boolean} whether or not the file exists within the dummy app
*/
_doesFileExistInDummyApp: function(relativePath) {
_doesFileExistInDummyApp(relativePath) {
relativePath = path.join('tests', 'dummy', 'app', relativePath);

if (this._existsSync(relativePath)) {
Expand All @@ -146,18 +173,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(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,12 +195,24 @@ 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(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
* @returns {Boolean} whether or not path exists
*/
_existsSync: function(path) {
_existsSync(path) {
return existsSync(path);
},

Expand All @@ -182,7 +222,7 @@ module.exports = {
* @param {String} relativePath - relative path to file
* @returns {Boolean} whether or not to filter out file
*/
_filterOutAddonFiles: function(name, relativePath) {
_filterOutAddonFiles(name, relativePath) {
if (relativePath.indexOf(name + '/') === 0) {
relativePath = relativePath.replace(name + '/', '');
}
Expand All @@ -205,15 +245,33 @@ module.exports = {
* Get project configuration
* @returns {Configuration} project configuration
*/
_getConfig: function() {
return config(this.project.configPath());
_getConfig() {
const appOptions = this._getAddonOptions() || {};
const options = appOptions['ember-cli-nacho-coverage'];
if (!this.myConfig) {
this.myConfig = config(this.project.configPath(), options);
}
return this.myConfig;
},

_getAddonOptions() {
return (this.parent && this.parent.options) || (this.app && this.app.options) || {};
},

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

/**
* Get paths to exclude from coverage
* @returns {Array<String>} exclude paths
*/
_getExcludes: function() {
_getExcludes() {
var excludes = this._getConfig().excludes || [];
var name = this._parentName();
excludes.push(this._filterOutAddonFiles.bind(this, name));
Expand All @@ -225,7 +283,7 @@ module.exports = {
* Determine whether or not coverage is enabled
* @returns {Boolean} whether or not coverage is enabled
*/
_isCoverageEnabled: function() {
_isCoverageEnabled() {
var value = process.env[this._getConfig().coverageEnvVar] || false;

if (value.toLowerCase) {
Expand All @@ -239,7 +297,7 @@ module.exports = {
* Determine the name of the parent app or addon.
* @returns {String} the name of the parent
*/
_parentName: function() {
_parentName() {
if (this.parent.isEmberCLIAddon()) {
return this._findCoveredAddon().name;
} else {
Expand All @@ -251,7 +309,7 @@ module.exports = {
* Find the addon (if any) that's being covered.
* @returns {Addon} the addon under test
*/
_findCoveredAddon: function() {
_findCoveredAddon() {
if (!this._coveredAddon) {
this._coveredAddon = this.project.findAddonByName(this.project.pkg.name);
}
Expand Down
1 change: 1 addition & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]}
3 changes: 1 addition & 2 deletions lib/attach-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

var bodyParser = require('body-parser');
var Istanbul = require('istanbul');
var config = require('./config');
var path = require('path');
var fs = require('fs-extra');
var crypto = require('crypto');
Expand All @@ -17,7 +16,7 @@ module.exports = function(app, options) {
bodyParser.json({ limit: '50mb' }),
function(req, res) {
var collector = new Istanbul.Collector();
var _config = config(options.configPath);
var _config = options.config;

if (_config.parallel) {
_config.coverageFolder = _config.coverageFolder + '_' + crypto.randomBytes(4).toString('hex');
Expand Down
Loading