UNPKG

12.1 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 */
5var 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
22function 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 glob patterns e.g. "**/vendor/**"'),
29 formatOption('-i <include-pattern> [-i <include-pattern>]', 'one or more glob 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
44function 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.dir = reportingDir;
131 reporter.addAll(config.reporting.reports());
132 if (config.reporting.print() !== 'none') {
133 switch (config.reporting.print()) {
134 case 'detail':
135 reporter.add('text');
136 break;
137 case 'both':
138 reporter.add('text');
139 reporter.add('text-summary');
140 break;
141 default:
142 reporter.add('text-summary');
143 break;
144 }
145 }
146
147 excludes.push(path.relative(process.cwd(), path.join(reportingDir, '**', '*')));
148 matcherFor({
149 root: config.instrumentation.root() || process.cwd(),
150 includes: opts.i || config.instrumentation.extensions().map(function (ext) {
151 return '**/*' + ext;
152 }),
153 excludes: excludes
154 },
155 function (err, matchFn) {
156 if (err) { return callback(err); }
157
158 var coverageVar = '$$cov_' + new Date().getTime() + '$$',
159 instrumenter = new Instrumenter({ coverageVariable: coverageVar , preserveComments: preserveComments}),
160 transformer = instrumenter.instrumentSync.bind(instrumenter),
161 hookOpts = { verbose: verbose, extensions: config.instrumentation.extensions() },
162 postRequireHook = config.hooks.postRequireHook(),
163 postLoadHookFile;
164
165 if (postRequireHook) {
166 postLoadHookFile = path.resolve(postRequireHook);
167 } else if (opts.yui) { //EXPERIMENTAL code: do not rely on this in anyway until the docs say it is allowed
168 postLoadHookFile = path.resolve(__dirname, '../../util/yui-load-hook');
169 }
170
171 if (postRequireHook) {
172 if (!existsSync(postLoadHookFile)) { //assume it is a module name and resolve it
173 try {
174 postLoadHookFile = resolve.sync(postRequireHook, { basedir: process.cwd() });
175 } catch (ex) {
176 if (verbose) { console.error('Unable to resolve [' + postRequireHook + '] as a node module'); }
177 callback(ex);
178 return;
179 }
180 }
181 }
182 if (postLoadHookFile) {
183 if (verbose) { console.error('Use post-load-hook: ' + postLoadHookFile); }
184 hookOpts.postLoadHook = require(postLoadHookFile)(matchFn, transformer, verbose);
185 }
186
187 if (opts['self-test']) {
188 hook.unloadRequireCache(matchFn);
189 }
190 // runInThisContext is used by RequireJS [issue #23]
191 if (config.hooks.hookRunInContext()) {
192 hook.hookRunInThisContext(matchFn, transformer, hookOpts);
193 }
194 hook.hookRequire(matchFn, transformer, hookOpts);
195
196 //initialize the global variable to stop mocha from complaining about leaks
197 global[coverageVar] = {};
198
199 // enable passing --handle-sigint to write reports on SIGINT.
200 // This allows a user to manually kill a process while
201 // still getting the istanbul report.
202 if (config.hooks.handleSigint()) {
203 process.once('SIGINT', process.exit);
204 }
205
206 process.once('exit', function () {
207 var pidExt = includePid ? ('-' + process.pid) : '',
208 file = path.resolve(reportingDir, 'coverage' + pidExt + '.json'),
209 collector,
210 cov;
211 if (typeof global[coverageVar] === 'undefined' || Object.keys(global[coverageVar]).length === 0) {
212 console.error('No coverage information was collected, exit without writing coverage information');
213 return;
214 } else {
215 cov = global[coverageVar];
216 }
217 //important: there is no event loop at this point
218 //everything that happens in this exit handler MUST be synchronous
219 if (config.instrumentation.includeAllSources()) {
220 // Files that are not touched by code ran by the test runner is manually instrumented, to
221 // illustrate the missing coverage.
222 matchFn.files.forEach(function (file) {
223 if (!cov[file]) {
224 transformer(fs.readFileSync(file, 'utf-8'), file);
225
226 // When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in coverState.s,
227 // presumably to compensate for function hoisting. We need to reset this, as the function was not hoisted,
228 // as it was never loaded.
229 Object.keys(instrumenter.coverState.s).forEach(function (key) {
230 instrumenter.coverState.s[key] = 0;
231 });
232
233 cov[file] = instrumenter.coverState;
234 }
235 });
236 }
237 mkdirp.sync(reportingDir); //yes, do this again since some test runners could clean the dir initially created
238 if (config.reporting.print() !== 'none') {
239 console.error('=============================================================================');
240 console.error('Writing coverage object [' + file + ']');
241 }
242 fs.writeFileSync(file, JSON.stringify(cov), 'utf8');
243 collector = new Collector();
244 collector.add(cov);
245 if (config.reporting.print() !== 'none') {
246 console.error('Writing coverage reports at [' + reportingDir + ']');
247 console.error('=============================================================================');
248 }
249 reporter.write(collector, true, callback);
250 });
251 runFn();
252 });
253 } else {
254 runFn();
255 }
256}
257
258module.exports = {
259 run: run,
260 usage: usage
261};