UNPKG

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