/* Copyright 2012-2015, Yahoo Inc. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ const path = require('path'); const fs = require('fs'); const mkdirp = require('make-dir'); const compareVersions = require('compare-versions'); const libInstrument = require('istanbul-lib-instrument'); const libCoverage = require('istanbul-lib-coverage'); const libSourceMaps = require('istanbul-lib-source-maps'); const hook = require('istanbul-lib-hook'); const matcherFor = require('./file-matcher').matcherFor; const Reporter = require('./reporter'); function getCoverFunctions(config, includes, callback) { if (!callback && typeof includes === 'function') { callback = includes; includes = null; } const includePid = config.instrumentation.includePid(); const reportingDir = path.resolve(config.reporting.dir()); const reporter = new Reporter(config); const 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. const coverageVar = '$$coverage$$'; const instOpts = config.instrumentation.getInstrumenterOpts(); const sourceMapStore = libSourceMaps.createSourceMapStore({}); let fakeRequire; instOpts.coverageVariable = coverageVar; instOpts.sourceMapUrlCallback = function(file, url) { sourceMapStore.registerURL(file, url); }; const coverageFinderFn = function() { return global[coverageVar]; }; const instrumenter = libInstrument.createInstrumenter(instOpts); const transformer = function(code, options) { const filename = typeof options === 'string' ? options : options.filename; return instrumenter.instrumentSync(code, filename); }; const runInContextTransformer = function(code, options) { return transformer(code, options); }; const runInThisContextTransformer = function(code, options) { return transformer(code, options); }; const requireTransformer = function(code, options) { let cov; const ret = transformer(code, options); const filename = typeof options === 'string' ? options : options.filename; if (fakeRequire) { cov = coverageFinderFn(); cov[filename] = instrumenter.lastFileCoverage(); return 'function x() {}'; } return ret; }; const coverageSetterFn = function(cov) { global[coverageVar] = cov; }; const 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; } } }; let disabler; const hookFn = function(matchFn) { const 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); } }; const unhookFn = function(matchFn) { if (disabler) { disabler(); } hook.unhookRunInThisContext(); hook.unhookRunInContext(); hook.unloadRequireCache(matchFn); }; const beforeReportFn = function(matchFn, cov) { const pidExt = includePid ? '-' + process.pid : ''; const file = path.resolve( reportingDir, 'coverage' + pidExt + '.raw.json' ); let missingFiles; const 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(file => { if (!cov[file]) { missingFiles.push(file); } }); fakeRequire = true; missingFiles.forEach(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; }; const exitFn = function(matchFn, reporterOpts) { let cov; 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; } const coverageMap = libCoverage.createCoverageMap(cov); const 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(ext => '**/*' + ext); const matchConfig = { root: config.instrumentation.root() || /* istanbul ignore next: untestable */ process.cwd(), includes, excludes }; matcherFor(matchConfig, (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 };