2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
5 var Module = require('module'),
6 path = require('path'),
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');
22 function usage(arg0, command) {
24 console.error('\nUsage: ' + arg0 + ' ' + command + ' [<options>] <executable-js-file-or-command> [-- <arguments-to-jsfile>]\n\nOptions are:\n\n'
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');
44 function run(args, commandName, enableHooks, callback) {
50 report: [Array, String ],
54 'default-excludes': 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,
63 'include-pid': Boolean
65 opts = nopt(template, { v : '--verbose' }, args, 0),
67 verbose: opts.verbose,
70 'default-excludes': opts['default-excludes'],
72 'include-all-sources': opts['include-all-sources'],
73 'preload-sources': opts['preload-sources'],
74 'include-pid': opts['include-pid']
82 'hook-run-in-context': opts['hook-run-in-context'],
83 'post-require-hook': opts['post-require-hook'],
84 'handle-sigint': opts['handle-sigint']
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'],
95 reporter = new Reporter(config),
99 if (cmdAndArgs.length === 0) {
100 return callback(inputError.create('Need a filename argument for the ' + commandName + ' command!'));
103 cmd = cmdAndArgs.shift();
104 cmdArgs = cmdAndArgs;
106 if (!existsSync(cmd)) {
108 cmd = which.sync(cmd);
110 return callback(inputError.create('Unable to resolve file [' + cmd + ']'));
113 cmd = path.resolve(cmd);
116 runFn = function () {
117 process.argv = ["node", cmd].concat(cmdArgs);
119 console.log('Running: ' + process.argv.join(' '));
121 process.env.running_under_istanbul=1;
122 Module.runMain(cmd, null, true);
125 excludes = config.instrumentation.excludes(true);
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()) {
135 reporter.add('text');
138 reporter.add('text');
139 reporter.add('text-summary');
142 reporter.add('text-summary');
147 excludes.push(path.relative(process.cwd(), path.join(reportingDir, '**', '*')));
149 root: config.instrumentation.root() || process.cwd(),
150 includes: opts.i || config.instrumentation.extensions().map(function (ext) {
155 function (err, matchFn) {
156 if (err) { return callback(err); }
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(),
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');
171 if (postRequireHook) {
172 if (!existsSync(postLoadHookFile)) { //assume it is a module name and resolve it
174 postLoadHookFile = resolve.sync(postRequireHook, { basedir: process.cwd() });
176 if (verbose) { console.error('Unable to resolve [' + postRequireHook + '] as a node module'); }
182 if (postLoadHookFile) {
183 if (verbose) { console.error('Use post-load-hook: ' + postLoadHookFile); }
184 hookOpts.postLoadHook = require(postLoadHookFile)(matchFn, transformer, verbose);
187 if (opts['self-test']) {
188 hook.unloadRequireCache(matchFn);
190 // runInThisContext is used by RequireJS [issue #23]
191 if (config.hooks.hookRunInContext()) {
192 hook.hookRunInThisContext(matchFn, transformer, hookOpts);
194 hook.hookRequire(matchFn, transformer, hookOpts);
196 //initialize the global variable to stop mocha from complaining about leaks
197 global[coverageVar] = {};
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);
206 process.once('exit', function () {
207 var pidExt = includePid ? ('-' + process.pid) : '',
208 file = path.resolve(reportingDir, 'coverage' + pidExt + '.json'),
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');
215 cov = global[coverageVar];
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) {
224 transformer(fs.readFileSync(file, 'utf-8'), file);
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;
233 cov[file] = instrumenter.coverState;
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 + ']');
242 fs.writeFileSync(file, JSON.stringify(cov), 'utf8');
243 collector = new Collector();
245 if (config.reporting.print() !== 'none') {
246 console.error('Writing coverage reports at [' + reportingDir + ']');
247 console.error('=============================================================================');
249 reporter.write(collector, true, callback);