1 |
|
2 |
|
3 |
|
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 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 |
|
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);
|
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) {
|
168 | postLoadHookFile = path.resolve(__dirname, '../../util/yui-load-hook');
|
169 | }
|
170 |
|
171 | if (postRequireHook) {
|
172 | if (!existsSync(postLoadHookFile)) {
|
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 |
|
191 | if (config.hooks.hookRunInContext()) {
|
192 | hook.hookRunInThisContext(matchFn, transformer, hookOpts);
|
193 | }
|
194 | hook.hookRequire(matchFn, transformer, hookOpts);
|
195 |
|
196 |
|
197 | global[coverageVar] = {};
|
198 |
|
199 |
|
200 |
|
201 |
|
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 |
|
218 |
|
219 | if (config.instrumentation.includeAllSources()) {
|
220 |
|
221 |
|
222 | matchFn.files.forEach(function (file) {
|
223 | if (!cov[file]) {
|
224 | transformer(fs.readFileSync(file, 'utf-8'), file);
|
225 |
|
226 |
|
227 |
|
228 |
|
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);
|
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 |
|
258 | module.exports = {
|
259 | run: run,
|
260 | usage: usage
|
261 | };
|