gulp-istanbul
Version:
Istanbul unit test coverage plugin for gulp.
194 lines (157 loc) • 6 kB
JavaScript
;
var through = require('through2').obj;
var path = require('path');
var checker = require('istanbul-threshold-checker');
// Make sure istanbul is `require`d after the istanbul-threshold-checker to use the istanbul version
// defined in this package.json instead of the one defined in istanbul-threshold-checker.
var istanbul = require('istanbul');
var _ = require('lodash');
var applySourceMap = require('vinyl-sourcemaps-apply');
var Report = istanbul.Report;
var Collector = istanbul.Collector;
var PluginError = require('plugin-error');
var PLUGIN_NAME = 'gulp-istanbul';
var COVERAGE_VARIABLE = '$$cov_' + new Date().getTime() + '$$';
function normalizePathSep(filepath) {
return filepath.replace(/\//g, path.sep);
}
var plugin = module.exports = function (opts) {
opts = opts || {};
_.defaults(opts, {
coverageVariable: COVERAGE_VARIABLE,
instrumenter: istanbul.Instrumenter
});
opts.includeUntested = opts.includeUntested === true;
return through(function (file, enc, cb) {
var fileContents = file.contents.toString();
var fileOpts = _.cloneDeep(opts);
if (file.sourceMap) {
fileOpts = _.defaultsDeep(fileOpts, {
codeGenerationOptions: {
sourceMap: file.sourceMap.file,
sourceMapWithCode: true,
sourceContent: fileContents,
sourceMapRoot: file.sourceMap.sourceRoot,
file: normalizePathSep(file.path)
}
});
}
var instrumenter = new opts.instrumenter(fileOpts);
cb = _.once(cb);
if (!(file.contents instanceof Buffer)) {
return cb(new PluginError(PLUGIN_NAME, 'streams not supported'));
}
var filepath = normalizePathSep(file.path);
instrumenter.instrument(fileContents, filepath, function (err, code) {
if (err) {
return cb(new PluginError(
PLUGIN_NAME,
'Unable to parse ' + filepath + '\n\n' + err.message + '\n'
));
}
var sourceMap = instrumenter.lastSourceMap();
if (sourceMap !== null) {
applySourceMap(file, sourceMap.toString());
}
file.contents = new Buffer(code);
// Parse the blank coverage object from the instrumented file and save it
// to the global coverage variable to enable reporting on non-required
// files, a workaround for
// https://github.com/gotwarlost/istanbul/issues/112
if (opts.includeUntested) {
var instrumentedSrc = file.contents.toString();
var covStubRE = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g;
var covStubMatch = covStubRE.exec(instrumentedSrc);
if (covStubMatch !== null) {
var covStub = JSON.parse(covStubMatch[0]);
global[opts.coverageVariable] = global[opts.coverageVariable] || {};
global[opts.coverageVariable][path.resolve(filepath)] = covStub;
}
}
return cb(err, file);
});
});
};
plugin.hookRequire = function (options) {
var fileMap = {};
istanbul.hook.unhookRequire();
istanbul.hook.hookRequire(function (path) {
return !!fileMap[normalizePathSep(path)];
}, function (code, path) {
return fileMap[normalizePathSep(path)];
}, options);
return through(function (file, enc, cb) {
// If the file is already required, delete it from the cache otherwise the covered
// version will be ignored.
delete require.cache[path.resolve(file.path)];
fileMap[normalizePathSep(file.path)] = file.contents.toString();
return cb();
});
};
plugin.summarizeCoverage = function (opts) {
opts = opts || {};
if (!opts.coverageVariable) opts.coverageVariable = COVERAGE_VARIABLE;
if (!global[opts.coverageVariable]) throw new Error('no coverage data found, run tests before calling `summarizeCoverage`');
var collector = new Collector();
collector.add(global[opts.coverageVariable]);
return istanbul.utils.summarizeCoverage(collector.getFinalCoverage());
};
plugin.writeReports = function (opts) {
if (typeof opts === 'string') opts = { dir: opts };
opts = opts || {};
var defaultDir = path.join(process.cwd(), 'coverage');
opts = _.defaultsDeep(opts, {
coverageVariable: COVERAGE_VARIABLE,
dir: defaultDir,
reportOpts: {
dir: opts.dir || defaultDir
}
});
opts.reporters = opts.reporters || [ 'lcov', 'json', 'text', 'text-summary' ];
var reporters = opts.reporters.map(function(reporter) {
if (reporter.TYPE) Report.register(reporter);
return reporter.TYPE || reporter;
});
var invalid = _.difference(reporters, Report.getReportList());
if (invalid.length) {
// throw before we start -- fail fast
throw new PluginError(PLUGIN_NAME, 'Invalid reporters: ' + invalid.join(', '));
}
reporters = reporters.map(function (r) {
var reportOpts = opts.reportOpts[r] || opts.reportOpts;
return Report.create(r, _.clone(reportOpts));
});
var cover = through();
cover.on('end', function () {
var collector = new Collector();
// Revert to an object if there are no matching source files.
collector.add(global[opts.coverageVariable] || {});
reporters.forEach(function (report) {
report.writeReport(collector, true);
});
}).resume();
return cover;
};
plugin.enforceThresholds = function (opts) {
opts = opts || {};
opts = _.defaults(opts, {
coverageVariable: COVERAGE_VARIABLE
});
var cover = through();
cover.on('end', function () {
var collector = new Collector();
// Revert to an object if there are no macthing source files.
collector.add(global[opts.coverageVariable] || {});
var results = checker.checkFailures(opts.thresholds, collector.getFinalCoverage());
var criteria = function(type) {
return (type.global && type.global.failed) || (type.each && type.each.failed);
};
if (_.some(results, criteria)) {
this.emit('error', new PluginError({
plugin: PLUGIN_NAME,
message: 'Coverage failed'
}));
}
}).resume();
return cover;
};