/* 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 once = require('once'); const async = require('async'); const libInstrument = require('istanbul-lib-instrument'); const libCoverage = require('istanbul-lib-coverage'); const filesFor = require('./file-matcher').filesFor; const inputError = require('./input-error'); /* * Chunk file size to use when reading non JavaScript files in memory * and copying them over when using complete-copy flag. */ const READ_FILE_CHUNK_SIZE = 64 * 1024; function BaselineCollector(instrumenter) { this.instrumenter = instrumenter; this.map = libCoverage.createCoverageMap(); this.instrument = instrumenter.instrument.bind(this.instrumenter); const origInstrumentSync = instrumenter.instrumentSync; this.instrumentSync = function(...args) { const ret = origInstrumentSync.apply(this.instrumenter, args); const baseline = this.instrumenter.lastFileCoverage(); this.map.addFileCoverage(baseline); return ret; }; //monkey patch the instrumenter to call our version instead instrumenter.instrumentSync = this.instrumentSync.bind(this); } BaselineCollector.prototype.getCoverage = function() { return this.map.toJSON(); }; function processFiles(instrumenter, opts, callback) { const inputDir = opts.inputDir; const outputDir = opts.outputDir; const relativeNames = opts.names; const extensions = opts.extensions; const verbose = opts.verbose; const processor = function(name, callback) { const inputFile = path.resolve(inputDir, name); const outputFile = path.resolve(outputDir, name); const inputFileExtension = path.extname(inputFile); const isJavaScriptFile = extensions.indexOf(inputFileExtension) > -1; const oDir = path.dirname(outputFile); let readStream; let writeStream; callback = once(callback); mkdirp.sync(oDir); /* istanbul ignore if */ if (fs.statSync(inputFile).isDirectory()) { return callback(null, name); } if (isJavaScriptFile) { fs.readFile(inputFile, 'utf8', (err, data) => { /* istanbul ignore if */ if (err) { return callback(err, name); } instrumenter.instrument( data, inputFile, (iErr, instrumented) => { if (iErr) { return callback(iErr, name); } fs.writeFile(outputFile, instrumented, 'utf8', err => callback(err, name) ); } ); }); } else { // non JavaScript file, copy it as is readStream = fs.createReadStream(inputFile, { bufferSize: READ_FILE_CHUNK_SIZE }); writeStream = fs.createWriteStream(outputFile); readStream.on('error', callback); writeStream.on('error', callback); readStream.pipe(writeStream); readStream.on('end', () => { callback(null, name); }); } }; const q = async.queue(processor, 10); const errors = []; let count = 0; const startTime = new Date().getTime(); q.push(relativeNames, (err, name) => { let inputFile; let outputFile; if (err) { errors.push({ file: name, error: err.message || /* istanbul ignore next */ err.toString() }); inputFile = path.resolve(inputDir, name); outputFile = path.resolve(outputDir, name); fs.writeFileSync(outputFile, fs.readFileSync(inputFile)); } if (verbose) { console.error('Processed: ' + name); } else { if (count % 100 === 0) { process.stdout.write('.'); } } count += 1; }); q.drain = function() { const endTime = new Date().getTime(); console.error( '\nProcessed [' + count + '] files in ' + Math.floor((endTime - startTime) / 1000) + ' secs' ); if (errors.length > 0) { console.error( 'The following ' + errors.length + ' file(s) had errors and were copied as-is' ); console.error(errors); } return callback(); }; } function run(config, opts, callback) { opts = opts || {}; const iOpts = config.instrumentation; const input = opts.input; const output = opts.output; const excludes = opts.excludes; let stream; let includes; let instrumenter; const origCallback = callback; const needBaseline = iOpts.saveBaseline(); const baselineFile = path.resolve(iOpts.baselineFile()); if (iOpts.completeCopy()) { includes = ['**/*']; } else { includes = iOpts.extensions().map(ext => '**/*' + ext); } if (!input) { return callback(new Error('No input specified')); } instrumenter = libInstrument.createInstrumenter( iOpts.getInstrumenterOpts() ); if (needBaseline) { mkdirp.sync(path.dirname(baselineFile)); instrumenter = new BaselineCollector(instrumenter); callback = function(err) { /* istanbul ignore else */ if (!err) { console.error('Saving baseline coverage at ' + baselineFile); fs.writeFileSync( baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8' ); } return origCallback(err); }; } const file = path.resolve(input); const stats = fs.statSync(file); if (stats.isDirectory()) { if (!output) { return callback( inputError.create( 'Need an output directory when input is a directory!' ) ); } if (output === file) { return callback( inputError.create( 'Cannot instrument into the same directory/ file as input!' ) ); } mkdirp.sync(output); filesFor( { root: file, includes, excludes: excludes || iOpts.excludes(false), relative: true }, (err, files) => { /* istanbul ignore if */ if (err) { return callback(err); } processFiles( instrumenter, { inputDir: file, outputDir: output, names: files, extensions: iOpts.extensions(), verbose: config.verbose }, callback ); } ); } else { if (output) { stream = fs.createWriteStream(output); } else { stream = process.stdout; } stream.write( instrumenter.instrumentSync(fs.readFileSync(file, 'utf8'), file) ); if (stream !== process.stdout) { stream.end(); } return callback(); } } module.exports = { run };