run-instrument.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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. mkdirp = require('mkdirp'),
  7. once = require('once'),
  8. async = require('async'),
  9. fs = require('fs'),
  10. filesFor = require('./file-matcher').filesFor,
  11. libInstrument = require('istanbul-lib-instrument'),
  12. libCoverage = require('istanbul-lib-coverage'),
  13. inputError = require('./input-error');
  14. /*
  15. * Chunk file size to use when reading non JavaScript files in memory
  16. * and copying them over when using complete-copy flag.
  17. */
  18. var READ_FILE_CHUNK_SIZE = 64 * 1024;
  19. function BaselineCollector(instrumenter) {
  20. this.instrumenter = instrumenter;
  21. this.map = libCoverage.createCoverageMap();
  22. this.instrument = instrumenter.instrument.bind(this.instrumenter);
  23. var origInstrumentSync = instrumenter.instrumentSync;
  24. this.instrumentSync = function () {
  25. var args = Array.prototype.slice.call(arguments),
  26. ret = origInstrumentSync.apply(this.instrumenter, args),
  27. baseline = this.instrumenter.lastFileCoverage();
  28. this.map.addFileCoverage(baseline);
  29. return ret;
  30. };
  31. //monkey patch the instrumenter to call our version instead
  32. instrumenter.instrumentSync = this.instrumentSync.bind(this);
  33. }
  34. BaselineCollector.prototype.getCoverage = function () {
  35. return this.map.toJSON();
  36. };
  37. function processFiles(instrumenter, opts, callback) {
  38. var inputDir = opts.inputDir,
  39. outputDir = opts.outputDir,
  40. relativeNames = opts.names,
  41. extensions = opts.extensions,
  42. verbose = opts.verbose;
  43. var processor = function (name, callback) {
  44. var inputFile = path.resolve(inputDir, name),
  45. outputFile = path.resolve(outputDir, name),
  46. inputFileExtension = path.extname(inputFile),
  47. isJavaScriptFile = extensions.indexOf(inputFileExtension) > -1,
  48. oDir = path.dirname(outputFile),
  49. readStream, writeStream;
  50. callback = once(callback);
  51. mkdirp.sync(oDir);
  52. /* istanbul ignore if */
  53. if (fs.statSync(inputFile).isDirectory()) {
  54. return callback(null, name);
  55. }
  56. if (isJavaScriptFile) {
  57. fs.readFile(inputFile, 'utf8', function (err, data) {
  58. /* istanbul ignore if */ if (err) { return callback(err, name); }
  59. instrumenter.instrument(data, inputFile, function (iErr, instrumented) {
  60. if (iErr) { return callback(iErr, name); }
  61. fs.writeFile(outputFile, instrumented, 'utf8', function (err) {
  62. return callback(err, name);
  63. });
  64. });
  65. });
  66. }
  67. else {
  68. // non JavaScript file, copy it as is
  69. readStream = fs.createReadStream(inputFile, {'bufferSize': READ_FILE_CHUNK_SIZE});
  70. writeStream = fs.createWriteStream(outputFile);
  71. readStream.on('error', callback);
  72. writeStream.on('error', callback);
  73. readStream.pipe(writeStream);
  74. readStream.on('end', function() {
  75. callback(null, name);
  76. });
  77. }
  78. },
  79. q = async.queue(processor, 10),
  80. errors = [],
  81. count = 0,
  82. startTime = new Date().getTime();
  83. q.push(relativeNames, function (err, name) {
  84. var inputFile, outputFile;
  85. if (err) {
  86. errors.push({ file: name, error: err.message || /* istanbul ignore next */ err.toString() });
  87. inputFile = path.resolve(inputDir, name);
  88. outputFile = path.resolve(outputDir, name);
  89. fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
  90. }
  91. if (verbose) {
  92. console.error('Processed: ' + name);
  93. } else {
  94. if (count % 100 === 0) { process.stdout.write('.'); }
  95. }
  96. count += 1;
  97. });
  98. q.drain = function () {
  99. var endTime = new Date().getTime();
  100. console.error('\nProcessed [' + count + '] files in ' + Math.floor((endTime - startTime) / 1000) + ' secs');
  101. if (errors.length > 0) {
  102. console.error('The following ' + errors.length + ' file(s) had errors and were copied as-is');
  103. console.error(errors);
  104. }
  105. return callback();
  106. };
  107. }
  108. function run(config, opts, callback) {
  109. opts = opts || {};
  110. var iOpts = config.instrumentation,
  111. input = opts.input,
  112. output = opts.output,
  113. excludes = opts.excludes,
  114. file,
  115. stats,
  116. stream,
  117. includes,
  118. instrumenter,
  119. origCallback = callback,
  120. needBaseline = iOpts.saveBaseline(),
  121. baselineFile = path.resolve(iOpts.baselineFile());
  122. if (iOpts.completeCopy()) {
  123. includes = ['**/*'];
  124. }
  125. else {
  126. includes = iOpts.extensions().map(function(ext) {
  127. return '**/*' + ext;
  128. });
  129. }
  130. if (!input) {
  131. return callback(new Error('No input specified'));
  132. }
  133. instrumenter = libInstrument.createInstrumenter(iOpts.getInstrumenterOpts());
  134. if (needBaseline) {
  135. mkdirp.sync(path.dirname(baselineFile));
  136. instrumenter = new BaselineCollector(instrumenter);
  137. callback = function (err) {
  138. /* istanbul ignore else */
  139. if (!err) {
  140. console.error('Saving baseline coverage at ' + baselineFile);
  141. fs.writeFileSync(baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8');
  142. }
  143. return origCallback(err);
  144. };
  145. }
  146. file = path.resolve(input);
  147. stats = fs.statSync(file);
  148. if (stats.isDirectory()) {
  149. if (!output) { return callback(inputError.create('Need an output directory when input is a directory!')); }
  150. if (output === file) { return callback(inputError.create('Cannot instrument into the same directory/ file as input!')); }
  151. mkdirp.sync(output);
  152. filesFor({
  153. root: file,
  154. includes: includes,
  155. excludes: excludes || iOpts.excludes(false),
  156. relative: true
  157. }, function (err, files) {
  158. /* istanbul ignore if */
  159. if (err) { return callback(err); }
  160. processFiles(instrumenter, {
  161. inputDir: file,
  162. outputDir: output,
  163. names: files,
  164. extensions: iOpts.extensions(),
  165. verbose: config.verbose
  166. }, callback);
  167. });
  168. } else {
  169. if (output) {
  170. stream = fs.createWriteStream(output);
  171. } else {
  172. stream = process.stdout;
  173. }
  174. stream.write(instrumenter.instrumentSync(fs.readFileSync(file, 'utf8'), file));
  175. if (stream !== process.stdout) {
  176. stream.end();
  177. }
  178. return callback();
  179. }
  180. }
  181. module.exports = {
  182. run: run
  183. };