UNPKG

7.67 kBJavaScriptView Raw
1/*
2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5
6var nopt = require('nopt'),
7 path = require('path'),
8 fs = require('fs'),
9 Collector = require('../collector'),
10 formatOption = require('../util/help-formatter').formatOption,
11 util = require('util'),
12 utils = require('../object-utils'),
13 filesFor = require('../util/file-matcher').filesFor,
14 Command = require('./index'),
15 configuration = require('../config');
16
17function isAbsolute(file) {
18 if (path.isAbsolute) {
19 return path.isAbsolute(file);
20 }
21
22 return path.resolve(file) === path.normalize(file);
23}
24
25function CheckCoverageCommand() {
26 Command.call(this);
27}
28
29function removeFiles(covObj, root, files) {
30 var filesObj = {},
31 obj = {};
32
33 // Create lookup table.
34 files.forEach(function (file) {
35 filesObj[file] = true;
36 });
37
38 Object.keys(covObj).forEach(function (key) {
39 // Exclude keys will always be relative, but covObj keys can be absolute or relative
40 var excludeKey = isAbsolute(key) ? path.relative(root, key) : key;
41 // Also normalize for files that start with `./`, etc.
42 excludeKey = path.normalize(excludeKey);
43 if (filesObj[excludeKey] !== true) {
44 obj[key] = covObj[key];
45 }
46 });
47
48 return obj;
49}
50
51CheckCoverageCommand.TYPE = 'check-coverage';
52util.inherits(CheckCoverageCommand, Command);
53
54Command.mix(CheckCoverageCommand, {
55 synopsis: function () {
56 return "checks overall/per-file coverage against thresholds from coverage JSON files. Exits 1 if thresholds are not met, 0 otherwise";
57 },
58
59 usage: function () {
60 console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> [<include-pattern>]\n\nOptions are:\n\n' +
61 [
62 formatOption('--statements <threshold>', 'global statement coverage threshold'),
63 formatOption('--functions <threshold>', 'global function coverage threshold'),
64 formatOption('--branches <threshold>', 'global branch coverage threshold'),
65 formatOption('--lines <threshold>', 'global line coverage threshold')
66 ].join('\n\n') + '\n');
67
68 console.error('\n\n');
69
70 console.error('Thresholds, when specified as a positive number are taken to be the minimum percentage required.');
71 console.error('When a threshold is specified as a negative number it represents the maximum number of uncovered entities allowed.\n');
72 console.error('For example, --statements 90 implies minimum statement coverage is 90%.');
73 console.error(' --statements -10 implies that no more than 10 uncovered statements are allowed\n');
74 console.error('Per-file thresholds can be specified via a configuration file.\n');
75 console.error('<include-pattern> is a glob pattern that can be used to select one or more coverage files ' +
76 'for merge. This defaults to "**/coverage*.json"');
77
78 console.error('\n');
79 },
80
81 run: function (args, callback) {
82
83 var template = {
84 config: path,
85 root: path,
86 statements: Number,
87 lines: Number,
88 branches: Number,
89 functions: Number,
90 verbose: Boolean
91 },
92 opts = nopt(template, { v : '--verbose' }, args, 0),
93 // Translate to config opts.
94 config = configuration.loadFile(opts.config, {
95 verbose: opts.verbose,
96 check: {
97 global: {
98 statements: opts.statements,
99 lines: opts.lines,
100 branches: opts.branches,
101 functions: opts.functions
102 }
103 }
104 }),
105 includePattern = '**/coverage*.json',
106 root,
107 collector = new Collector(),
108 errors = [];
109
110 if (opts.argv.remain.length > 0) {
111 includePattern = opts.argv.remain[0];
112 }
113
114 root = opts.root || process.cwd();
115 filesFor({
116 root: root,
117 includes: [ includePattern ]
118 }, function (err, files) {
119 if (err) { throw err; }
120 if (files.length === 0) {
121 return callback('ERROR: No coverage files found.');
122 }
123 files.forEach(function (file) {
124 var coverageObject = JSON.parse(fs.readFileSync(file, 'utf8'));
125 collector.add(coverageObject);
126 });
127 var thresholds = {
128 global: {
129 statements: config.check.global.statements || 0,
130 branches: config.check.global.branches || 0,
131 lines: config.check.global.lines || 0,
132 functions: config.check.global.functions || 0,
133 excludes: config.check.global.excludes || []
134 },
135 each: {
136 statements: config.check.each.statements || 0,
137 branches: config.check.each.branches || 0,
138 lines: config.check.each.lines || 0,
139 functions: config.check.each.functions || 0,
140 excludes: config.check.each.excludes || []
141 }
142 },
143 rawCoverage = collector.getFinalCoverage(),
144 globalResults = utils.summarizeCoverage(removeFiles(rawCoverage, root, thresholds.global.excludes)),
145 eachResults = removeFiles(rawCoverage, root, thresholds.each.excludes);
146
147 // Summarize per-file results and mutate original results.
148 Object.keys(eachResults).forEach(function (key) {
149 eachResults[key] = utils.summarizeFileCoverage(eachResults[key]);
150 });
151
152 if (config.verbose) {
153 console.log('Compare actuals against thresholds');
154 console.log(JSON.stringify({ global: globalResults, each: eachResults, thresholds: thresholds }, undefined, 4));
155 }
156
157 function check(name, thresholds, actuals) {
158 [
159 "statements",
160 "branches",
161 "lines",
162 "functions"
163 ].forEach(function (key) {
164 var actual = actuals[key].pct,
165 actualUncovered = actuals[key].total - actuals[key].covered,
166 threshold = thresholds[key];
167
168 if (threshold < 0) {
169 if (threshold * -1 < actualUncovered) {
170 errors.push('ERROR: Uncovered count for ' + key + ' (' + actualUncovered +
171 ') exceeds ' + name + ' threshold (' + -1 * threshold + ')');
172 }
173 } else {
174 if (actual < threshold) {
175 errors.push('ERROR: Coverage for ' + key + ' (' + actual +
176 '%) does not meet ' + name + ' threshold (' + threshold + '%)');
177 }
178 }
179 });
180 }
181
182 check("global", thresholds.global, globalResults);
183
184 Object.keys(eachResults).forEach(function (key) {
185 check("per-file" + " (" + key + ") ", thresholds.each, eachResults[key]);
186 });
187
188 return callback(errors.length === 0 ? null : errors.join("\n"));
189 });
190 }
191});
192
193module.exports = CheckCoverageCommand;
194
195