blob: 13d1c823fa71a0a61aaf6e60353761f53f72276d [file] [log] [blame]
// Coverage Reporter
// Part of this code is based on [1], which is licensed under the New BSD License.
// For more information see the See the accompanying LICENSE-istanbul file for terms.
//
// [1]: https://github.com/gotwarlost/istanbul/blob/master/lib/command/check-coverage.js
// =====================
//
// Generates the report
// Dependencies
// ------------
var path = require('path')
var istanbul = require('istanbul')
var minimatch = require('minimatch')
var _ = require('lodash')
var globalSourceCache = require('./source-cache')
var coverageMap = require('./coverage-map')
var SourceCacheStore = require('./source-cache-store')
function isAbsolute (file) {
if (path.isAbsolute) {
return path.isAbsolute(file)
}
return path.resolve(file) === path.normalize(file)
}
// TODO(vojta): inject only what required (config.basePath, config.coverageReporter)
var CoverageReporter = function (rootConfig, helper, logger, emitter) {
var log = logger.create('coverage')
// Instance variables
// ------------------
this.adapters = []
// Options
// -------
var config = rootConfig.coverageReporter || {}
var basePath = rootConfig.basePath
var reporters = config.reporters
var sourceCache = globalSourceCache.get(basePath)
var includeAllSources = config.includeAllSources === true
if (config.watermarks) {
config.watermarks = helper.merge({}, istanbul.config.defaultConfig().reporting.watermarks, config.watermarks)
}
if (!helper.isDefined(reporters)) {
reporters = [config]
}
var collectors
var pendingFileWritings = 0
var fileWritingFinished = function () {}
function writeReport (reporter, collector) {
try {
if (typeof config._onWriteReport === 'function') {
var newCollector = config._onWriteReport(collector)
if (typeof newCollector === 'object') {
collector = newCollector
}
}
reporter.writeReport(collector, true)
} catch (e) {
log.error(e)
}
--pendingFileWritings
}
function disposeCollectors () {
if (pendingFileWritings <= 0) {
_.forEach(collectors, function (collector) {
collector.dispose()
})
fileWritingFinished()
}
}
function normalize (key) {
// Exclude keys will always be relative, but covObj keys can be absolute or relative
var excludeKey = isAbsolute(key) ? path.relative(basePath, key) : key
// Also normalize for files that start with `./`, etc.
excludeKey = path.normalize(excludeKey)
return excludeKey
}
function removeFiles (covObj, patterns) {
var obj = {}
Object.keys(covObj).forEach(function (key) {
// Do any patterns match the resolved key
var found = patterns.some(function (pattern) {
return minimatch(normalize(key), pattern, {dot: true})
})
// if no patterns match, keep the key
if (!found) {
obj[key] = covObj[key]
}
})
return obj
}
function overrideThresholds (key, overrides) {
var thresholds = {}
// First match wins
Object.keys(overrides).some(function (pattern) {
if (minimatch(normalize(key), pattern, {dot: true})) {
thresholds = overrides[pattern]
return true
}
})
return thresholds
}
function checkCoverage (browser, collector) {
var defaultThresholds = {
global: {
statements: 0,
branches: 0,
lines: 0,
functions: 0,
excludes: []
},
each: {
statements: 0,
branches: 0,
lines: 0,
functions: 0,
excludes: [],
overrides: {}
}
}
var thresholds = helper.merge({}, defaultThresholds, config.check)
var rawCoverage = collector.getFinalCoverage()
var globalResults = istanbul.utils.summarizeCoverage(removeFiles(rawCoverage, thresholds.global.excludes))
var eachResults = removeFiles(rawCoverage, thresholds.each.excludes)
// Summarize per-file results and mutate original results.
Object.keys(eachResults).forEach(function (key) {
eachResults[key] = istanbul.utils.summarizeFileCoverage(eachResults[key])
})
var coverageFailed = false
function check (name, thresholds, actuals) {
var keys = [
'statements',
'branches',
'lines',
'functions'
]
keys.forEach(function (key) {
var actual = actuals[key].pct
var actualUncovered = actuals[key].total - actuals[key].covered
var threshold = thresholds[key]
if (threshold < 0) {
if (threshold * -1 < actualUncovered) {
coverageFailed = true
log.error(browser.name + ': Uncovered count for ' + key + ' (' + actualUncovered +
') exceeds ' + name + ' threshold (' + -1 * threshold + ')')
}
} else {
if (actual < threshold) {
coverageFailed = true
log.error(browser.name + ': Coverage for ' + key + ' (' + actual +
'%) does not meet ' + name + ' threshold (' + threshold + '%)')
}
}
})
}
check('global', thresholds.global, globalResults)
Object.keys(eachResults).forEach(function (key) {
var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides))
check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key])
})
return coverageFailed
}
// Generate the output directory from the `coverageReporter.dir` and
// `coverageReporter.subdir` options.
function generateOutputDir (browserName, dir, subdir) {
dir = dir || 'coverage'
subdir = subdir || browserName
if (_.isFunction(subdir)) {
subdir = subdir(browserName)
}
return path.join(dir, subdir)
}
this.onRunStart = function (browsers) {
collectors = Object.create(null)
// TODO(vojta): remove once we don't care about Karma 0.10
if (browsers) {
browsers.forEach(this.onBrowserStart.bind(this))
}
}
this.onBrowserStart = function (browser) {
collectors[browser.id] = new istanbul.Collector()
if (!includeAllSources) return
collectors[browser.id].add(coverageMap.get())
}
this.onBrowserComplete = function (browser, result) {
var collector = collectors[browser.id]
if (!collector) return
if (!result || !result.coverage) return
collector.add(result.coverage)
}
this.onSpecComplete = function (browser, result) {
if (!result.coverage) return
collectors[browser.id].add(result.coverage)
}
this.onRunComplete = function (browsers, results) {
var checkedCoverage = {}
reporters.forEach(function (reporterConfig) {
browsers.forEach(function (browser) {
var collector = collectors[browser.id]
if (!collector) {
return
}
// If config.check is defined, check coverage levels for each browser
if (config.hasOwnProperty('check') && !checkedCoverage[browser.id]) {
checkedCoverage[browser.id] = true
var coverageFailed = checkCoverage(browser, collector)
if (coverageFailed) {
if (results) {
results.exitCode = 1
}
}
}
pendingFileWritings++
var mainDir = reporterConfig.dir || config.dir
var subDir = reporterConfig.subdir || config.subdir
var browserName = browser.name.replace(':', '')
var simpleOutputDir = generateOutputDir(browserName, mainDir, subDir)
var resolvedOutputDir = path.resolve(basePath, simpleOutputDir)
var outputDir = helper.normalizeWinPath(resolvedOutputDir)
var sourceStore = _.isEmpty(sourceCache) ? null : new SourceCacheStore({
sourceCache: sourceCache
})
var options = helper.merge({
sourceStore: sourceStore
}, config, reporterConfig, {
dir: outputDir,
browser: browser,
emitter: emitter
})
var reporter = istanbul.Report.create(reporterConfig.type || 'html', options)
// If reporting to console or in-memory skip directory creation
var toDisk = !reporterConfig.type || !reporterConfig.type.match(/^(text|text-summary|in-memory)$/)
var hasNoFile = _.isUndefined(reporterConfig.file)
if (!toDisk && hasNoFile) {
writeReport(reporter, collector)
return
}
helper.mkdirIfNotExists(outputDir, function () {
log.debug('Writing coverage to %s', outputDir)
writeReport(reporter, collector)
disposeCollectors()
})
})
})
disposeCollectors()
}
this.onExit = function (done) {
if (pendingFileWritings) {
fileWritingFinished = (
typeof config._onExit === 'function'
? (function (done) { return function () { config._onExit(done) } }(done))
: done
)
} else {
(typeof config._onExit === 'function' ? config._onExit(done) : done())
}
}
}
CoverageReporter.$inject = ['config', 'helper', 'logger', 'emitter']
// PUBLISH
module.exports = CoverageReporter