UNPKG

15.5 kBJavaScriptView Raw
1/*
2 Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5var path = require('path'),
6 fs = require('fs'),
7 existsSync = fs.existsSync || path.existsSync,
8 CAMEL_PATTERN = /([a-z])([A-Z])/g,
9 YML_PATTERN = /\.ya?ml$/,
10 yaml = require('js-yaml'),
11 defaults = require('./report/common/defaults');
12
13function defaultConfig(includeBackCompatAttrs) {
14 var ret = {
15 verbose: false,
16 instrumentation: {
17 root: '.',
18 extensions: ['.js'],
19 'default-excludes': true,
20 excludes: [],
21 'embed-source': false,
22 variable: '__coverage__',
23 compact: true,
24 'preserve-comments': false,
25 'complete-copy': false,
26 'save-baseline': false,
27 'baseline-file': './coverage/coverage-baseline.json',
28 'include-all-sources': false,
29 'include-pid': false,
30 'es-modules': false
31 },
32 reporting: {
33 print: 'summary',
34 reports: [ 'lcov' ],
35 dir: './coverage'
36 },
37 hooks: {
38 'hook-run-in-context': false,
39 'post-require-hook': null,
40 'handle-sigint': false
41 },
42 check: {
43 global: {
44 statements: 0,
45 lines: 0,
46 branches: 0,
47 functions: 0,
48 excludes: [] // Currently list of files (root + path). For future, extend to patterns.
49 },
50 each: {
51 statements: 0,
52 lines: 0,
53 branches: 0,
54 functions: 0,
55 excludes: []
56 }
57 }
58 };
59 ret.reporting.watermarks = defaults.watermarks();
60 ret.reporting['report-config'] = defaults.defaultReportConfig();
61
62 if (includeBackCompatAttrs) {
63 ret.instrumentation['preload-sources'] = false;
64 }
65
66 return ret;
67}
68
69function dasherize(word) {
70 return word.replace(CAMEL_PATTERN, function (match, lch, uch) {
71 return lch + '-' + uch.toLowerCase();
72 });
73}
74function isScalar(v) {
75 if (v === null) { return true; }
76 return v !== undefined && !Array.isArray(v) && typeof v !== 'object';
77}
78
79function isObject(v) {
80 return typeof v === 'object' && v !== null && !Array.isArray(v);
81}
82
83function mergeObjects(explicit, template) {
84
85 var ret = {};
86
87 Object.keys(template).forEach(function (k) {
88 var v1 = template[k],
89 v2 = explicit[k];
90
91 if (Array.isArray(v1)) {
92 ret[k] = Array.isArray(v2) && v2.length > 0 ? v2 : v1;
93 } else if (isObject(v1)) {
94 v2 = isObject(v2) ? v2 : {};
95 ret[k] = mergeObjects(v2, v1);
96 } else {
97 ret[k] = isScalar(v2) ? v2 : v1;
98 }
99 });
100 return ret;
101}
102
103function mergeDefaults(explicit, implicit) {
104 return mergeObjects(explicit || {}, implicit);
105}
106
107function addMethods() {
108 var args = Array.prototype.slice.call(arguments),
109 cons = args.shift();
110
111 args.forEach(function (arg) {
112 var method = arg,
113 property = dasherize(arg);
114 cons.prototype[method] = function () {
115 return this.config[property];
116 };
117 });
118}
119
120/**
121 * Object that returns instrumentation options
122 * @class InstrumentOptions
123 * @module config
124 * @constructor
125 * @param config the instrumentation part of the config object
126 */
127function InstrumentOptions(config) {
128 if (config['preload-sources']) {
129 console.error('The preload-sources option is deprecated, please use include-all-sources instead.');
130 config['include-all-sources'] = config['preload-sources'];
131 }
132 this.config = config;
133}
134
135/**
136 * returns if default excludes should be turned on. Used by the `cover` command.
137 * @method defaultExcludes
138 * @return {Boolean} true if default excludes should be turned on
139 */
140/**
141 * returns if non-JS files should be copied during instrumentation. Used by the
142 * `instrument` command.
143 * @method completeCopy
144 * @return {Boolean} true if non-JS files should be copied
145 */
146/**
147 * returns if the source should be embedded in the instrumented code. Used by the
148 * `instrument` command.
149 * @method embedSource
150 * @return {Boolean} true if the source should be embedded in the instrumented code
151 */
152/**
153 * the coverage variable name to use. Used by the `instrument` command.
154 * @method variable
155 * @return {String} the coverage variable name to use
156 */
157/**
158 * returns if the output should be compact JS. Used by the `instrument` command.
159 * @method compact
160 * @return {Boolean} true if the output should be compact
161 */
162/**
163 * returns if comments should be preserved in the generated JS. Used by the
164 * `cover` and `instrument` commands.
165 * @method preserveComments
166 * @return {Boolean} true if comments should be preserved in the generated JS
167 */
168/**
169 * returns if a zero-coverage baseline file should be written as part of
170 * instrumentation. This allows reporting to display numbers for files that have
171 * no tests. Used by the `instrument` command.
172 * @method saveBaseline
173 * @return {Boolean} true if a baseline coverage file should be written.
174 */
175/**
176 * Sets the baseline coverage filename. Used by the `instrument` command.
177 * @method baselineFile
178 * @return {String} the name of the baseline coverage file.
179 */
180/**
181 * returns if comments the JS to instrument contains es6 Module syntax.
182 * @method esModules
183 * @return {Boolean} true if code contains es6 import/export statements.
184 */
185/**
186 * returns if the coverage filename should include the PID. Used by the `instrument` command.
187 * @method includePid
188 * @return {Boolean} true to include pid in coverage filename.
189 */
190
191
192addMethods(InstrumentOptions,
193 'extensions', 'defaultExcludes', 'completeCopy',
194 'embedSource', 'variable', 'compact', 'preserveComments',
195 'saveBaseline', 'baselineFile', 'esModules',
196 'includeAllSources', 'includePid');
197
198/**
199 * returns the root directory used by istanbul which is typically the root of the
200 * source tree. Used by the `cover` and `report` commands.
201 * @method root
202 * @return {String} the root directory used by istanbul.
203 */
204InstrumentOptions.prototype.root = function () { return path.resolve(this.config.root); };
205/**
206 * returns an array of glob patterns that should be excluded for instrumentation.
207 * Used by the `instrument` and `cover` commands.
208 * @method excludes
209 * @return {Array} an array of glob patterns that should be excluded for
210 * instrumentation.
211 */
212InstrumentOptions.prototype.excludes = function (excludeTests) {
213 var defs;
214 if (this.defaultExcludes()) {
215 defs = [ '**/node_modules/**' ];
216 if (excludeTests) {
217 defs = defs.concat(['**/test/**', '**/tests/**']);
218 }
219 return defs.concat(this.config.excludes);
220 }
221 return this.config.excludes;
222};
223
224/**
225 * Object that returns reporting options
226 * @class ReportingOptions
227 * @module config
228 * @constructor
229 * @param config the reporting part of the config object
230 */
231function ReportingOptions(config) {
232 this.config = config;
233}
234
235/**
236 * returns the kind of information to be printed on the console. May be one
237 * of `summary`, `detail`, `both` or `none`. Used by the
238 * `cover` command.
239 * @method print
240 * @return {String} the kind of information to print to the console at the end
241 * of the `cover` command execution.
242 */
243/**
244 * returns a list of reports that should be generated at the end of a run. Used
245 * by the `cover` and `report` commands.
246 * @method reports
247 * @return {Array} an array of reports that should be produced
248 */
249/**
250 * returns the directory under which reports should be generated. Used by the
251 * `cover` and `report` commands.
252 *
253 * @method dir
254 * @return {String} the directory under which reports should be generated.
255 */
256/**
257 * returns an object that has keys that are report format names and values that are objects
258 * containing detailed configuration for each format. Running `istanbul help config`
259 * will give you all the keys per report format that can be overridden.
260 * Used by the `cover` and `report` commands.
261 * @method reportConfig
262 * @return {Object} detailed report configuration per report format.
263 */
264addMethods(ReportingOptions, 'print', 'reports', 'dir', 'reportConfig');
265
266function isInvalidMark(v, key) {
267 var prefix = 'Watermark for [' + key + '] :';
268
269 if (v.length !== 2) {
270 return prefix + 'must be an array of length 2';
271 }
272 v[0] = Number(v[0]);
273 v[1] = Number(v[1]);
274
275 if (isNaN(v[0]) || isNaN(v[1])) {
276 return prefix + 'must have valid numbers';
277 }
278 if (v[0] < 0 || v[1] < 0) {
279 return prefix + 'must be positive numbers';
280 }
281 if (v[1] > 100) {
282 return prefix + 'cannot exceed 100';
283 }
284 if (v[1] <= v[0]) {
285 return prefix + 'low must be less than high';
286 }
287 return null;
288}
289
290/**
291 * returns the low and high watermarks to be used to designate whether coverage
292 * is `low`, `medium` or `high`. Statements, functions, branches and lines can
293 * have independent watermarks. These are respected by all reports
294 * that color for low, medium and high coverage. See the default configuration for exact syntax
295 * using `istanbul help config`. Used by the `cover` and `report` commands.
296 *
297 * @method watermarks
298 * @return {Object} an object containing low and high watermarks for statements,
299 * branches, functions and lines.
300 */
301ReportingOptions.prototype.watermarks = function () {
302 var v = this.config.watermarks,
303 defs = defaults.watermarks(),
304 ret = {};
305
306 Object.keys(defs).forEach(function (k) {
307 var mark = v[k], //it will already be a non-zero length array because of the way the merge works
308 message = isInvalidMark(mark, k);
309 if (message) {
310 console.error(message);
311 ret[k] = defs[k];
312 } else {
313 ret[k] = mark;
314 }
315 });
316 return ret;
317};
318
319/**
320 * Object that returns hook options. Note that istanbul does not provide an
321 * option to hook `require`. This is always done by the `cover` command.
322 * @class HookOptions
323 * @module config
324 * @constructor
325 * @param config the hooks part of the config object
326 */
327function HookOptions(config) {
328 this.config = config;
329}
330
331/**
332 * returns if `vm.runInThisContext` needs to be hooked, in addition to the standard
333 * `require` hooks added by istanbul. This should be true for code that uses
334 * RequireJS for example. Used by the `cover` command.
335 * @method hookRunInContext
336 * @return {Boolean} true if `vm.runInThisContext` needs to be hooked for coverage
337 */
338/**
339 * returns a path to JS file or a dependent module that should be used for
340 * post-processing files after they have been required. See the `yui-istanbul` module for
341 * an example of a post-require hook. This particular hook modifies the yui loader when
342 * that file is required to add istanbul interceptors. Use by the `cover` command
343 *
344 * @method postRequireHook
345 * @return {String} a path to a JS file or the name of a node module that needs
346 * to be used as a `require` post-processor
347 */
348/**
349 * returns if istanbul needs to add a SIGINT (control-c, usually) handler to
350 * save coverage information. Useful for getting code coverage out of processes
351 * that run forever and need a SIGINT to terminate.
352 * @method handleSigint
353 * @return {Boolean} true if SIGINT needs to be hooked to write coverage information
354 */
355
356addMethods(HookOptions, 'hookRunInContext', 'postRequireHook', 'handleSigint');
357
358/**
359 * represents the istanbul configuration and provides sub-objects that can
360 * return instrumentation, reporting and hook options respectively.
361 * Usage
362 * -----
363 *
364 * var configObj = require('istanbul').config.loadFile();
365 *
366 * console.log(configObj.reporting.reports());
367 *
368 * @class Configuration
369 * @module config
370 * @param {Object} obj the base object to use as the configuration
371 * @param {Object} overrides optional - override attributes that are merged into
372 * the base config
373 * @constructor
374 */
375function Configuration(obj, overrides) {
376
377 var config = mergeDefaults(obj, defaultConfig(true));
378 if (isObject(overrides)) {
379 config = mergeDefaults(overrides, config);
380 }
381 if (config.verbose) {
382 console.error('Using configuration');
383 console.error('-------------------');
384 console.error(yaml.safeDump(config, { indent: 4, flowLevel: 3 }));
385 console.error('-------------------\n');
386 }
387 this.verbose = config.verbose;
388 this.instrumentation = new InstrumentOptions(config.instrumentation);
389 this.reporting = new ReportingOptions(config.reporting);
390 this.hooks = new HookOptions(config.hooks);
391 this.check = config.check; // Pass raw config sub-object.
392}
393
394/**
395 * true if verbose logging is required
396 * @property verbose
397 * @type Boolean
398 */
399/**
400 * instrumentation options
401 * @property instrumentation
402 * @type InstrumentOptions
403 */
404/**
405 * reporting options
406 * @property reporting
407 * @type ReportingOptions
408 */
409/**
410 * hook options
411 * @property hooks
412 * @type HookOptions
413 */
414
415
416function loadFile(file, overrides) {
417 var defaultConfigFile = path.resolve('.istanbul.yml'),
418 configObject;
419
420 if (file) {
421 if (!existsSync(file)) {
422 throw new Error('Invalid configuration file specified:' + file);
423 }
424 } else {
425 if (existsSync(defaultConfigFile)) {
426 file = defaultConfigFile;
427 }
428 }
429
430 if (file) {
431 if (overrides && overrides.verbose === true) {
432 console.error('Loading config: ' + file);
433 }
434 configObject = file.match(YML_PATTERN) ?
435 yaml.safeLoad(fs.readFileSync(file, 'utf8'), { filename: file }) :
436 require(path.resolve(file));
437 }
438
439 return new Configuration(configObject, overrides);
440}
441
442function loadObject(obj, overrides) {
443 return new Configuration(obj, overrides);
444}
445
446/**
447 * methods to load the configuration object.
448 * Usage
449 * -----
450 *
451 * var config = require('istanbul').config,
452 * configObj = config.loadFile();
453 *
454 * console.log(configObj.reporting.reports());
455 *
456 * @class Config
457 * @module main
458 * @static
459 */
460module.exports = {
461 /**
462 * loads the specified configuration file with optional overrides. Throws
463 * when a file is specified and it is not found.
464 * @method loadFile
465 * @static
466 * @param {String} file the file to load. If falsy, the default config file, if present, is loaded.
467 * If not a default config is used.
468 * @param {Object} overrides - an object with override keys that are merged into the
469 * config object loaded
470 * @return {Configuration} the config object with overrides applied
471 */
472 loadFile: loadFile,
473 /**
474 * loads the specified configuration object with optional overrides.
475 * @method loadObject
476 * @static
477 * @param {Object} obj the object to use as the base configuration.
478 * @param {Object} overrides - an object with override keys that are merged into the
479 * config object
480 * @return {Configuration} the config object with overrides applied
481 */
482 loadObject: loadObject,
483 /**
484 * returns the default configuration object. Note that this is a plain object
485 * and not a `Configuration` instance.
486 * @method defaultConfig
487 * @static
488 * @return {Object} an object that represents the default config
489 */
490 defaultConfig: defaultConfig
491};