run-cover.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /*
  2. Copyright 2012-2015, Yahoo Inc.
  3. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
  4. */
  5. const path = require('path');
  6. const fs = require('fs');
  7. const mkdirp = require('make-dir');
  8. const compareVersions = require('compare-versions');
  9. const libInstrument = require('istanbul-lib-instrument');
  10. const libCoverage = require('istanbul-lib-coverage');
  11. const libSourceMaps = require('istanbul-lib-source-maps');
  12. const hook = require('istanbul-lib-hook');
  13. const matcherFor = require('./file-matcher').matcherFor;
  14. const Reporter = require('./reporter');
  15. function getCoverFunctions(config, includes, callback) {
  16. if (!callback && typeof includes === 'function') {
  17. callback = includes;
  18. includes = null;
  19. }
  20. const includePid = config.instrumentation.includePid();
  21. const reportingDir = path.resolve(config.reporting.dir());
  22. const reporter = new Reporter(config);
  23. const excludes = config.instrumentation.excludes(true);
  24. // The coverage variable below should have different value than
  25. // that of the coverage variable actually used by the instrumenter (in this case: __coverage__).
  26. // Otherwise if you run nyc to provide coverage on these files,
  27. // both the actual instrumenter and this file will write to the global coverage variable,
  28. // and provide unexpected coverage result.
  29. const coverageVar = '$$coverage$$';
  30. const instOpts = config.instrumentation.getInstrumenterOpts();
  31. const sourceMapStore = libSourceMaps.createSourceMapStore({});
  32. let fakeRequire;
  33. instOpts.coverageVariable = coverageVar;
  34. instOpts.sourceMapUrlCallback = function(file, url) {
  35. sourceMapStore.registerURL(file, url);
  36. };
  37. const coverageFinderFn = function() {
  38. return global[coverageVar];
  39. };
  40. const instrumenter = libInstrument.createInstrumenter(instOpts);
  41. const transformer = function(code, options) {
  42. const filename =
  43. typeof options === 'string' ? options : options.filename;
  44. return instrumenter.instrumentSync(code, filename);
  45. };
  46. const runInContextTransformer = function(code, options) {
  47. return transformer(code, options);
  48. };
  49. const runInThisContextTransformer = function(code, options) {
  50. return transformer(code, options);
  51. };
  52. const requireTransformer = function(code, options) {
  53. let cov;
  54. const ret = transformer(code, options);
  55. const filename =
  56. typeof options === 'string' ? options : options.filename;
  57. if (fakeRequire) {
  58. cov = coverageFinderFn();
  59. cov[filename] = instrumenter.lastFileCoverage();
  60. return 'function x() {}';
  61. }
  62. return ret;
  63. };
  64. const coverageSetterFn = function(cov) {
  65. global[coverageVar] = cov;
  66. };
  67. const reportInitFn = function() {
  68. // set up reporter
  69. mkdirp.sync(reportingDir); //ensure we fail early if we cannot do this
  70. reporter.addAll(config.reporting.reports());
  71. if (config.reporting.print() !== 'none') {
  72. switch (config.reporting.print()) {
  73. case 'detail':
  74. reporter.add('text');
  75. break;
  76. case 'both':
  77. reporter.add('text');
  78. reporter.add('text-summary');
  79. break;
  80. default:
  81. reporter.add('text-summary');
  82. break;
  83. }
  84. }
  85. };
  86. let disabler;
  87. const hookFn = function(matchFn) {
  88. const hookOpts = {
  89. verbose: config.verbose,
  90. extensions: config.instrumentation.extensions(),
  91. coverageVariable: coverageVar
  92. };
  93. //initialize the global variable
  94. coverageSetterFn({});
  95. reportInitFn();
  96. if (config.hooks.hookRunInContext()) {
  97. hook.hookRunInContext(matchFn, runInContextTransformer, hookOpts);
  98. }
  99. if (config.hooks.hookRunInThisContext()) {
  100. hook.hookRunInThisContext(
  101. matchFn,
  102. runInThisContextTransformer,
  103. hookOpts
  104. );
  105. if (compareVersions(process.versions.node, '6.0.0') === -1) {
  106. disabler = hook.hookRequire(
  107. matchFn,
  108. requireTransformer,
  109. hookOpts
  110. );
  111. }
  112. } else {
  113. disabler = hook.hookRequire(matchFn, requireTransformer, hookOpts);
  114. }
  115. };
  116. const unhookFn = function(matchFn) {
  117. if (disabler) {
  118. disabler();
  119. }
  120. hook.unhookRunInThisContext();
  121. hook.unhookRunInContext();
  122. hook.unloadRequireCache(matchFn);
  123. };
  124. const beforeReportFn = function(matchFn, cov) {
  125. const pidExt = includePid ? '-' + process.pid : '';
  126. const file = path.resolve(
  127. reportingDir,
  128. 'coverage' + pidExt + '.raw.json'
  129. );
  130. let missingFiles;
  131. const finalCoverage = cov;
  132. if (config.instrumentation.includeAllSources()) {
  133. if (config.verbose) {
  134. console.error("Including all sources not require'd by tests");
  135. }
  136. missingFiles = [];
  137. // Files that are not touched by code ran by the test runner is manually instrumented, to
  138. // illustrate the missing coverage.
  139. matchFn.files.forEach(file => {
  140. if (!cov[file]) {
  141. missingFiles.push(file);
  142. }
  143. });
  144. fakeRequire = true;
  145. missingFiles.forEach(file => {
  146. try {
  147. require(file);
  148. } catch (ex) {
  149. console.error('Unable to post-instrument: ' + file);
  150. }
  151. });
  152. }
  153. if (Object.keys(finalCoverage).length > 0) {
  154. if (config.verbose) {
  155. console.error(
  156. '============================================================================='
  157. );
  158. console.error('Writing coverage object [' + file + ']');
  159. console.error(
  160. 'Writing coverage reports at [' + reportingDir + ']'
  161. );
  162. console.error(
  163. '============================================================================='
  164. );
  165. }
  166. fs.writeFileSync(file, JSON.stringify(finalCoverage), 'utf8');
  167. }
  168. return finalCoverage;
  169. };
  170. const exitFn = function(matchFn, reporterOpts) {
  171. let cov;
  172. cov = coverageFinderFn() || {};
  173. cov = beforeReportFn(matchFn, cov);
  174. coverageSetterFn(cov);
  175. if (
  176. !(cov && typeof cov === 'object') ||
  177. Object.keys(cov).length === 0
  178. ) {
  179. console.error(
  180. 'No coverage information was collected, exit without writing coverage information'
  181. );
  182. return;
  183. }
  184. const coverageMap = libCoverage.createCoverageMap(cov);
  185. const transformed = sourceMapStore.transformCoverage(coverageMap);
  186. reporterOpts.sourceFinder = transformed.sourceFinder;
  187. reporter.write(transformed.map, reporterOpts);
  188. sourceMapStore.dispose();
  189. };
  190. excludes.push(
  191. path.relative(process.cwd(), path.join(reportingDir, '**', '*'))
  192. );
  193. includes =
  194. includes ||
  195. config.instrumentation.extensions().map(ext => '**/*' + ext);
  196. const matchConfig = {
  197. root:
  198. config.instrumentation.root() ||
  199. /* istanbul ignore next: untestable */ process.cwd(),
  200. includes,
  201. excludes
  202. };
  203. matcherFor(matchConfig, (err, matchFn) => {
  204. /* istanbul ignore if: untestable */
  205. if (err) {
  206. return callback(err);
  207. }
  208. return callback(null, {
  209. coverageFn: coverageFinderFn,
  210. hookFn: hookFn.bind(null, matchFn),
  211. exitFn: exitFn.bind(null, matchFn, {}), // XXX: reporter opts
  212. unhookFn: unhookFn.bind(null, matchFn)
  213. });
  214. });
  215. }
  216. module.exports = {
  217. getCoverFunctions
  218. };