/* Copyright (c) 2012, Yahoo! Inc. All rights reserved. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ var path = require('path'), mkdirp = require('mkdirp'), once = require('once'), async = require('async'), fs = require('fs'), filesFor = require('../util/file-matcher').filesFor, nopt = require('nopt'), Instrumenter = require('../instrumenter'), inputError = require('../util/input-error'), formatOption = require('../util/help-formatter').formatOption, util = require('util'), Command = require('./index'), Collector = require('../collector'), configuration = require('../config'), verbose; /* * Chunk file size to use when reading non JavaScript files in memory * and copying them over when using complete-copy flag. */ var READ_FILE_CHUNK_SIZE = 64 * 1024; function BaselineCollector(instrumenter) { this.instrumenter = instrumenter; this.collector = new Collector(); this.instrument = instrumenter.instrument.bind(this.instrumenter); var origInstrumentSync = instrumenter.instrumentSync; this.instrumentSync = function () { var args = Array.prototype.slice.call(arguments), ret = origInstrumentSync.apply(this.instrumenter, args), baseline = this.instrumenter.lastFileCoverage(), coverage = {}; coverage[baseline.path] = baseline; this.collector.add(coverage); return ret; }; //monkey patch the instrumenter to call our version instead instrumenter.instrumentSync = this.instrumentSync.bind(this); } BaselineCollector.prototype = { getCoverage: function () { return this.collector.getFinalCoverage(); } }; function processFiles(instrumenter, inputDir, outputDir, relativeNames, extensions) { var processor = function (name, callback) { var inputFile = path.resolve(inputDir, name), outputFile = path.resolve(outputDir, name), inputFileExtenstion = path.extname(inputFile), isJavaScriptFile = extensions.indexOf(inputFileExtenstion) > -1, oDir = path.dirname(outputFile), readStream, writeStream; callback = once(callback); mkdirp.sync(oDir); if (fs.statSync(inputFile).isDirectory()) { return callback(null, name); } if (isJavaScriptFile) { fs.readFile(inputFile, 'utf8', function (err, data) { if (err) { return callback(err, name); } instrumenter.instrument(data, inputFile, function (iErr, instrumented) { if (iErr) { return callback(iErr, name); } fs.writeFile(outputFile, instrumented, 'utf8', function (err) { return 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', function() { callback(null, name); }); } }, q = async.queue(processor, 10), errors = [], count = 0, startTime = new Date().getTime(); q.push(relativeNames, function (err, name) { var inputFile, outputFile; if (err) { errors.push({ file: name, error: err.message || err.toString() }); inputFile = path.resolve(inputDir, name); outputFile = path.resolve(outputDir, name); fs.writeFileSync(outputFile, fs.readFileSync(inputFile)); } if (verbose) { console.log('Processed: ' + name); } else { if (count % 100 === 0) { process.stdout.write('.'); } } count += 1; }); q.drain = function () { var endTime = new Date().getTime(); console.log('\nProcessed [' + count + '] files in ' + Math.floor((endTime - startTime) / 1000) + ' secs'); if (errors.length > 0) { console.log('The following ' + errors.length + ' file(s) had errors and were copied as-is'); console.log(errors); } }; } function InstrumentCommand() { Command.call(this); } InstrumentCommand.TYPE = 'instrument'; util.inherits(InstrumentCommand, Command); Command.mix(InstrumentCommand, { synopsis: function synopsis() { return "instruments a file or a directory tree and writes the instrumented code to the desired output location"; }, usage: function () { console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' \n\nOptions are:\n\n' + [ formatOption('--config ', 'the configuration file to use, defaults to .istanbul.yml'), formatOption('--output ', 'The output file or directory. This is required when the input is a directory, ' + 'defaults to standard output when input is a file'), formatOption('-x [-x ]', 'one or more glob patterns (e.g. "**/vendor/**" to ignore all files ' + 'under a vendor directory). Also see the --default-excludes option'), formatOption('--variable ', 'change the variable name of the global coverage variable from the ' + 'default value of `__coverage__` to something else'), formatOption('--embed-source', 'embed source code into the coverage object, defaults to false'), formatOption('--[no-]compact', 'produce [non]compact output, defaults to compact'), formatOption('--[no-]preserve-comments', 'remove / preserve comments in the output, defaults to false'), formatOption('--[no-]complete-copy', 'also copy non-javascript files to the ouput directory as is, defaults to false'), formatOption('--save-baseline', 'produce a baseline coverage.json file out of all files instrumented'), formatOption('--baseline-file ', 'filename of baseline file, defaults to coverage/coverage-baseline.json'), formatOption('--es-modules', 'source code uses es import/export module syntax') ].join('\n\n') + '\n'); console.error('\n'); }, run: function (args, callback) { var template = { config: path, output: path, x: [Array, String], variable: String, compact: Boolean, 'complete-copy': Boolean, verbose: Boolean, 'save-baseline': Boolean, 'baseline-file': path, 'embed-source': Boolean, 'preserve-comments': Boolean, 'es-modules': Boolean }, opts = nopt(template, { v : '--verbose' }, args, 0), overrides = { verbose: opts.verbose, instrumentation: { variable: opts.variable, compact: opts.compact, 'embed-source': opts['embed-source'], 'preserve-comments': opts['preserve-comments'], excludes: opts.x, 'complete-copy': opts['complete-copy'], 'save-baseline': opts['save-baseline'], 'baseline-file': opts['baseline-file'], 'es-modules': opts['es-modules'] } }, config = configuration.loadFile(opts.config, overrides), iOpts = config.instrumentation, cmdArgs = opts.argv.remain, file, stats, stream, includes, instrumenter, needBaseline = iOpts.saveBaseline(), baselineFile = path.resolve(iOpts.baselineFile()), output = opts.output; verbose = config.verbose; if (cmdArgs.length !== 1) { return callback(inputError.create('Need exactly one filename/ dirname argument for the instrument command!')); } if (iOpts.completeCopy()) { includes = ['**/*']; } else { includes = iOpts.extensions().map(function(ext) { return '**/*' + ext; }); } instrumenter = new Instrumenter({ coverageVariable: iOpts.variable(), embedSource: iOpts.embedSource(), noCompact: !iOpts.compact(), preserveComments: iOpts.preserveComments(), esModules: iOpts.esModules() }); if (needBaseline) { mkdirp.sync(path.dirname(baselineFile)); instrumenter = new BaselineCollector(instrumenter); process.on('exit', function () { console.log('Saving baseline coverage at: ' + baselineFile); fs.writeFileSync(baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8'); }); } file = path.resolve(cmdArgs[0]); stats = fs.statSync(file); if (stats.isDirectory()) { if (!output) { return callback(inputError.create('Need an output directory [-o ] 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: includes, excludes: opts.x || iOpts.excludes(false), // backwards-compat, *sigh* relative: true }, function (err, files) { if (err) { return callback(err); } processFiles(instrumenter, file, output, files, iOpts.extensions()); }); } 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(); } } } }); module.exports = InstrumentCommand;