Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80684 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
var Module = require('module'),
6
path = require('path'),
7
fs = require('fs'),
8
nopt = require('nopt'),
9
which = require('which'),
10
mkdirp = require('mkdirp'),
11
existsSync = fs.existsSync || path.existsSync,
12
inputError = require('../../util/input-error'),
13
matcherFor = require('../../util/file-matcher').matcherFor,
14
Instrumenter = require('../../instrumenter'),
15
Collector = require('../../collector'),
16
formatOption = require('../../util/help-formatter').formatOption,
17
hook = require('../../hook'),
18
Reporter = require('../../reporter'),
19
resolve = require('resolve'),
20
configuration = require('../../config');
21
22
function usage(arg0, command) {
23
24
console.error('\nUsage: ' + arg0 + ' ' + command + ' [<options>] <executable-js-file-or-command> [-- <arguments-to-jsfile>]\n\nOptions are:\n\n'
25
+ [
26
formatOption('--config <path-to-config>', 'the configuration file to use, defaults to .istanbul.yml'),
27
formatOption('--root <path> ', 'the root path to look for files to instrument, defaults to .'),
28
formatOption('-x <exclude-pattern> [-x <exclude-pattern>]', 'one or more fileset patterns e.g. "**/vendor/**"'),
29
formatOption('-i <include-pattern> [-i <include-pattern>]', 'one or more fileset patterns e.g. "**/*.js"'),
30
formatOption('--[no-]default-excludes', 'apply default excludes [ **/node_modules/**, **/test/**, **/tests/** ], defaults to true'),
31
formatOption('--hook-run-in-context', 'hook vm.runInThisContext in addition to require (supports RequireJS), defaults to false'),
32
formatOption('--post-require-hook <file> | <module>', 'JS module that exports a function for post-require processing'),
33
formatOption('--report <format> [--report <format>] ', 'report format, defaults to lcov (= lcov.info + HTML)'),
34
formatOption('--dir <report-dir>', 'report directory, defaults to ./coverage'),
35
formatOption('--print <type>', 'type of report to print to console, one of summary (default), detail, both or none'),
36
formatOption('--verbose, -v', 'verbose mode'),
37
formatOption('--[no-]preserve-comments', 'remove / preserve comments in the output, defaults to false'),
38
formatOption('--include-all-sources', 'instrument all unused sources after running tests, defaults to false'),
39
formatOption('--[no-]include-pid', 'include PID in output coverage filename')
40
].join('\n\n') + '\n');
41
console.error('\n');
42
}
43
44
function run(args, commandName, enableHooks, callback) {
45
46
var template = {
47
config: path,
48
root: path,
49
x: [ Array, String ],
50
report: [Array, String ],
51
dir: path,
52
verbose: Boolean,
53
yui: Boolean,
54
'default-excludes': Boolean,
55
print: String,
56
'self-test': Boolean,
57
'hook-run-in-context': Boolean,
58
'post-require-hook': String,
59
'preserve-comments': Boolean,
60
'include-all-sources': Boolean,
61
'preload-sources': Boolean,
62
i: [ Array, String ],
63
'include-pid': Boolean
64
},
65
opts = nopt(template, { v : '--verbose' }, args, 0),
66
overrides = {
67
verbose: opts.verbose,
68
instrumentation: {
69
root: opts.root,
70
'default-excludes': opts['default-excludes'],
71
excludes: opts.x,
72
'include-all-sources': opts['include-all-sources'],
73
'preload-sources': opts['preload-sources'],
74
'include-pid': opts['include-pid']
75
},
76
reporting: {
77
reports: opts.report,
78
print: opts.print,
79
dir: opts.dir
80
},
81
hooks: {
82
'hook-run-in-context': opts['hook-run-in-context'],
83
'post-require-hook': opts['post-require-hook'],
84
'handle-sigint': opts['handle-sigint']
85
}
86
},
87
config = configuration.loadFile(opts.config, overrides),
88
verbose = config.verbose,
89
cmdAndArgs = opts.argv.remain,
90
preserveComments = opts['preserve-comments'],
91
includePid = opts['include-pid'],
92
cmd,
93
cmdArgs,
94
reportingDir,
95
reporter = new Reporter(config),
96
runFn,
97
excludes;
98
99
if (cmdAndArgs.length === 0) {
100
return callback(inputError.create('Need a filename argument for the ' + commandName + ' command!'));
101
}
102
103
cmd = cmdAndArgs.shift();
104
cmdArgs = cmdAndArgs;
105
106
if (!existsSync(cmd)) {
107
try {
108
cmd = which.sync(cmd);
109
} catch (ex) {
110
return callback(inputError.create('Unable to resolve file [' + cmd + ']'));
111
}
112
} else {
113
cmd = path.resolve(cmd);
114
}
115
116
runFn = function () {
117
process.argv = ["node", cmd].concat(cmdArgs);
118
if (verbose) {
119
console.log('Running: ' + process.argv.join(' '));
120
}
121
process.env.running_under_istanbul=1;
122
Module.runMain(cmd, null, true);
123
};
124
125
excludes = config.instrumentation.excludes(true);
126
127
if (enableHooks) {
128
reportingDir = path.resolve(config.reporting.dir());
129
mkdirp.sync(reportingDir); //ensure we fail early if we cannot do this
130
reporter.addAll(config.reporting.reports());
131
if (config.reporting.print() !== 'none') {
132
switch (config.reporting.print()) {
133
case 'detail':
134
reporter.add('text');
135
break;
136
case 'both':
137
reporter.add('text');
138
reporter.add('text-summary');
139
break;
140
default:
141
reporter.add('text-summary');
142
break;
143
}
144
}
145
146
excludes.push(path.relative(process.cwd(), path.join(reportingDir, '**', '*')));
147
matcherFor({
148
root: config.instrumentation.root() || process.cwd(),
149
includes: opts.i || [ '**/*.js' ],
150
excludes: excludes
151
},
152
function (err, matchFn) {
153
if (err) { return callback(err); }
154
155
var coverageVar = '$$cov_' + new Date().getTime() + '$$',
156
instrumenter = new Instrumenter({ coverageVariable: coverageVar , preserveComments: preserveComments}),
157
transformer = instrumenter.instrumentSync.bind(instrumenter),
158
hookOpts = { verbose: verbose },
159
postRequireHook = config.hooks.postRequireHook(),
160
postLoadHookFile;
161
162
if (postRequireHook) {
163
postLoadHookFile = path.resolve(postRequireHook);
164
} else if (opts.yui) { //EXPERIMENTAL code: do not rely on this in anyway until the docs say it is allowed
165
postLoadHookFile = path.resolve(__dirname, '../../util/yui-load-hook');
166
}
167
168
if (postRequireHook) {
169
if (!existsSync(postLoadHookFile)) { //assume it is a module name and resolve it
170
try {
171
postLoadHookFile = resolve.sync(postRequireHook, { basedir: process.cwd() });
172
} catch (ex) {
173
if (verbose) { console.error('Unable to resolve [' + postRequireHook + '] as a node module'); }
174
callback(ex);
175
return;
176
}
177
}
178
}
179
if (postLoadHookFile) {
180
if (verbose) { console.error('Use post-load-hook: ' + postLoadHookFile); }
181
hookOpts.postLoadHook = require(postLoadHookFile)(matchFn, transformer, verbose);
182
}
183
184
if (opts['self-test']) {
185
hook.unloadRequireCache(matchFn);
186
}
187
// runInThisContext is used by RequireJS [issue #23]
188
if (config.hooks.hookRunInContext()) {
189
hook.hookRunInThisContext(matchFn, transformer, hookOpts);
190
}
191
hook.hookRequire(matchFn, transformer, hookOpts);
192
193
//initialize the global variable to stop mocha from complaining about leaks
194
global[coverageVar] = {};
195
196
// enable passing --handle-sigint to write reports on SIGINT.
197
// This allows a user to manually kill a process while
198
// still getting the istanbul report.
199
if (config.hooks.handleSigint()) {
200
process.once('SIGINT', process.exit);
201
}
202
203
process.once('exit', function () {
204
var pidExt = includePid ? ('-' + process.pid) : '',
205
file = path.resolve(reportingDir, 'coverage' + pidExt + '.json'),
206
collector,
207
cov;
208
if (typeof global[coverageVar] === 'undefined' || Object.keys(global[coverageVar]).length === 0) {
209
console.error('No coverage information was collected, exit without writing coverage information');
210
return;
211
} else {
212
cov = global[coverageVar];
213
}
214
//important: there is no event loop at this point
215
//everything that happens in this exit handler MUST be synchronous
216
if (config.instrumentation.includeAllSources()) {
217
// Files that are not touched by code ran by the test runner is manually instrumented, to
218
// illustrate the missing coverage.
219
matchFn.files.forEach(function (file) {
220
if (!cov[file]) {
221
transformer(fs.readFileSync(file, 'utf-8'), file);
222
223
// When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in coverState.s,
224
// presumably to compensate for function hoisting. We need to reset this, as the function was not hoisted,
225
// as it was never loaded.
226
Object.keys(instrumenter.coverState.s).forEach(function (key) {
227
instrumenter.coverState.s[key] = 0;
228
});
229
230
cov[file] = instrumenter.coverState;
231
}
232
});
233
}
234
mkdirp.sync(reportingDir); //yes, do this again since some test runners could clean the dir initially created
235
if (config.reporting.print() !== 'none') {
236
console.error('=============================================================================');
237
console.error('Writing coverage object [' + file + ']');
238
}
239
fs.writeFileSync(file, JSON.stringify(cov), 'utf8');
240
collector = new Collector();
241
collector.add(cov);
242
if (config.reporting.print() !== 'none') {
243
console.error('Writing coverage reports at [' + reportingDir + ']');
244
console.error('=============================================================================');
245
}
246
reporter.write(collector, true, callback);
247
});
248
runFn();
249
});
250
} else {
251
runFn();
252
}
253
}
254
255
module.exports = {
256
run: run,
257
usage: usage
258
};
259
260