UNPKG

10.7 kBJavaScriptView Raw
1/*
2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5
6var path = require('path'),
7 mkdirp = require('mkdirp'),
8 once = require('once'),
9 async = require('async'),
10 fs = require('fs'),
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'),
20 verbose;
21
22
23/*
24 * Chunk file size to use when reading non JavaScript files in memory
25 * and copying them over when using complete-copy flag.
26 */
27var READ_FILE_CHUNK_SIZE = 64 * 1024;
28
29function BaselineCollector(instrumenter) {
30 this.instrumenter = instrumenter;
31 this.collector = new Collector();
32 this.instrument = instrumenter.instrument.bind(this.instrumenter);
33
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(),
39 coverage = {};
40 coverage[baseline.path] = baseline;
41 this.collector.add(coverage);
42 return ret;
43 };
44 //monkey patch the instrumenter to call our version instead
45 instrumenter.instrumentSync = this.instrumentSync.bind(this);
46}
47
48BaselineCollector.prototype = {
49 getCoverage: function () {
50 return this.collector.getFinalCoverage();
51 }
52};
53
54
55function 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;
63
64 callback = once(callback);
65 mkdirp.sync(oDir);
66
67 if (fs.statSync(inputFile).isDirectory()) {
68 return callback(null, name);
69 }
70
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);
78 });
79 });
80 });
81 }
82 else {
83 // non JavaScript file, copy it as is
84 readStream = fs.createReadStream(inputFile, {'bufferSize': READ_FILE_CHUNK_SIZE});
85 writeStream = fs.createWriteStream(outputFile);
86
87 readStream.on('error', callback);
88 writeStream.on('error', callback);
89
90 readStream.pipe(writeStream);
91 readStream.on('end', function() {
92 callback(null, name);
93 });
94 }
95 },
96 q = async.queue(processor, 10),
97 errors = [],
98 count = 0,
99 startTime = new Date().getTime();
100
101 q.push(relativeNames, function (err, name) {
102 var inputFile, outputFile;
103 if (err) {
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));
108 }
109 if (verbose) {
110 console.log('Processed: ' + name);
111 } else {
112 if (count % 100 === 0) { process.stdout.write('.'); }
113 }
114 count += 1;
115 });
116
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');
122 console.log(errors);
123 }
124 };
125}
126
127
128function InstrumentCommand() {
129 Command.call(this);
130}
131
132InstrumentCommand.TYPE = 'instrument';
133util.inherits(InstrumentCommand, Command);
134
135Command.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";
138 },
139
140 usage: function () {
141 console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> <file-or-directory>\n\nOptions are:\n\n' +
142 [
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 glob 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 formatOption('--es-modules', 'source code uses es import/export module syntax')
157 ].join('\n\n') + '\n');
158 console.error('\n');
159 },
160
161 run: function (args, callback) {
162
163 var template = {
164 config: path,
165 output: path,
166 x: [Array, String],
167 variable: String,
168 compact: Boolean,
169 'complete-copy': Boolean,
170 verbose: Boolean,
171 'save-baseline': Boolean,
172 'baseline-file': path,
173 'embed-source': Boolean,
174 'preserve-comments': Boolean,
175 'es-modules': Boolean
176 },
177 opts = nopt(template, { v : '--verbose' }, args, 0),
178 overrides = {
179 verbose: opts.verbose,
180 instrumentation: {
181 variable: opts.variable,
182 compact: opts.compact,
183 'embed-source': opts['embed-source'],
184 'preserve-comments': opts['preserve-comments'],
185 excludes: opts.x,
186 'complete-copy': opts['complete-copy'],
187 'save-baseline': opts['save-baseline'],
188 'baseline-file': opts['baseline-file'],
189 'es-modules': opts['es-modules']
190 }
191 },
192 config = configuration.loadFile(opts.config, overrides),
193 iOpts = config.instrumentation,
194 cmdArgs = opts.argv.remain,
195 file,
196 stats,
197 stream,
198 includes,
199 instrumenter,
200 needBaseline = iOpts.saveBaseline(),
201 baselineFile = path.resolve(iOpts.baselineFile()),
202 output = opts.output;
203
204 verbose = config.verbose;
205 if (cmdArgs.length !== 1) {
206 return callback(inputError.create('Need exactly one filename/ dirname argument for the instrument command!'));
207 }
208
209 if (iOpts.completeCopy()) {
210 includes = ['**/*'];
211 }
212 else {
213 includes = iOpts.extensions().map(function(ext) {
214 return '**/*' + ext;
215 });
216 }
217
218 instrumenter = new Instrumenter({
219 coverageVariable: iOpts.variable(),
220 embedSource: iOpts.embedSource(),
221 noCompact: !iOpts.compact(),
222 preserveComments: iOpts.preserveComments(),
223 esModules: iOpts.esModules()
224 });
225
226 if (needBaseline) {
227 mkdirp.sync(path.dirname(baselineFile));
228 instrumenter = new BaselineCollector(instrumenter);
229 process.on('exit', function () {
230 console.log('Saving baseline coverage at: ' + baselineFile);
231 fs.writeFileSync(baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8');
232 });
233 }
234
235 file = path.resolve(cmdArgs[0]);
236 stats = fs.statSync(file);
237 if (stats.isDirectory()) {
238 if (!output) { return callback(inputError.create('Need an output directory [-o <dir>] when input is a directory!')); }
239 if (output === file) { return callback(inputError.create('Cannot instrument into the same directory/ file as input!')); }
240 mkdirp.sync(output);
241 filesFor({
242 root: file,
243 includes: includes,
244 excludes: opts.x || iOpts.excludes(false), // backwards-compat, *sigh*
245 relative: true
246 }, function (err, files) {
247 if (err) { return callback(err); }
248 processFiles(instrumenter, file, output, files, iOpts.extensions());
249 });
250 } else {
251 if (output) {
252 stream = fs.createWriteStream(output);
253 } else {
254 stream = process.stdout;
255 }
256 stream.write(instrumenter.instrumentSync(fs.readFileSync(file, 'utf8'), file));
257 if (stream !== process.stdout) {
258 stream.end();
259 }
260 }
261 }
262});
263
264module.exports = InstrumentCommand;
265