1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | var path = require('path')
|
14 | var istanbul = require('istanbul')
|
15 | var minimatch = require('minimatch')
|
16 |
|
17 | var globalSourceCache = require('./source-cache')
|
18 | var coverageMap = require('./coverage-map')
|
19 | var SourceCacheStore = require('./source-cache-store')
|
20 |
|
21 | function isAbsolute (file) {
|
22 | if (path.isAbsolute) {
|
23 | return path.isAbsolute(file)
|
24 | }
|
25 |
|
26 | return path.resolve(file) === path.normalize(file)
|
27 | }
|
28 |
|
29 |
|
30 | var CoverageReporter = function (rootConfig, helper, logger, emitter) {
|
31 | var _ = helper._
|
32 | var log = logger.create('coverage')
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | this.adapters = []
|
38 |
|
39 |
|
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 |
|
82 | var excludeKey = isAbsolute(key) ? path.relative(basePath, key) : key
|
83 |
|
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 |
|
94 | var found = patterns.some(function (pattern) {
|
95 | return minimatch(normalize(key), pattern, {dot: true})
|
96 | })
|
97 |
|
98 |
|
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 |
|
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 |
|
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 |
|
193 |
|
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 |
|
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 |
|
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 |
|
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 |
|
308 | CoverageReporter.$inject = ['config', 'helper', 'logger', 'emitter']
|
309 |
|
310 |
|
311 | module.exports = CoverageReporter
|