Skip to content

Commit

Permalink
Support reporting coverage for tests
Browse files Browse the repository at this point in the history
If `coverage` options is set, template files will be instrumented
before execution (can be enabled per-technology). If templates are
built with source map, there is an option to show coverage for
original files.
There is also an option to filter out some sources from report. By
default, all content of `node_modules` and `libs` will be omitted.
  • Loading branch information
Sergey Tatarintsev committed Nov 27, 2014
1 parent 4f49c91 commit 640985c
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 19 deletions.
81 changes: 81 additions & 0 deletions lib/coverage-object.js
@@ -0,0 +1,81 @@
var _ = require('lodash'),
inherit = require('inherit');

module.exports = inherit({
__constructor: function () {
this._indices = {
};
this.coverage = {
};
},

addStatement: function (start, end, counter) {
var sourceName = start.source,
sourceObj = this._getFileObject(sourceName),
idx = this._indices[sourceName].s++;
sourceObj.statementMap[idx] = {
start: getLocation(start),
end: getLocation(end)
};
sourceObj.s[idx] = counter;
},

addFunction: function (name, start, end, counter) {
var sourceName = start.source,
sourceObj = this._getFileObject(sourceName),
idx = this._indices[sourceName].f++;
sourceObj.fnMap[idx] = {
name: name,
line: start.line,
loc: {
start: getLocation(start),
end: getLocation(end)
}
};

sourceObj.f[idx] = counter;
},

addBranch: function (type, locations, counter) {
var sourceName = locations[0].start.source,
sourceObj = this._getFileObject(sourceName),
idx = this._indices[sourceName].b++;
sourceObj.branchMap[idx] = {
line: locations[0].start.line,
type: type,
locations: locations.map(function (loc) {
return {
start: getLocation(loc.start),
end: getLocation(loc.end)
};
})
};
sourceObj.b[idx] = counter;
},

_getFileObject: function (sourceName) {
var fileObject = this.coverage[sourceName];
if (!fileObject) {
fileObject = this.coverage[sourceName] = {
path: sourceName,
s: {},
b: {},
f: {},
statementMap: {},
fnMap: {},
branchMap: {}
};

this._indices[sourceName] = {
s: 1,
b: 1,
f: 1
};
}
return fileObject;
}
});

function getLocation(soureMapLocation) {
return _.pick(soureMapLocation, 'line', 'column');
}
26 changes: 22 additions & 4 deletions lib/node-configurator.js
@@ -1,6 +1,8 @@
var path = require('path'),
fs = require('fs'),

_ = require('lodash'),

levels = require('enb-bem-techs/techs/levels'),
files = require('enb-bem-techs/techs/files'),

Expand All @@ -10,7 +12,9 @@ var path = require('path'),
mergeBemdecl = require('enb-bem-techs/techs/merge-bemdecl'),

references = require('./techs/references'),
spec = require('./techs/tmpl-spec');
istanbul = require('./techs/istanbul'),
spec = require('./techs/tmpl-spec'),
instrumentedTarget = require('./util').instrumentedTarget;

