| /* |
| Copyright (c) 2012, Yahoo! Inc. All rights reserved. |
| Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. |
| */ |
| |
| var nopt = require('nopt'), |
| path = require('path'), |
| fs = require('fs'), |
| Collector = require('../collector'), |
| formatOption = require('../util/help-formatter').formatOption, |
| util = require('util'), |
| utils = require('../object-utils'), |
| filesFor = require('../util/file-matcher').filesFor, |
| Command = require('./index'), |
| configuration = require('../config'); |
| |
| function isAbsolute(file) { |
| if (path.isAbsolute) { |
| return path.isAbsolute(file); |
| } |
| |
| return path.resolve(file) === path.normalize(file); |
| } |
| |
| function CheckCoverageCommand() { |
| Command.call(this); |
| } |
| |
| function removeFiles(covObj, root, files) { |
| var filesObj = {}, |
| obj = {}; |
| |
| // Create lookup table. |
| files.forEach(function (file) { |
| filesObj[file] = true; |
| }); |
| |
| Object.keys(covObj).forEach(function (key) { |
| // Exclude keys will always be relative, but covObj keys can be absolute or relative |
| var excludeKey = isAbsolute(key) ? path.relative(root, key) : key; |
| // Also normalize for files that start with `./`, etc. |
| excludeKey = path.normalize(excludeKey); |
| if (filesObj[excludeKey] !== true) { |
| obj[key] = covObj[key]; |
| } |
| }); |
| |
| return obj; |
| } |
| |
| CheckCoverageCommand.TYPE = 'check-coverage'; |
| util.inherits(CheckCoverageCommand, Command); |
| |
| Command.mix(CheckCoverageCommand, { |
| synopsis: function () { |
| return "checks overall/per-file coverage against thresholds from coverage JSON files. Exits 1 if thresholds are not met, 0 otherwise"; |
| }, |
| |
| usage: function () { |
| console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> [<include-pattern>]\n\nOptions are:\n\n' + |
| [ |
| formatOption('--statements <threshold>', 'global statement coverage threshold'), |
| formatOption('--functions <threshold>', 'global function coverage threshold'), |
| formatOption('--branches <threshold>', 'global branch coverage threshold'), |
| formatOption('--lines <threshold>', 'global line coverage threshold') |
| ].join('\n\n') + '\n'); |
| |
| console.error('\n\n'); |
| |
| console.error('Thresholds, when specified as a positive number are taken to be the minimum percentage required.'); |
| console.error('When a threshold is specified as a negative number it represents the maximum number of uncovered entities allowed.\n'); |
| console.error('For example, --statements 90 implies minimum statement coverage is 90%.'); |
| console.error(' --statements -10 implies that no more than 10 uncovered statements are allowed\n'); |
| console.error('Per-file thresholds can be specified via a configuration file.\n'); |
| console.error('<include-pattern> is a glob pattern that can be used to select one or more coverage files ' + |
| 'for merge. This defaults to "**/coverage*.json"'); |
| |
| console.error('\n'); |
| }, |
| |
| run: function (args, callback) { |
| |
| var template = { |
| config: path, |
| root: path, |
| statements: Number, |
| lines: Number, |
| branches: Number, |
| functions: Number, |
| verbose: Boolean |
| }, |
| opts = nopt(template, { v : '--verbose' }, args, 0), |
| // Translate to config opts. |
| config = configuration.loadFile(opts.config, { |
| verbose: opts.verbose, |
| check: { |
| global: { |
| statements: opts.statements, |
| lines: opts.lines, |
| branches: opts.branches, |
| functions: opts.functions |
| } |
| } |
| }), |
| includePattern = '**/coverage*.json', |
| root, |
| collector = new Collector(), |
| errors = []; |
| |
| if (opts.argv.remain.length > 0) { |
| includePattern = opts.argv.remain[0]; |
| } |
| |
| root = opts.root || process.cwd(); |
| filesFor({ |
| root: root, |
| includes: [ includePattern ] |
| }, function (err, files) { |
| if (err) { throw err; } |
| if (files.length === 0) { |
| return callback('ERROR: No coverage files found.'); |
| } |
| files.forEach(function (file) { |
| var coverageObject = JSON.parse(fs.readFileSync(file, 'utf8')); |
| collector.add(coverageObject); |
| }); |
| var thresholds = { |
| global: { |
| statements: config.check.global.statements || 0, |
| branches: config.check.global.branches || 0, |
| lines: config.check.global.lines || 0, |
| functions: config.check.global.functions || 0, |
| excludes: config.check.global.excludes || [] |
| }, |
| each: { |
| statements: config.check.each.statements || 0, |
| branches: config.check.each.branches || 0, |
| lines: config.check.each.lines || 0, |
| functions: config.check.each.functions || 0, |
| excludes: config.check.each.excludes || [] |
| } |
| }, |
| rawCoverage = collector.getFinalCoverage(), |
| globalResults = utils.summarizeCoverage(removeFiles(rawCoverage, root, thresholds.global.excludes)), |
| eachResults = removeFiles(rawCoverage, root, thresholds.each.excludes); |
| |
| // Summarize per-file results and mutate original results. |
| Object.keys(eachResults).forEach(function (key) { |
| eachResults[key] = utils.summarizeFileCoverage(eachResults[key]); |
| }); |
| |
| if (config.verbose) { |
| console.log('Compare actuals against thresholds'); |
| console.log(JSON.stringify({ global: globalResults, each: eachResults, thresholds: thresholds }, undefined, 4)); |
| } |
| |
| function check(name, thresholds, actuals) { |
| [ |
| "statements", |
| "branches", |
| "lines", |
| "functions" |
| ].forEach(function (key) { |
| var actual = actuals[key].pct, |
| actualUncovered = actuals[key].total - actuals[key].covered, |
| threshold = thresholds[key]; |
| |
| if (threshold < 0) { |
| if (threshold * -1 < actualUncovered) { |
| errors.push('ERROR: Uncovered count for ' + key + ' (' + actualUncovered + |
| ') exceeds ' + name + ' threshold (' + -1 * threshold + ')'); |
| } |
| } else { |
| if (actual < threshold) { |
| errors.push('ERROR: Coverage for ' + key + ' (' + actual + |
| '%) does not meet ' + name + ' threshold (' + threshold + '%)'); |
| } |
| } |
| }); |
| } |
| |
| check("global", thresholds.global, globalResults); |
| |
| Object.keys(eachResults).forEach(function (key) { |
| check("per-file" + " (" + key + ") ", thresholds.each, eachResults[key]); |
| }); |
| |
| return callback(errors.length === 0 ? null : errors.join("\n")); |
| }); |
| } |
| }); |
| |
| module.exports = CheckCoverageCommand; |
| |
| |