196 lines
7.5 KiB
JavaScript
196 lines
7.5 KiB
JavaScript
|
/*
|
||
|
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;
|
||
|
|
||
|
|