exports.configure = function (config, options) {
var pattern = path.join(options.destPath, '*'),
Expand All @@ -20,6 +24,7 @@ exports.configure = function (config, options) {
var nodePath = nodeConfig.getNodePath(),
sublevel = path.join(nodePath, 'blocks'),
engines = options.engines,
coverageEngines = options.coverage.engines,
engineTargets = [];

if (fs.existsSync(sublevel)) {
Expand Down Expand Up @@ -47,13 +52,26 @@ exports.configure = function (config, options) {

engines.forEach(function (engine) {
nodeConfig.addTech([engine.tech, engine.options]);
nodeConfig.addTarget(engine.target);
engineTargets.push(engine.target);
if (_.contains(coverageEngines, engine.name)) {
var instrumented = instrumentedTarget(engine.target);
nodeConfig.addTech([
istanbul,
{ source: engine.target, target: instrumented }
]);
engineTargets.push(instrumented);
} else {
engineTargets.push(engine.target);
}
});

nodeConfig.addTechs([
[references, { dirsTarget: '?.base.dirs' }],
[spec, { engines: engines, engineTargets: engineTargets, saveHtml: options.saveHtml }]
[spec, {
engines: engines,
engineTargets: engineTargets,
saveHtml: options.saveHtml,
coverageEngines: coverageEngines
}]
]);

nodeConfig.addTarget('?.tmpl-spec.js');
Expand Down
26 changes: 23 additions & 3 deletions lib/plugin.js
Expand Up @@ -22,7 +22,14 @@ module.exports = function (helper) {

_init: function (options) {
var root = helper.getRootPath(),
engines = options.engines;
engines = options.engines,
coverage = options.coverage || {};

if (coverage === true) {
coverage = {
engines: Object.keys(engines)
};
}

return {
_options: options,
Expand Down Expand Up @@ -58,6 +65,18 @@ module.exports = function (helper) {
};
}),
sourceLevels: options.sourceLevels || options.levels,
coverage: _.defaults(coverage, {
engines: [],
reportDirectory: 'coverage',
exclude: [
'**/node_modules/**',
'**/libs/**'
],
reporters: process.env.BEM_TMPL_SPECS_COV_REPORTERS ?
process.env.BEM_TMPL_SPECS_COV_REPORTERS.split(',')
: ['lcov']
}),

referenceDirSuffixes: options.referenceDirSuffixes || ['tmpl-specs'],
saveHtml: (typeof process.env.BEM_TMPL_SPECS_SAVE_HTML === 'undefined' ?
options.saveHtml :
Expand Down Expand Up @@ -106,7 +125,8 @@ module.exports = function (helper) {
destPath: options.destPath,
sourceLevels: options.sourceLevels,
engines: options.engines,
saveHtml: options.saveHtml
saveHtml: options.saveHtml,
coverage: options.coverage
});
});
},
Expand Down Expand Up @@ -142,7 +162,7 @@ module.exports = function (helper) {
return path.join(options.root, node, basename + '.tmpl-spec.js');
});

return filesToRun.length && runner.run(filesToRun)
return filesToRun.length && runner.run(filesToRun, options)
.fail(function (err) {
if (err.stack) {
console.error(err.stack);
Expand Down
53 changes: 50 additions & 3 deletions lib/runner.js
@@ -1,7 +1,12 @@
var vow = require('vow'),
Mocha = require('mocha');
Mocha = require('mocha'),
istanbul = require('istanbul'),
minimatch = require('minimatch'),
_ = require('lodash'),

exports.run = function (files) {
unmapCoverageObject = require('./unmap-coverage');

exports.run = function (files, opts) {
var defer = vow.defer(),
mocha = new Mocha({
ui: 'bdd',
Expand All @@ -10,7 +15,15 @@ exports.run = function (files) {

mocha.files = files;
mocha.run(function (failures) {
failures ? defer.reject(failures) : defer.resolve();
function resolvePromise() {
failures ? defer.reject(failures) : defer.resolve();
}

if (opts.coverage.engines.length > 0) {
processCoverage(opts.coverage).done(resolvePromise);
} else {
resolvePromise();
}

process.on('exit', function () {
process.exit(failures);
Expand Down Expand Up @@ -49,3 +62,37 @@ function getReporters() {

return reporters;
}

function processCoverage(coverageOpts) {
var coverage = global.__coverage__;

return unmapCoverageObject(coverage)
.then(function (unmapedCoverage) {
return filterAndSaveCoverage(unmapedCoverage, coverageOpts);
});
}

function filterAndSaveCoverage(coverage, opts) {
coverage = filterCoverage(coverage, opts.exclude);
return saveCoverageReport(coverage, opts);
}

function filterCoverage(coverage, exclude) {
return _.omit(coverage, function (value, fileName) {
return exclude.some(function (pattern) {
return minimatch(fileName, pattern, { matchBase: true });
});
});
}

function saveCoverageReport(coverage, opts) {
var defer = vow.defer(),
reporter = new istanbul.Reporter(null, opts.reportDirectory);
reporter.addAll(opts.reporters);
var collector = new istanbul.Collector();
collector.add(coverage);
reporter.write(collector, false, function () {
defer.resolve();
});
return defer.promise();
}
15 changes: 15 additions & 0 deletions lib/techs/istanbul.js
@@ -0,0 +1,15 @@
var Instrumenter = require('istanbul').Instrumenter,
fs = require('vow-fs');

module.exports = require('enb/lib/build-flow').create()
.name('istanbul')
.target('target', '?.instrumented.js')
.useSourceFilename('source', '?.js')
.builder(function (source) {
return fs.read(source, 'utf8')
.then(function (content) {
var instrumenter = new Instrumenter();
return instrumenter.instrumentSync(content, source);
});
})
.createTech();
12 changes: 9 additions & 3 deletions lib/techs/tmpl-spec.js
@@ -1,15 +1,18 @@
var path = require('path'),
vfs = require('enb/lib/fs/async-fs'),
readAsset = vfs.read(path.join(__dirname, '..', 'assets', 'tmpl-spec.jst')),
template = require('lodash').template,
_ = require('lodash'),
template = _.template,
htmlDifferFilename = require.resolve('html-differ'),
jsBeautifyFilename = require.resolve('js-beautify');
jsBeautifyFilename = require.resolve('js-beautify'),
instrumentedTarget = require('../util').instrumentedTarget;

module.exports = require('enb/lib/build-flow').create()
.name('tmpl-spec')
.target('target', '?.tmpl-spec.js')
.defineRequiredOption('engines')
.defineOption('saveHtml', false)
.defineOption('coverageEngines', [])
.useSourceFilename('references', '?.references.js')
.useSourceListFilenames('engineTargets', [])
.needRebuild(function (cache) {
Expand All @@ -25,6 +28,7 @@ module.exports = require('enb/lib/build-flow').create()
references = require(referencesFilename),
engines = this._engines,
saveHtml = this._saveHtml,
coverageEngines = this._coverageEngines,
its = [];

Object.keys(references).forEach(function (name) {
Expand All @@ -39,9 +43,11 @@ module.exports = require('enb/lib/build-flow').create()
describe: path.basename(nodePath) + ' (' + path.dirname(nodePath) + ')',
its: its,
engines: engines.map(function (engine) {
var target = _.contains(coverageEngines, engine.name) ?
instrumentedTarget(engine.target) : engine.target;
return {
name: engine.name,
target: node.unmaskTargetName(engine.target),
target: node.unmaskTargetName(target),
exportName: engine.exportName
};
}),
Expand Down

0 comments on commit 640985c

Please sign in to comment.