1 |
|
2 |
|
3 |
|
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 |
|
25 |
|
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 |
|
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, 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 |
|
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 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),
|
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 |
|
264 | module.exports = InstrumentCommand;
|
265 |
|