1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | var 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 |
|
17 | function isAbsolute(file) {
|
18 | if (path.isAbsolute) {
|
19 | return path.isAbsolute(file);
|
20 | }
|
21 |
|
22 | return path.resolve(file) === path.normalize(file);
|
23 | }
|
24 |
|
25 | function CheckCoverageCommand() {
|
26 | Command.call(this);
|
27 | }
|
28 |
|
29 | function removeFiles(covObj, root, files) {
|
30 | var filesObj = {},
|
31 | obj = {};
|
32 |
|
33 |
|
34 | files.forEach(function (file) {
|
35 | filesObj[file] = true;
|
36 | });
|
37 |
|
38 | Object.keys(covObj).forEach(function (key) {
|
39 |
|
40 | var excludeKey = isAbsolute(key) ? path.relative(root, key) : key;
|
41 |
|
42 | excludeKey = path.normalize(excludeKey);
|
43 | if (filesObj[excludeKey] !== true) {
|
44 | obj[key] = covObj[key];
|
45 | }
|
46 | });
|
47 |
|
48 | return obj;
|
49 | }
|
50 |
|
51 | CheckCoverageCommand.TYPE = 'check-coverage';
|
52 | util.inherits(CheckCoverageCommand, Command);
|
53 |
|
54 | Command.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 |
|
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 |
|
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 |
|
193 | module.exports = CheckCoverageCommand;
|
194 |
|
195 |
|