UNPKG

8.63 kBJavaScriptView Raw
1// Coverage Reporter
2// Part of this code is based on [1], which is licensed under the New BSD License.
3// For more information see the See the accompanying LICENSE-istanbul file for terms.
4//
5// [1]: https://github.com/gotwarlost/istanbul/blob/master/lib/command/check-coverage.js
6// =====================
7//
8// Generates the report
9
10// Dependencies
11// ------------
12
13var path = require('path')
14var istanbul = require('istanbul')
15var minimatch = require('minimatch')
16
17var globalSourceCache = require('./source-cache')
18var coverageMap = require('./coverage-map')
19var SourceCacheStore = require('./source-cache-store')
20
21function isAbsolute (file) {
22 if (path.isAbsolute) {
23 return path.isAbsolute(file)
24 }
25
26 return path.resolve(file) === path.normalize(file)
27}
28
29// TODO(vojta): inject only what required (config.basePath, config.coverageReporter)
30var CoverageReporter = function (rootConfig, helper, logger, emitter) {
31 var _ = helper._
32 var log = logger.create('coverage')
33
34 // Instance variables
35 // ------------------
36
37 this.adapters = []
38
39 // Options
40 // -------
41
42 var config = rootConfig.coverageReporter || {}
43 var basePath = rootConfig.basePath
44 var reporters = config.reporters
45 var sourceCache = globalSourceCache.get(basePath)
46 var includeAllSources = config.includeAllSources === true
47
48 if (config.watermarks) {
49 config.watermarks = helper.merge({}, istanbul.config.defaultConfig().reporting.watermarks, config.watermarks)
50 }
51
52 if (!helper.isDefined(reporters)) {
53 reporters = [config]
54 }
55
56 var collectors
57 var pendingFileWritings = 0
58 var fileWritingFinished = function () {}
59
60 function writeReport (reporter, collector) {
61 try {
62 reporter.writeReport(collector, true)
63 } catch (e) {
64 log.error(e)
65 }
66
67 --pendingFileWritings
68 }
69
70 function disposeCollectors () {
71 if (pendingFileWritings <= 0) {
72 _.forEach(collectors, function (collector) {
73 collector.dispose()
74 })
75
76 fileWritingFinished()
77 }
78 }
79
80 function normalize (key) {
81 // Exclude keys will always be relative, but covObj keys can be absolute or relative
82 var excludeKey = isAbsolute(key) ? path.relative(basePath, key) : key
83 // Also normalize for files that start with `./`, etc.
84 excludeKey = path.normalize(excludeKey)
85
86 return excludeKey
87 }
88
89 function removeFiles (covObj, patterns) {
90 var obj = {}
91
92 Object.keys(covObj).forEach(function (key) {
93 // Do any patterns match the resolved key
94 var found = patterns.some(function (pattern) {
95 return minimatch(normalize(key), pattern, {dot: true})
96 })
97
98 // if no patterns match, keep the key
99 if (!found) {
100 obj[key] = covObj[key]
101 }
102 })
103
104 return obj
105 }
106
107 function overrideThresholds (key, overrides) {
108 var thresholds = {}
109
110 // First match wins
111 Object.keys(overrides).some(function (pattern) {
112 if (minimatch(normalize(key), pattern, {dot: true})) {
113 thresholds = overrides[pattern]
114 return true
115 }
116 })
117
118 return thresholds
119 }
120
121 function checkCoverage (browser, collector) {
122 var defaultThresholds = {
123 global: {
124 statements: 0,
125 branches: 0,
126 lines: 0,
127 functions: 0,
128 excludes: []
129 },
130 each: {
131 statements: 0,
132 branches: 0,
133 lines: 0,
134 functions: 0,
135 excludes: [],
136 overrides: {}
137 }
138 }
139
140 var thresholds = helper.merge({}, defaultThresholds, config.check)
141
142 var rawCoverage = collector.getFinalCoverage()
143 var globalResults = istanbul.utils.summarizeCoverage(removeFiles(rawCoverage, thresholds.global.excludes))
144 var eachResults = removeFiles(rawCoverage, thresholds.each.excludes)
145
146 // Summarize per-file results and mutate original results.
147 Object.keys(eachResults).forEach(function (key) {
148 eachResults[key] = istanbul.utils.summarizeFileCoverage(eachResults[key])
149 })
150
151 var coverageFailed = false
152
153 function check (name, thresholds, actuals) {
154 var keys = [
155 'statements',
156 'branches',
157 'lines',
158 'functions'
159 ]
160
161 keys.forEach(function (key) {
162 var actual = actuals[key].pct
163 var actualUncovered = actuals[key].total - actuals[key].covered
164 var threshold = thresholds[key]
165
166 if (threshold < 0) {
167 if (threshold * -1 < actualUncovered) {
168 coverageFailed = true
169 log.error(browser.name + ': Uncovered count for ' + key + ' (' + actualUncovered +
170 ') exceeds ' + name + ' threshold (' + -1 * threshold + ')')
171 }
172 } else {
173 if (actual < threshold) {
174 coverageFailed = true
175 log.error(browser.name + ': 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 var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides))
186 check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key])
187 })
188
189 return coverageFailed
190 }
191
192 // Generate the output directory from the `coverageReporter.dir` and
193 // `coverageReporter.subdir` options.
194 function generateOutputDir (browserName, dir, subdir) {
195 dir = dir || 'coverage'
196 subdir = subdir || browserName
197
198 if (_.isFunction(subdir)) {
199 subdir = subdir(browserName)
200 }
201
202 return path.join(dir, subdir)
203 }
204
205 this.onRunStart = function (browsers) {
206 collectors = Object.create(null)
207
208 // TODO(vojta): remove once we don't care about Karma 0.10
209 if (browsers) {
210 browsers.forEach(this.onBrowserStart.bind(this))
211 }
212 }
213
214 this.onBrowserStart = function (browser) {
215 collectors[browser.id] = new istanbul.Collector()
216
217 if (!includeAllSources) return
218
219 collectors[browser.id].add(coverageMap.get())
220 }
221
222 this.onBrowserComplete = function (browser, result) {
223 var collector = collectors[browser.id]
224
225 if (!collector) return
226 if (!result || !result.coverage) return
227
228 collector.add(result.coverage)
229 }
230
231 this.onSpecComplete = function (browser, result) {
232 if (!result.coverage) return
233
234 collectors[browser.id].add(result.coverage)
235 }
236
237 this.onRunComplete = function (browsers, results) {
238 var checkedCoverage = {}
239
240 reporters.forEach(function (reporterConfig) {
241 browsers.forEach(function (browser) {
242 var collector = collectors[browser.id]
243
244 if (!collector) {
245 return
246 }
247
248 // If config.check is defined, check coverage levels for each browser
249 if (config.hasOwnProperty('check') && !checkedCoverage[browser.id]) {
250 checkedCoverage[browser.id] = true
251 var coverageFailed = checkCoverage(browser, collector)
252 if (coverageFailed) {
253 if (results) {
254 results.exitCode = 1
255 }
256 }
257 }
258
259 pendingFileWritings++
260
261 var mainDir = reporterConfig.dir || config.dir
262 var subDir = reporterConfig.subdir || config.subdir
263 var simpleOutputDir = generateOutputDir(browser.name, mainDir, subDir)
264 var resolvedOutputDir = path.resolve(basePath, simpleOutputDir)
265
266 var outputDir = helper.normalizeWinPath(resolvedOutputDir)
267 var sourceStore = _.isEmpty(sourceCache) ? null : new SourceCacheStore({
268 sourceCache: sourceCache
269 })
270 var options = helper.merge({
271 sourceStore: sourceStore
272 }, config, reporterConfig, {
273 dir: outputDir,
274 browser: browser,
275 emitter: emitter
276 })
277 var reporter = istanbul.Report.create(reporterConfig.type || 'html', options)
278
279 // If reporting to console or in-memory skip directory creation
280 var toDisk = !reporterConfig.type || !reporterConfig.type.match(/^(text|text-summary|in-memory)$/)
281 var hasNoFile = _.isUndefined(reporterConfig.file)
282
283 if (!toDisk && hasNoFile) {
284 writeReport(reporter, collector)
285 return
286 }
287
288 helper.mkdirIfNotExists(outputDir, function () {
289 log.debug('Writing coverage to %s', outputDir)
290 writeReport(reporter, collector)
291 disposeCollectors()
292 })
293 })
294 })
295
296 disposeCollectors()
297 }
298
299 this.onExit = function (done) {
300 if (pendingFileWritings) {
301 fileWritingFinished = done
302 } else {
303 done()
304 }
305 }
306}
307
308CoverageReporter.$inject = ['config', 'helper', 'logger', 'emitter']
309
310// PUBLISH
311module.exports = CoverageReporter