/*1Copyright (c) 2013, Yahoo! Inc. All rights reserved.2Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.3*/4var path = require('path'),5fs = require('fs'),6existsSync = fs.existsSync || path.existsSync,7CAMEL_PATTERN = /([a-z])([A-Z])/g,8YML_PATTERN = /\.ya?ml$/,9yaml = require('js-yaml'),10defaults = require('./report/common/defaults');1112function defaultConfig(includeBackCompatAttrs) {13var ret = {14verbose: false,15instrumentation: {16root: '.',17'default-excludes': true,18excludes: [],19'embed-source': false,20variable: '__coverage__',21compact: true,22'preserve-comments': false,23'complete-copy': false,24'save-baseline': false,25'baseline-file': './coverage/coverage-baseline.json',26'include-all-sources': false,27'include-pid': false28},29reporting: {30print: 'summary',31reports: [ 'lcov' ],32dir: './coverage'33},34hooks: {35'hook-run-in-context': false,36'post-require-hook': null,37'handle-sigint': false38},39check: {40global: {41statements: 0,42lines: 0,43branches: 0,44functions: 0,45excludes: [] // Currently list of files (root + path). For future, extend to patterns.46},47each: {48statements: 0,49lines: 0,50branches: 0,51functions: 0,52excludes: []53}54}55};56ret.reporting.watermarks = defaults.watermarks();57ret.reporting['report-config'] = defaults.defaultReportConfig();5859if (includeBackCompatAttrs) {60ret.instrumentation['preload-sources'] = false;61}6263return ret;64}6566function dasherize(word) {67return word.replace(CAMEL_PATTERN, function (match, lch, uch) {68return lch + '-' + uch.toLowerCase();69});70}71function isScalar(v) {72if (v === null) { return true; }73return v !== undefined && !Array.isArray(v) && typeof v !== 'object';74}7576function isObject(v) {77return typeof v === 'object' && v !== null && !Array.isArray(v);78}7980function mergeObjects(explicit, template) {8182var ret = {};8384Object.keys(template).forEach(function (k) {85var v1 = template[k],86v2 = explicit[k];8788if (Array.isArray(v1)) {89ret[k] = Array.isArray(v2) && v2.length > 0 ? v2 : v1;90} else if (isObject(v1)) {91v2 = isObject(v2) ? v2 : {};92ret[k] = mergeObjects(v2, v1);93} else {94ret[k] = isScalar(v2) ? v2 : v1;95}96});97return ret;98}99100function mergeDefaults(explicit, implicit) {101return mergeObjects(explicit || {}, implicit);102}103104function addMethods() {105var args = Array.prototype.slice.call(arguments),106cons = args.shift();107108args.forEach(function (arg) {109var method = arg,110property = dasherize(arg);111cons.prototype[method] = function () {112return this.config[property];113};114});115}116117/**118* Object that returns instrumentation options119* @class InstrumentOptions120* @module config121* @constructor122* @param config the instrumentation part of the config object123*/124function InstrumentOptions(config) {125if (config['preload-sources']) {126console.error('The preload-sources option is deprecated, please use include-all-sources instead.');127config['include-all-sources'] = config['preload-sources'];128}129this.config = config;130}131132/**133* returns if default excludes should be turned on. Used by the `cover` command.134* @method defaultExcludes135* @return {Boolean} true if default excludes should be turned on136*/137/**138* returns if non-JS files should be copied during instrumentation. Used by the139* `instrument` command.140* @method completeCopy141* @return {Boolean} true if non-JS files should be copied142*/143/**144* returns if the source should be embedded in the instrumented code. Used by the145* `instrument` command.146* @method embedSource147* @return {Boolean} true if the source should be embedded in the instrumented code148*/149/**150* the coverage variable name to use. Used by the `instrument` command.151* @method variable152* @return {String} the coverage variable name to use153*/154/**155* returns if the output should be compact JS. Used by the `instrument` command.156* @method compact157* @return {Boolean} true if the output should be compact158*/159/**160* returns if comments should be preserved in the generated JS. Used by the161* `cover` and `instrument` commands.162* @method preserveComments163* @return {Boolean} true if comments should be preserved in the generated JS164*/165/**166* returns if a zero-coverage baseline file should be written as part of167* instrumentation. This allows reporting to display numbers for files that have168* no tests. Used by the `instrument` command.169* @method saveBaseline170* @return {Boolean} true if a baseline coverage file should be written.171*/172/**173* Sets the baseline coverage filename. Used by the `instrument` command.174* @method baselineFile175* @return {String} the name of the baseline coverage file.176*/177/**178* returns if the coverage filename should include the PID. Used by the `instrument` command.179* @method includePid180* @return {Boolean} true to include pid in coverage filename.181*/182183184addMethods(InstrumentOptions,185'defaultExcludes', 'completeCopy',186'embedSource', 'variable', 'compact', 'preserveComments',187'saveBaseline', 'baselineFile',188'includeAllSources', 'includePid');189190/**191* returns the root directory used by istanbul which is typically the root of the192* source tree. Used by the `cover` and `report` commands.193* @method root194* @return {String} the root directory used by istanbul.195*/196InstrumentOptions.prototype.root = function () { return path.resolve(this.config.root); };197/**198* returns an array of fileset patterns that should be excluded for instrumentation.199* Used by the `instrument` and `cover` commands.200* @method excludes201* @return {Array} an array of fileset patterns that should be excluded for202* instrumentation.203*/204InstrumentOptions.prototype.excludes = function (excludeTests) {205var defs;206if (this.defaultExcludes()) {207defs = [ '**/node_modules/**' ];208if (excludeTests) {209defs = defs.concat(['**/test/**', '**/tests/**']);210}211return defs.concat(this.config.excludes);212}213return this.config.excludes;214};215216/**217* Object that returns reporting options218* @class ReportingOptions219* @module config220* @constructor221* @param config the reporting part of the config object222*/223function ReportingOptions(config) {224this.config = config;225}226227/**228* returns the kind of information to be printed on the console. May be one229* of `summary`, `detail`, `both` or `none`. Used by the230* `cover` command.231* @method print232* @return {String} the kind of information to print to the console at the end233* of the `cover` command execution.234*/235/**236* returns a list of reports that should be generated at the end of a run. Used237* by the `cover` and `report` commands.238* @method reports239* @return {Array} an array of reports that should be produced240*/241/**242* returns the directory under which reports should be generated. Used by the243* `cover` and `report` commands.244*245* @method dir246* @return {String} the directory under which reports should be generated.247*/248/**249* returns an object that has keys that are report format names and values that are objects250* containing detailed configuration for each format. Running `istanbul help config`251* will give you all the keys per report format that can be overridden.252* Used by the `cover` and `report` commands.253* @method reportConfig254* @return {Object} detailed report configuration per report format.255*/256addMethods(ReportingOptions, 'print', 'reports', 'dir', 'reportConfig');257258function isInvalidMark(v, key) {259var prefix = 'Watermark for [' + key + '] :';260261if (v.length !== 2) {262return prefix + 'must be an array of length 2';263}264v[0] = Number(v[0]);265v[1] = Number(v[1]);266267if (isNaN(v[0]) || isNaN(v[1])) {268return prefix + 'must have valid numbers';269}270if (v[0] < 0 || v[1] < 0) {271return prefix + 'must be positive numbers';272}273if (v[1] > 100) {274return prefix + 'cannot exceed 100';275}276if (v[1] <= v[0]) {277return prefix + 'low must be less than high';278}279return null;280}281282/**283* returns the low and high watermarks to be used to designate whether coverage284* is `low`, `medium` or `high`. Statements, functions, branches and lines can285* have independent watermarks. These are respected by all reports286* that color for low, medium and high coverage. See the default configuration for exact syntax287* using `istanbul help config`. Used by the `cover` and `report` commands.288*289* @method watermarks290* @return {Object} an object containing low and high watermarks for statements,291* branches, functions and lines.292*/293ReportingOptions.prototype.watermarks = function () {294var v = this.config.watermarks,295defs = defaults.watermarks(),296ret = {};297298Object.keys(defs).forEach(function (k) {299var mark = v[k], //it will already be a non-zero length array because of the way the merge works300message = isInvalidMark(mark, k);301if (message) {302console.error(message);303ret[k] = defs[k];304} else {305ret[k] = mark;306}307});308return ret;309};310311/**312* Object that returns hook options. Note that istanbul does not provide an313* option to hook `require`. This is always done by the `cover` command.314* @class HookOptions315* @module config316* @constructor317* @param config the hooks part of the config object318*/319function HookOptions(config) {320this.config = config;321}322323/**324* returns if `vm.runInThisContext` needs to be hooked, in addition to the standard325* `require` hooks added by istanbul. This should be true for code that uses326* RequireJS for example. Used by the `cover` command.327* @method hookRunInContext328* @return {Boolean} true if `vm.runInThisContext` needs to be hooked for coverage329*/330/**331* returns a path to JS file or a dependent module that should be used for332* post-processing files after they have been required. See the `yui-istanbul` module for333* an example of a post-require hook. This particular hook modifies the yui loader when334* that file is required to add istanbul interceptors. Use by the `cover` command335*336* @method postRequireHook337* @return {String} a path to a JS file or the name of a node module that needs338* to be used as a `require` post-processor339*/340/**341* returns if istanbul needs to add a SIGINT (control-c, usually) handler to342* save coverage information. Useful for getting code coverage out of processes343* that run forever and need a SIGINT to terminate.344* @method handleSigint345* @return {Boolean} true if SIGINT needs to be hooked to write coverage information346*/347348addMethods(HookOptions, 'hookRunInContext', 'postRequireHook', 'handleSigint');349350/**351* represents the istanbul configuration and provides sub-objects that can352* return instrumentation, reporting and hook options respectively.353* Usage354* -----355*356* var configObj = require('istanbul').config.loadFile();357*358* console.log(configObj.reporting.reports());359*360* @class Configuration361* @module config362* @param {Object} obj the base object to use as the configuration363* @param {Object} overrides optional - override attributes that are merged into364* the base config365* @constructor366*/367function Configuration(obj, overrides) {368369var config = mergeDefaults(obj, defaultConfig(true));370if (isObject(overrides)) {371config = mergeDefaults(overrides, config);372}373if (config.verbose) {374console.error('Using configuration');375console.error('-------------------');376console.error(yaml.safeDump(config, { indent: 4, flowLevel: 3 }));377console.error('-------------------\n');378}379this.verbose = config.verbose;380this.instrumentation = new InstrumentOptions(config.instrumentation);381this.reporting = new ReportingOptions(config.reporting);382this.hooks = new HookOptions(config.hooks);383this.check = config.check; // Pass raw config sub-object.384}385386/**387* true if verbose logging is required388* @property verbose389* @type Boolean390*/391/**392* instrumentation options393* @property instrumentation394* @type InstrumentOptions395*/396/**397* reporting options398* @property reporting399* @type ReportingOptions400*/401/**402* hook options403* @property hooks404* @type HookOptions405*/406407408function loadFile(file, overrides) {409var defaultConfigFile = path.resolve('.istanbul.yml'),410configObject;411412if (file) {413if (!existsSync(file)) {414throw new Error('Invalid configuration file specified:' + file);415}416} else {417if (existsSync(defaultConfigFile)) {418file = defaultConfigFile;419}420}421422if (file) {423console.error('Loading config: ' + file);424configObject = file.match(YML_PATTERN) ?425yaml.safeLoad(fs.readFileSync(file, 'utf8'), { filename: file }) :426require(path.resolve(file));427}428429return new Configuration(configObject, overrides);430}431432function loadObject(obj, overrides) {433return new Configuration(obj, overrides);434}435436/**437* methods to load the configuration object.438* Usage439* -----440*441* var config = require('istanbul').config,442* configObj = config.loadFile();443*444* console.log(configObj.reporting.reports());445*446* @class Config447* @module main448* @static449*/450module.exports = {451/**452* loads the specified configuration file with optional overrides. Throws453* when a file is specified and it is not found.454* @method loadFile455* @static456* @param {String} file the file to load. If falsy, the default config file, if present, is loaded.457* If not a default config is used.458* @param {Object} overrides - an object with override keys that are merged into the459* config object loaded460* @return {Configuration} the config object with overrides applied461*/462loadFile: loadFile,463/**464* loads the specified configuration object with optional overrides.465* @method loadObject466* @static467* @param {Object} obj the object to use as the base configuration.468* @param {Object} overrides - an object with override keys that are merged into the469* config object470* @return {Configuration} the config object with overrides applied471*/472loadObject: loadObject,473/**474* returns the default configuration object. Note that this is a plain object475* and not a `Configuration` instance.476* @method defaultConfig477* @static478* @return {Object} an object that represents the default config479*/480defaultConfig: defaultConfig481};482483484