| /* |
| 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() + ' <options> <file-or-directory>\n\nOptions are:\n\n' + |
| [ |
| formatOption('--config <path-to-config>', 'the configuration file to use, defaults to .istanbul.yml'), |
| formatOption('--output <file-or-dir>', '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 <exclude-pattern> [-x <exclude-pattern>]', 'one or more glob patterns (e.g. "**/vendor/**" to ignore all files ' + |
| 'under a vendor directory). Also see the --default-excludes option'), |
| formatOption('--variable <global-coverage-variable-name>', '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 <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 <dir>] 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; |
| |