blob: e66f4a98f48a25129b09819dc72606fc1e968c88 [file] [log] [blame]
/*
Copyright 2012-2015, Yahoo Inc.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
const path = require('path');
const fs = require('fs');
const mkdirp = require('make-dir');
const once = require('once');
const async = require('async');
const libInstrument = require('istanbul-lib-instrument');
const libCoverage = require('istanbul-lib-coverage');
const filesFor = require('./file-matcher').filesFor;
const inputError = require('./input-error');
/*
* Chunk file size to use when reading non JavaScript files in memory
* and copying them over when using complete-copy flag.
*/
const READ_FILE_CHUNK_SIZE = 64 * 1024;
function BaselineCollector(instrumenter) {
this.instrumenter = instrumenter;
this.map = libCoverage.createCoverageMap();
this.instrument = instrumenter.instrument.bind(this.instrumenter);
const origInstrumentSync = instrumenter.instrumentSync;
this.instrumentSync = function(...args) {
const ret = origInstrumentSync.apply(this.instrumenter, args);
const baseline = this.instrumenter.lastFileCoverage();
this.map.addFileCoverage(baseline);
return ret;
};
//monkey patch the instrumenter to call our version instead
instrumenter.instrumentSync = this.instrumentSync.bind(this);
}
BaselineCollector.prototype.getCoverage = function() {
return this.map.toJSON();
};
function processFiles(instrumenter, opts, callback) {
const inputDir = opts.inputDir;
const outputDir = opts.outputDir;
const relativeNames = opts.names;
const extensions = opts.extensions;
const verbose = opts.verbose;
const processor = function(name, callback) {
const inputFile = path.resolve(inputDir, name);
const outputFile = path.resolve(outputDir, name);
const inputFileExtension = path.extname(inputFile);
const isJavaScriptFile = extensions.indexOf(inputFileExtension) > -1;
const oDir = path.dirname(outputFile);
let readStream;
let writeStream;
callback = once(callback);
mkdirp.sync(oDir);
/* istanbul ignore if */
if (fs.statSync(inputFile).isDirectory()) {
return callback(null, name);
}
if (isJavaScriptFile) {
fs.readFile(inputFile, 'utf8', (err, data) => {
/* istanbul ignore if */ if (err) {
return callback(err, name);
}
instrumenter.instrument(
data,
inputFile,
(iErr, instrumented) => {
if (iErr) {
return callback(iErr, name);
}
fs.writeFile(outputFile, instrumented, 'utf8', err =>
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', () => {
callback(null, name);
});
}
};
const q = async.queue(processor, 10);
const errors = [];
let count = 0;
const startTime = new Date().getTime();
q.push(relativeNames, (err, name) => {
let inputFile;
let outputFile;
if (err) {
errors.push({
file: name,
error: err.message || /* istanbul ignore next */ err.toString()
});
inputFile = path.resolve(inputDir, name);
outputFile = path.resolve(outputDir, name);
fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
}
if (verbose) {
console.error('Processed: ' + name);
} else {
if (count % 100 === 0) {
process.stdout.write('.');
}
}
count += 1;
});
q.drain = function() {
const endTime = new Date().getTime();
console.error(
'\nProcessed [' +
count +
'] files in ' +
Math.floor((endTime - startTime) / 1000) +
' secs'
);
if (errors.length > 0) {
console.error(
'The following ' +
errors.length +
' file(s) had errors and were copied as-is'
);
console.error(errors);
}
return callback();
};
}
function run(config, opts, callback) {
opts = opts || {};
const iOpts = config.instrumentation;
const input = opts.input;
const output = opts.output;
const excludes = opts.excludes;
let stream;
let includes;
let instrumenter;
const origCallback = callback;
const needBaseline = iOpts.saveBaseline();
const baselineFile = path.resolve(iOpts.baselineFile());
if (iOpts.completeCopy()) {
includes = ['**/*'];
} else {
includes = iOpts.extensions().map(ext => '**/*' + ext);
}
if (!input) {
return callback(new Error('No input specified'));
}
instrumenter = libInstrument.createInstrumenter(
iOpts.getInstrumenterOpts()
);
if (needBaseline) {
mkdirp.sync(path.dirname(baselineFile));
instrumenter = new BaselineCollector(instrumenter);
callback = function(err) {
/* istanbul ignore else */
if (!err) {
console.error('Saving baseline coverage at ' + baselineFile);
fs.writeFileSync(
baselineFile,
JSON.stringify(instrumenter.getCoverage()),
'utf8'
);
}
return origCallback(err);
};
}
const file = path.resolve(input);
const stats = fs.statSync(file);
if (stats.isDirectory()) {
if (!output) {
return callback(
inputError.create(
'Need an output directory 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,
excludes: excludes || iOpts.excludes(false),
relative: true
},
(err, files) => {
/* istanbul ignore if */
if (err) {
return callback(err);
}
processFiles(
instrumenter,
{
inputDir: file,
outputDir: output,
names: files,
extensions: iOpts.extensions(),
verbose: config.verbose
},
callback
);
}
);
} 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();
}
return callback();
}
}
module.exports = {
run
};