blob: b81930e357f4c2f1763d784c5b0eb572ee381397 [file] [log] [blame]
/*
Copyright 2012-2015, Yahoo Inc.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
fs = require('fs'),
mkdirp = require('make-dir'),
compareVersions = require('compare-versions'),
matcherFor = require('./file-matcher').matcherFor,
libInstrument = require('istanbul-lib-instrument'),
libCoverage = require('istanbul-lib-coverage'),
libSourceMaps = require('istanbul-lib-source-maps'),
hook = require('istanbul-lib-hook'),
Reporter = require('./reporter');
function getCoverFunctions(config, includes, callback) {
if (!callback && typeof includes === 'function') {
callback = includes;
includes = null;
}
var includePid = config.instrumentation.includePid(),
reportingDir = path.resolve(config.reporting.dir()),
reporter = new Reporter(config),
excludes = config.instrumentation.excludes(true),
// The coverage variable below should have different value than
// that of the coverage variable actually used by the instrumenter (in this case: __coverage__).
// Otherwise if you run nyc to provide coverage on these files,
// both the actual instrumenter and this file will write to the global coverage variable,
// and provide unexpected coverage result.
coverageVar = '$$coverage$$',
instOpts = config.instrumentation.getInstrumenterOpts(),
sourceMapStore = libSourceMaps.createSourceMapStore({}),
instrumenter,
transformer,
runInContextTransformer,
runInThisContextTransformer,
fakeRequire,
requireTransformer,
reportInitFn,
hookFn,
unhookFn,
coverageFinderFn,
coverageSetterFn,
beforeReportFn,
exitFn;
instOpts.coverageVariable = coverageVar;
instOpts.sourceMapUrlCallback = function(file, url) {
sourceMapStore.registerURL(file, url);
};
coverageFinderFn = function() {
return global[coverageVar];
};
instrumenter = libInstrument.createInstrumenter(instOpts);
transformer = function(code, options) {
var filename = typeof options === 'string' ? options : options.filename;
return instrumenter.instrumentSync(code, filename);
};
runInContextTransformer = function(code, options) {
return transformer(code, options);
};
runInThisContextTransformer = function(code, options) {
return transformer(code, options);
};
requireTransformer = function(code, options) {
var cov,
ret = transformer(code, options),
filename = typeof options === 'string' ? options : options.filename;
if (fakeRequire) {
cov = coverageFinderFn();
cov[filename] = instrumenter.lastFileCoverage();
return 'function x() {}';
}
return ret;
};
coverageSetterFn = function(cov) {
global[coverageVar] = cov;
};
reportInitFn = function() {
// set up reporter
mkdirp.sync(reportingDir); //ensure we fail early if we cannot do this
reporter.addAll(config.reporting.reports());
if (config.reporting.print() !== 'none') {
switch (config.reporting.print()) {
case 'detail':
reporter.add('text');
break;
case 'both':
reporter.add('text');
reporter.add('text-summary');
break;
default:
reporter.add('text-summary');
break;
}
}
};
var disabler;
hookFn = function(matchFn) {
var hookOpts = {
verbose: config.verbose,
extensions: config.instrumentation.extensions(),
coverageVariable: coverageVar
};
//initialize the global variable
coverageSetterFn({});
reportInitFn();
if (config.hooks.hookRunInContext()) {
hook.hookRunInContext(matchFn, runInContextTransformer, hookOpts);
}
if (config.hooks.hookRunInThisContext()) {
hook.hookRunInThisContext(
matchFn,
runInThisContextTransformer,
hookOpts
);
if (compareVersions(process.versions.node, '6.0.0') === -1) {
disabler = hook.hookRequire(
matchFn,
requireTransformer,
hookOpts
);
}
} else {
disabler = hook.hookRequire(matchFn, requireTransformer, hookOpts);
}
};
unhookFn = function(matchFn) {
if (disabler) {
disabler();
}
hook.unhookRunInThisContext();
hook.unhookRunInContext();
hook.unloadRequireCache(matchFn);
};
beforeReportFn = function(matchFn, cov) {
var pidExt = includePid ? '-' + process.pid : '',
file = path.resolve(
reportingDir,
'coverage' + pidExt + '.raw.json'
),
missingFiles,
finalCoverage = cov;
if (config.instrumentation.includeAllSources()) {
if (config.verbose) {
console.error("Including all sources not require'd by tests");
}
missingFiles = [];
// Files that are not touched by code ran by the test runner is manually instrumented, to
// illustrate the missing coverage.
matchFn.files.forEach(function(file) {
if (!cov[file]) {
missingFiles.push(file);
}
});
fakeRequire = true;
missingFiles.forEach(function(file) {
try {
require(file);
} catch (ex) {
console.error('Unable to post-instrument: ' + file);
}
});
}
if (Object.keys(finalCoverage).length > 0) {
if (config.verbose) {
console.error(
'============================================================================='
);
console.error('Writing coverage object [' + file + ']');
console.error(
'Writing coverage reports at [' + reportingDir + ']'
);
console.error(
'============================================================================='
);
}
fs.writeFileSync(file, JSON.stringify(finalCoverage), 'utf8');
}
return finalCoverage;
};
exitFn = function(matchFn, reporterOpts) {
var cov, coverageMap, transformed;
cov = coverageFinderFn() || {};
cov = beforeReportFn(matchFn, cov);
coverageSetterFn(cov);
if (
!(cov && typeof cov === 'object') ||
Object.keys(cov).length === 0
) {
console.error(
'No coverage information was collected, exit without writing coverage information'
);
return;
}
coverageMap = libCoverage.createCoverageMap(cov);
transformed = sourceMapStore.transformCoverage(coverageMap);
reporterOpts.sourceFinder = transformed.sourceFinder;
reporter.write(transformed.map, reporterOpts);
sourceMapStore.dispose();
};
excludes.push(
path.relative(process.cwd(), path.join(reportingDir, '**', '*'))
);
includes =
includes ||
config.instrumentation.extensions().map(function(ext) {
return '**/*' + ext;
});
var matchConfig = {
root:
config.instrumentation.root() ||
/* istanbul ignore next: untestable */ process.cwd(),
includes: includes,
excludes: excludes
};
matcherFor(matchConfig, function(err, matchFn) {
/* istanbul ignore if: untestable */
if (err) {
return callback(err);
}
return callback(null, {
coverageFn: coverageFinderFn,
hookFn: hookFn.bind(null, matchFn),
exitFn: exitFn.bind(null, matchFn, {}), // XXX: reporter opts
unhookFn: unhookFn.bind(null, matchFn)
});
});
}
module.exports = {
getCoverFunctions: getCoverFunctions
};