Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80680 views
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
6
var 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
*/
27
var READ_FILE_CHUNK_SIZE = 64 * 1024;
28
29
function 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
48
BaselineCollector.prototype = {
49
getCoverage: function () {
50
return this.collector.getFinalCoverage();
51
}
52
};
53
54
55
function processFiles(instrumenter, inputDir, outputDir, relativeNames) {
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 = (inputFileExtenstion === '.js'),
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
128
function InstrumentCommand() {
129
Command.call(this);
130
}
131
132
InstrumentCommand.TYPE = 'instrument';
133
util.inherits(InstrumentCommand, Command);
134
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";
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 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');
157
console.error('\n');
158
},
159
160
run: function (args, callback) {
161
162
var template = {
163
config: path,
164
output: path,
165
x: [Array, String],
166
variable: String,
167
compact: Boolean,
168
'complete-copy': Boolean,
169
verbose: Boolean,
170
'save-baseline': Boolean,
171
'baseline-file': path,
172
'embed-source': Boolean,
173
'preserve-comments': Boolean
174
},
175
opts = nopt(template, { v : '--verbose' }, args, 0),
176
overrides = {
177
verbose: opts.verbose,
178
instrumentation: {
179
variable: opts.variable,
180
compact: opts.compact,
181
'embed-source': opts['embed-source'],
182
'preserve-comments': opts['preserve-comments'],
183
excludes: opts.x,
184
'complete-copy': opts['complete-copy'],
185
'save-baseline': opts['save-baseline'],
186
'baseline-file': opts['baseline-file']
187
}
188
},
189
config = configuration.loadFile(opts.config, overrides),
190
iOpts = config.instrumentation,
191
cmdArgs = opts.argv.remain,
192
file,
193
stats,
194
stream,
195
includes,
196
instrumenter,
197
needBaseline = iOpts.saveBaseline(),
198
baselineFile = path.resolve(iOpts.baselineFile()),
199
output = opts.output;
200
201
verbose = config.verbose;
202
if (cmdArgs.length !== 1) {
203
return callback(inputError.create('Need exactly one filename/ dirname argument for the instrument command!'));
204
}
205
206
if (iOpts.completeCopy()) {
207
includes = ['**/*'];
208
}
209
else {
210
includes = ['**/*.js'];
211
}
212
213
instrumenter = new Instrumenter({
214
coverageVariable: iOpts.variable(),
215
embedSource: iOpts.embedSource(),
216
noCompact: !iOpts.compact(),
217
preserveComments: iOpts.preserveComments()
218
});
219
220
if (needBaseline) {
221
mkdirp.sync(path.dirname(baselineFile));
222
instrumenter = new BaselineCollector(instrumenter);
223
process.on('exit', function () {
224
util.puts('Saving baseline coverage at: ' + baselineFile);
225
fs.writeFileSync(baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8');
226
});
227
}
228
229
file = path.resolve(cmdArgs[0]);
230
stats = fs.statSync(file);
231
if (stats.isDirectory()) {
232
if (!output) { return callback(inputError.create('Need an output directory [-o <dir>] when input is a directory!')); }
233
if (output === file) { return callback(inputError.create('Cannot instrument into the same directory/ file as input!')); }
234
mkdirp.sync(output);
235
filesFor({
236
root: file,
237
includes: includes,
238
excludes: opts.x || iOpts.excludes(false), // backwards-compat, *sigh*
239
relative: true
240
}, function (err, files) {
241
if (err) { return callback(err); }
242
processFiles(instrumenter, file, output, files);
243
});
244
} else {
245
if (output) {
246
stream = fs.createWriteStream(output);
247
} else {
248
stream = process.stdout;
249
}
250
stream.write(instrumenter.instrumentSync(fs.readFileSync(file, 'utf8'), file));
251
if (stream !== process.stdout) {
252
stream.end();
253
}
254
}
255
}
256
});
257
258
module.exports = InstrumentCommand;
259
260
261