2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
6 var path = require('path'),
7 mkdirp = require('mkdirp'),
8 once = require('once'),
9 async = require('async'),
11 filesFor = require('../util/file-matcher').filesFor,
12 nopt = require('nopt'),
13 Instrumenter = require('../instrumenter'),
14 inputError = require('../util/input-error'),
15 formatOption = require('../util/help-formatter').formatOption,
16 util = require('util'),
17 Command = require('./index'),
18 Collector = require('../collector'),
19 configuration = require('../config'),
24 * Chunk file size to use when reading non JavaScript files in memory
25 * and copying them over when using complete-copy flag.
27 var READ_FILE_CHUNK_SIZE = 64 * 1024;
29 function BaselineCollector(instrumenter) {
30 this.instrumenter = instrumenter;
31 this.collector = new Collector();
32 this.instrument = instrumenter.instrument.bind(this.instrumenter);
34 var origInstrumentSync = instrumenter.instrumentSync;
35 this.instrumentSync = function () {
36 var args = Array.prototype.slice.call(arguments),
37 ret = origInstrumentSync.apply(this.instrumenter, args),
38 baseline = this.instrumenter.lastFileCoverage(),
40 coverage[baseline.path] = baseline;
41 this.collector.add(coverage);
44 //monkey patch the instrumenter to call our version instead
45 instrumenter.instrumentSync = this.instrumentSync.bind(this);
48 BaselineCollector.prototype = {
49 getCoverage: function () {
50 return this.collector.getFinalCoverage();
55 function processFiles(instrumenter, inputDir, outputDir, relativeNames, extensions) {
56 var processor = function (name, callback) {
57 var inputFile = path.resolve(inputDir, name),
58 outputFile = path.resolve(outputDir, name),
59 inputFileExtenstion = path.extname(inputFile),
60 isJavaScriptFile = extensions.indexOf(inputFileExtenstion) > -1,
61 oDir = path.dirname(outputFile),
62 readStream, writeStream;
64 callback = once(callback);
67 if (fs.statSync(inputFile).isDirectory()) {
68 return callback(null, name);
71 if (isJavaScriptFile) {
72 fs.readFile(inputFile, 'utf8', function (err, data) {
73 if (err) { return callback(err, name); }
74 instrumenter.instrument(data, inputFile, function (iErr, instrumented) {
75 if (iErr) { return callback(iErr, name); }
76 fs.writeFile(outputFile, instrumented, 'utf8', function (err) {
77 return callback(err, name);
83 // non JavaScript file, copy it as is
84 readStream = fs.createReadStream(inputFile, {'bufferSize': READ_FILE_CHUNK_SIZE});
85 writeStream = fs.createWriteStream(outputFile);
87 readStream.on('error', callback);
88 writeStream.on('error', callback);
90 readStream.pipe(writeStream);
91 readStream.on('end', function() {
96 q = async.queue(processor, 10),
99 startTime = new Date().getTime();
101 q.push(relativeNames, function (err, name) {
102 var inputFile, outputFile;
104 errors.push({ file: name, error: err.message || err.toString() });
105 inputFile = path.resolve(inputDir, name);
106 outputFile = path.resolve(outputDir, name);
107 fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
110 console.log('Processed: ' + name);
112 if (count % 100 === 0) { process.stdout.write('.'); }
117 q.drain = function () {
118 var endTime = new Date().getTime();
119 console.log('\nProcessed [' + count + '] files in ' + Math.floor((endTime - startTime) / 1000) + ' secs');
120 if (errors.length > 0) {
121 console.log('The following ' + errors.length + ' file(s) had errors and were copied as-is');
128 function InstrumentCommand() {
132 InstrumentCommand.TYPE = 'instrument';
133 util.inherits(InstrumentCommand, Command);
135 Command.mix(InstrumentCommand, {
136 synopsis: function synopsis() {
137 return "instruments a file or a directory tree and writes the instrumented code to the desired output location";
141 console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> <file-or-directory>\n\nOptions are:\n\n' +
143 formatOption('--config <path-to-config>', 'the configuration file to use, defaults to .istanbul.yml'),
144 formatOption('--output <file-or-dir>', 'The output file or directory. This is required when the input is a directory, ' +
145 'defaults to standard output when input is a file'),
146 formatOption('-x <exclude-pattern> [-x <exclude-pattern>]', 'one or more fileset patterns (e.g. "**/vendor/**" to ignore all files ' +
147 'under a vendor directory). Also see the --default-excludes option'),
148 formatOption('--variable <global-coverage-variable-name>', 'change the variable name of the global coverage variable from the ' +
149 'default value of `__coverage__` to something else'),
150 formatOption('--embed-source', 'embed source code into the coverage object, defaults to false'),
151 formatOption('--[no-]compact', 'produce [non]compact output, defaults to compact'),
152 formatOption('--[no-]preserve-comments', 'remove / preserve comments in the output, defaults to false'),
153 formatOption('--[no-]complete-copy', 'also copy non-javascript files to the ouput directory as is, defaults to false'),
154 formatOption('--save-baseline', 'produce a baseline coverage.json file out of all files instrumented'),
155 formatOption('--baseline-file <file>', 'filename of baseline file, defaults to coverage/coverage-baseline.json')
156 ].join('\n\n') + '\n');
160 run: function (args, callback) {
168 'complete-copy': Boolean,
170 'save-baseline': Boolean,
171 'baseline-file': path,
172 'embed-source': Boolean,
173 'preserve-comments': Boolean
175 opts = nopt(template, { v : '--verbose' }, args, 0),
177 verbose: opts.verbose,
179 variable: opts.variable,
180 compact: opts.compact,
181 'embed-source': opts['embed-source'],
182 'preserve-comments': opts['preserve-comments'],
184 'complete-copy': opts['complete-copy'],
185 'save-baseline': opts['save-baseline'],
186 'baseline-file': opts['baseline-file']
189 config = configuration.loadFile(opts.config, overrides),
190 iOpts = config.instrumentation,
191 cmdArgs = opts.argv.remain,
197 needBaseline = iOpts.saveBaseline(),
198 baselineFile = path.resolve(iOpts.baselineFile()),
199 output = opts.output;
201 verbose = config.verbose;
202 if (cmdArgs.length !== 1) {
203 return callback(inputError.create('Need exactly one filename/ dirname argument for the instrument command!'));
206 if (iOpts.completeCopy()) {
210 includes = iOpts.extensions().map(function(ext) {
215 instrumenter = new Instrumenter({
216 coverageVariable: iOpts.variable(),
217 embedSource: iOpts.embedSource(),
218 noCompact: !iOpts.compact(),
219 preserveComments: iOpts.preserveComments()
223 mkdirp.sync(path.dirname(baselineFile));
224 instrumenter = new BaselineCollector(instrumenter);
225 process.on('exit', function () {
226 console.log('Saving baseline coverage at: ' + baselineFile);
227 fs.writeFileSync(baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8');
231 file = path.resolve(cmdArgs[0]);
232 stats = fs.statSync(file);
233 if (stats.isDirectory()) {
234 if (!output) { return callback(inputError.create('Need an output directory [-o <dir>] when input is a directory!')); }
235 if (output === file) { return callback(inputError.create('Cannot instrument into the same directory/ file as input!')); }
240 excludes: opts.x || iOpts.excludes(false), // backwards-compat, *sigh*
242 }, function (err, files) {
243 if (err) { return callback(err); }
244 processFiles(instrumenter, file, output, files, iOpts.extensions());
248 stream = fs.createWriteStream(output);
250 stream = process.stdout;
252 stream.write(instrumenter.instrumentSync(fs.readFileSync(file, 'utf8'), file));
253 if (stream !== process.stdout) {
260 module.exports = InstrumentCommand;