| /* | |
| 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; | |