266 lines
10 KiB
JavaScript
266 lines
10 KiB
JavaScript
|
/*
|
||
|
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;
|
||
|
|