run-cover.js 7.2 KB

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