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 10, 2014
1 parent 9d7a952 commit a4dbf9e
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 18 deletions.
85 changes: 85 additions & 0 deletions lib/coverage-object.js
@@ -0,0 +1,85 @@
var _ = require('lodash');

function CoverageObject() {
this._indices = {
};
this.coverage = {
};
}

CoverageObject.prototype = {
constructor: CoverageObject,

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');
}

module.exports = CoverageObject;
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
18 changes: 16 additions & 2 deletions lib/plugin.js
Expand Up @@ -58,6 +58,19 @@ module.exports = function (helper) {
};
}),
sourceLevels: options.sourceLevels || options.levels,
coverage: _.defaults(options.coverage || {}, {
engines: [],
hasSourceMap: false,
reportDirectory: 'tmpl-coverage',
exclude: [
'**/node_modules/**',
'**/libs/**'
],
reporters: process.env.BEM_TMPL_SPECS_COV_REPORTERS ?
process.env.BEM_TMPL_SPECS_COV_REPORTERS.split(',')
: ['html']
}),

referenceDirSuffixes: options.referenceDirSuffixes || ['tmpl-specs'],
saveHtml: (typeof process.env.BEM_TMPL_SPECS_SAVE_HTML === 'undefined' ?
options.saveHtml :
Expand Down Expand Up @@ -106,7 +119,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 +156,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
57 changes: 54 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,41 @@ function getReporters() {

return reporters;
}

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

if (!coverageOpts.hasSourceMap) {
return filterAndSaveCoverage(coverage, coverageOpts);
}

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
63 changes: 63 additions & 0 deletions lib/unmap-coverage.js
@@ -0,0 +1,63 @@
'use strict';
var vow = require('vow'),
vowFs = require('vow-fs'),
_ = require('lodash'),
istanbul = require('istanbul'),
SourceLocator = require('enb-source-map/lib/source-locator'),

CoverageObject = require('./coverage-object');

function unmapCoverageObject(sourceObject) {
return vow.all(Object.keys(sourceObject).map(function (fileName) {
return vowFs.read(fileName, 'utf8').then(function (source) {
return unmapFile(fileName, sourceObject[fileName], source);
});
}))
.then(function (coverageObjects) {
return coverageObjects.reduce(istanbul.utils.mergeFileCoverage);
})
.then(function (totalCoverage) {
return _.omit(totalCoverage, function (value, fileName) {
return fileName in sourceObject;
});
});
}

function unmapFile(fileName, fileObject, source) {
var locator = new SourceLocator(fileName, source),
result = new CoverageObject();

Object.keys(fileObject.statementMap).forEach(function (key) {
var statement = fileObject.statementMap[key],
originalStart = locator.locate(statement.start.line, statement.start.column),
originalEnd = locator.locate(statement.end.line, statement.end.column);
result.addStatement(originalStart, originalEnd, fileObject.s[key]);
});

Object.keys(fileObject.fnMap).forEach(function (key) {
var fn = fileObject.fnMap[key],
loc = fn.loc,
originalStart = locator.locate(loc.start.line, loc.start.column),
originalEnd = locator.locate(loc.end.line, loc.end.column);
result.addFunction(fn.name, originalStart, originalEnd, fileObject.f[key]);
});

Object.keys(fileObject.branchMap).forEach(function (key) {
var branch = fileObject.branchMap[key],
locations = branch.locations.map(function (loc) {
return {
start: locator.locate(loc.start.line, loc.start.column),
end: locator.locate(loc.end.line, loc.end.column)
};
});
result.addBranch(
branch.type,
locations,
fileObject.b[key]
);
});

return result.coverage;
}

module.exports = unmapCoverageObject;
3 changes: 3 additions & 0 deletions lib/util.js
@@ -0,0 +1,3 @@
exports.instrumentedTarget = function instrumentedTarget(target) {
return target + '.instr.js';
};

0 comments on commit a4dbf9e

Please sign in to comment.