2 Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
5 var path = require('path'),
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');
13 function defaultConfig(includeBackCompatAttrs) {
19 'default-excludes': true,
21 'embed-source': false,
22 variable: '__coverage__',
24 'preserve-comments': false,
25 'complete-copy': false,
26 'save-baseline': false,
27 'baseline-file': './coverage/coverage-baseline.json',
28 'include-all-sources': false,
37 'hook-run-in-context': false,
38 'post-require-hook': null,
39 'handle-sigint': false
47 excludes: [] // Currently list of files (root + path). For future, extend to patterns.
58 ret.reporting.watermarks = defaults.watermarks();
59 ret.reporting['report-config'] = defaults.defaultReportConfig();
61 if (includeBackCompatAttrs) {
62 ret.instrumentation['preload-sources'] = false;
68 function dasherize(word) {
69 return word.replace(CAMEL_PATTERN, function (match, lch, uch) {
70 return lch + '-' + uch.toLowerCase();
73 function isScalar(v) {
74 if (v === null) { return true; }
75 return v !== undefined && !Array.isArray(v) && typeof v !== 'object';
78 function isObject(v) {
79 return typeof v === 'object' && v !== null && !Array.isArray(v);
82 function mergeObjects(explicit, template) {
86 Object.keys(template).forEach(function (k) {
90 if (Array.isArray(v1)) {
91 ret[k] = Array.isArray(v2) && v2.length > 0 ? v2 : v1;
92 } else if (isObject(v1)) {
93 v2 = isObject(v2) ? v2 : {};
94 ret[k] = mergeObjects(v2, v1);
96 ret[k] = isScalar(v2) ? v2 : v1;
102 function mergeDefaults(explicit, implicit) {
103 return mergeObjects(explicit || {}, implicit);
106 function addMethods() {
107 var args = Array.prototype.slice.call(arguments),
110 args.forEach(function (arg) {
112 property = dasherize(arg);
113 cons.prototype[method] = function () {
114 return this.config[property];
120 * Object that returns instrumentation options
121 * @class InstrumentOptions
124 * @param config the instrumentation part of the config object
126 function InstrumentOptions(config) {
127 if (config['preload-sources']) {
128 console.error('The preload-sources option is deprecated, please use include-all-sources instead.');
129 config['include-all-sources'] = config['preload-sources'];
131 this.config = config;
135 * returns if default excludes should be turned on. Used by the `cover` command.
136 * @method defaultExcludes
137 * @return {Boolean} true if default excludes should be turned on
140 * returns if non-JS files should be copied during instrumentation. Used by the
141 * `instrument` command.
142 * @method completeCopy
143 * @return {Boolean} true if non-JS files should be copied
146 * returns if the source should be embedded in the instrumented code. Used by the
147 * `instrument` command.
148 * @method embedSource
149 * @return {Boolean} true if the source should be embedded in the instrumented code
152 * the coverage variable name to use. Used by the `instrument` command.
154 * @return {String} the coverage variable name to use
157 * returns if the output should be compact JS. Used by the `instrument` command.
159 * @return {Boolean} true if the output should be compact
162 * returns if comments should be preserved in the generated JS. Used by the
163 * `cover` and `instrument` commands.
164 * @method preserveComments
165 * @return {Boolean} true if comments should be preserved in the generated JS
168 * returns if a zero-coverage baseline file should be written as part of
169 * instrumentation. This allows reporting to display numbers for files that have
170 * no tests. Used by the `instrument` command.
171 * @method saveBaseline
172 * @return {Boolean} true if a baseline coverage file should be written.
175 * Sets the baseline coverage filename. Used by the `instrument` command.
176 * @method baselineFile
177 * @return {String} the name of the baseline coverage file.
180 * returns if the coverage filename should include the PID. Used by the `instrument` command.
182 * @return {Boolean} true to include pid in coverage filename.
186 addMethods(InstrumentOptions,
187 'extensions', 'defaultExcludes', 'completeCopy',
188 'embedSource', 'variable', 'compact', 'preserveComments',
189 'saveBaseline', 'baselineFile',
190 'includeAllSources', 'includePid');
193 * returns the root directory used by istanbul which is typically the root of the
194 * source tree. Used by the `cover` and `report` commands.
196 * @return {String} the root directory used by istanbul.
198 InstrumentOptions.prototype.root = function () { return path.resolve(this.config.root); };
200 * returns an array of fileset patterns that should be excluded for instrumentation.
201 * Used by the `instrument` and `cover` commands.
203 * @return {Array} an array of fileset patterns that should be excluded for
206 InstrumentOptions.prototype.excludes = function (excludeTests) {
208 if (this.defaultExcludes()) {
209 defs = [ '**/node_modules/**' ];
211 defs = defs.concat(['**/test/**', '**/tests/**']);
213 return defs.concat(this.config.excludes);
215 return this.config.excludes;
219 * Object that returns reporting options
220 * @class ReportingOptions
223 * @param config the reporting part of the config object
225 function ReportingOptions(config) {
226 this.config = config;
230 * returns the kind of information to be printed on the console. May be one
231 * of `summary`, `detail`, `both` or `none`. Used by the
234 * @return {String} the kind of information to print to the console at the end
235 * of the `cover` command execution.
238 * returns a list of reports that should be generated at the end of a run. Used
239 * by the `cover` and `report` commands.
241 * @return {Array} an array of reports that should be produced
244 * returns the directory under which reports should be generated. Used by the
245 * `cover` and `report` commands.
248 * @return {String} the directory under which reports should be generated.
251 * returns an object that has keys that are report format names and values that are objects
252 * containing detailed configuration for each format. Running `istanbul help config`
253 * will give you all the keys per report format that can be overridden.
254 * Used by the `cover` and `report` commands.
255 * @method reportConfig
256 * @return {Object} detailed report configuration per report format.
258 addMethods(ReportingOptions, 'print', 'reports', 'dir', 'reportConfig');
260 function isInvalidMark(v, key) {
261 var prefix = 'Watermark for [' + key + '] :';
263 if (v.length !== 2) {
264 return prefix + 'must be an array of length 2';
269 if (isNaN(v[0]) || isNaN(v[1])) {
270 return prefix + 'must have valid numbers';
272 if (v[0] < 0 || v[1] < 0) {
273 return prefix + 'must be positive numbers';
276 return prefix + 'cannot exceed 100';
279 return prefix + 'low must be less than high';
285 * returns the low and high watermarks to be used to designate whether coverage
286 * is `low`, `medium` or `high`. Statements, functions, branches and lines can
287 * have independent watermarks. These are respected by all reports
288 * that color for low, medium and high coverage. See the default configuration for exact syntax
289 * using `istanbul help config`. Used by the `cover` and `report` commands.
292 * @return {Object} an object containing low and high watermarks for statements,
293 * branches, functions and lines.
295 ReportingOptions.prototype.watermarks = function () {
296 var v = this.config.watermarks,
297 defs = defaults.watermarks(),
300 Object.keys(defs).forEach(function (k) {
301 var mark = v[k], //it will already be a non-zero length array because of the way the merge works
302 message = isInvalidMark(mark, k);
304 console.error(message);
314 * Object that returns hook options. Note that istanbul does not provide an
315 * option to hook `require`. This is always done by the `cover` command.
319 * @param config the hooks part of the config object
321 function HookOptions(config) {
322 this.config = config;
326 * returns if `vm.runInThisContext` needs to be hooked, in addition to the standard
327 * `require` hooks added by istanbul. This should be true for code that uses
328 * RequireJS for example. Used by the `cover` command.
329 * @method hookRunInContext
330 * @return {Boolean} true if `vm.runInThisContext` needs to be hooked for coverage
333 * returns a path to JS file or a dependent module that should be used for
334 * post-processing files after they have been required. See the `yui-istanbul` module for
335 * an example of a post-require hook. This particular hook modifies the yui loader when
336 * that file is required to add istanbul interceptors. Use by the `cover` command
338 * @method postRequireHook
339 * @return {String} a path to a JS file or the name of a node module that needs
340 * to be used as a `require` post-processor
343 * returns if istanbul needs to add a SIGINT (control-c, usually) handler to
344 * save coverage information. Useful for getting code coverage out of processes
345 * that run forever and need a SIGINT to terminate.
346 * @method handleSigint
347 * @return {Boolean} true if SIGINT needs to be hooked to write coverage information
350 addMethods(HookOptions, 'hookRunInContext', 'postRequireHook', 'handleSigint');
353 * represents the istanbul configuration and provides sub-objects that can
354 * return instrumentation, reporting and hook options respectively.
358 * var configObj = require('istanbul').config.loadFile();
360 * console.log(configObj.reporting.reports());
362 * @class Configuration
364 * @param {Object} obj the base object to use as the configuration
365 * @param {Object} overrides optional - override attributes that are merged into
369 function Configuration(obj, overrides) {
371 var config = mergeDefaults(obj, defaultConfig(true));
372 if (isObject(overrides)) {
373 config = mergeDefaults(overrides, config);
375 if (config.verbose) {
376 console.error('Using configuration');
377 console.error('-------------------');
378 console.error(yaml.safeDump(config, { indent: 4, flowLevel: 3 }));
379 console.error('-------------------\n');
381 this.verbose = config.verbose;
382 this.instrumentation = new InstrumentOptions(config.instrumentation);
383 this.reporting = new ReportingOptions(config.reporting);
384 this.hooks = new HookOptions(config.hooks);
385 this.check = config.check; // Pass raw config sub-object.
389 * true if verbose logging is required
394 * instrumentation options
395 * @property instrumentation
396 * @type InstrumentOptions
400 * @property reporting
401 * @type ReportingOptions
410 function loadFile(file, overrides) {
411 var defaultConfigFile = path.resolve('.istanbul.yml'),
415 if (!existsSync(file)) {
416 throw new Error('Invalid configuration file specified:' + file);
419 if (existsSync(defaultConfigFile)) {
420 file = defaultConfigFile;
425 console.error('Loading config: ' + file);
426 configObject = file.match(YML_PATTERN) ?
427 yaml.safeLoad(fs.readFileSync(file, 'utf8'), { filename: file }) :
428 require(path.resolve(file));
431 return new Configuration(configObject, overrides);
434 function loadObject(obj, overrides) {
435 return new Configuration(obj, overrides);
439 * methods to load the configuration object.
443 * var config = require('istanbul').config,
444 * configObj = config.loadFile();
446 * console.log(configObj.reporting.reports());
454 * loads the specified configuration file with optional overrides. Throws
455 * when a file is specified and it is not found.
458 * @param {String} file the file to load. If falsy, the default config file, if present, is loaded.
459 * If not a default config is used.
460 * @param {Object} overrides - an object with override keys that are merged into the
461 * config object loaded
462 * @return {Configuration} the config object with overrides applied
466 * loads the specified configuration object with optional overrides.
469 * @param {Object} obj the object to use as the base configuration.
470 * @param {Object} overrides - an object with override keys that are merged into the
472 * @return {Configuration} the config object with overrides applied
474 loadObject: loadObject,
476 * returns the default configuration object. Note that this is a plain object
477 * and not a `Configuration` instance.
478 * @method defaultConfig
480 * @return {Object} an object that represents the default config
482 defaultConfig: defaultConfig