315 lines
9 KiB
JavaScript
315 lines
9 KiB
JavaScript
|
// 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')
|
||
|
const { promisify } = require('util')
|
||
|
var istanbulLibCoverage = require('istanbul-lib-coverage')
|
||
|
var istanbulLibReport = require('istanbul-lib-report')
|
||
|
var minimatch = require('minimatch')
|
||
|
|
||
|
var globalSourceMapStore = require('./source-map-store')
|
||
|
var globalCoverageMap = require('./coverage-map')
|
||
|
var reports = require('./report-creator')
|
||
|
const hasOwnProperty = Object.prototype.hasOwnProperty
|
||
|
|
||
|
// 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 sourceMapStore = globalSourceMapStore.get(basePath)
|
||
|
var includeAllSources = config.includeAllSources === true
|
||
|
|
||
|
if (config.watermarks) {
|
||
|
config.watermarks = helper.merge({}, istanbulLibReport.getDefaultWatermarks(), config.watermarks)
|
||
|
}
|
||
|
|
||
|
if (!helper.isDefined(reporters)) {
|
||
|
reporters = [config]
|
||
|
}
|
||
|
|
||
|
var coverageMaps
|
||
|
|
||
|
function normalize (key) {
|
||
|
// Exclude keys will always be relative, but covObj keys can be absolute or relative
|
||
|
var excludeKey = path.isAbsolute(key) ? path.relative(basePath, key) : key
|
||
|
// Also normalize for files that start with `./`, etc.
|
||
|
excludeKey = path.normalize(excludeKey)
|
||
|
|
||
|
return excludeKey
|
||
|
}
|
||
|
|
||
|
function getTrackedFiles (coverageMap, patterns) {
|
||
|
var files = []
|
||
|
|
||
|
coverageMap.files().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) {
|
||
|
files.push(key)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return files
|
||
|
}
|
||
|
|
||
|
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, coverageMap) {
|
||
|
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 globalTrackedFiles = getTrackedFiles(coverageMap, thresholds.global.excludes)
|
||
|
var eachTrackedFiles = getTrackedFiles(coverageMap, thresholds.each.excludes)
|
||
|
var globalResults = istanbulLibCoverage.createCoverageSummary()
|
||
|
var eachResults = {}
|
||
|
globalTrackedFiles.forEach(function (f) {
|
||
|
var fileCoverage = coverageMap.fileCoverageFor(f)
|
||
|
var summary = fileCoverage.toSummary()
|
||
|
globalResults.merge(summary)
|
||
|
})
|
||
|
eachTrackedFiles.forEach(function (f) {
|
||
|
var fileCoverage = coverageMap.fileCoverageFor(f)
|
||
|
var summary = fileCoverage.toSummary()
|
||
|
eachResults[f] = summary
|
||
|
})
|
||
|
|
||
|
var coverageFailed = false
|
||
|
const { emitWarning = false } = thresholds
|
||
|
|
||
|
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) {
|
||
|
const message = `${browser.name}: Coverage for ${key} (${actual}%) does not meet ${name} threshold (${threshold}%)`
|
||
|
if (emitWarning) {
|
||
|
log.warn(message)
|
||
|
} else {
|
||
|
coverageFailed = true
|
||
|
log.error(message)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
check('global', thresholds.global, globalResults.toJSON())
|
||
|
|
||
|
eachTrackedFiles.forEach(function (key) {
|
||
|
var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides))
|
||
|
check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key].toJSON())
|
||
|
})
|
||
|
|
||
|
return coverageFailed
|
||
|
}
|
||
|
|
||
|
// Generate the output path from the `coverageReporter.dir` and
|
||
|
// `coverageReporter.subdir` options.
|
||
|
function generateOutputPath (basePath, browserName, dir = 'coverage', subdir) {
|
||
|
if (subdir && typeof subdir === 'function') {
|
||
|
subdir = subdir(browserName)
|
||
|
}
|
||
|
if (browserName) {
|
||
|
browserName = browserName.replace(':', '')
|
||
|
}
|
||
|
|
||
|
let outPutPath = path.join(dir, subdir || browserName)
|
||
|
outPutPath = path.resolve(basePath, outPutPath)
|
||
|
|
||
|
return helper.normalizeWinPath(outPutPath)
|
||
|
}
|
||
|
|
||
|
this.onRunStart = function (browsers) {
|
||
|
coverageMaps = 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) {
|
||
|
var startingMap = {}
|
||
|
if (includeAllSources) {
|
||
|
startingMap = globalCoverageMap.get()
|
||
|
}
|
||
|
|
||
|
coverageMaps[browser.id] = istanbulLibCoverage.createCoverageMap(startingMap)
|
||
|
}
|
||
|
|
||
|
this.onBrowserComplete = function (browser, result) {
|
||
|
var coverageMap = coverageMaps[browser.id]
|
||
|
|
||
|
if (!coverageMap) return
|
||
|
if (!result || !result.coverage) return
|
||
|
|
||
|
coverageMap.merge(result.coverage)
|
||
|
}
|
||
|
|
||
|
this.onSpecComplete = function (browser, result) {
|
||
|
var coverageMap = coverageMaps[browser.id]
|
||
|
|
||
|
if (!coverageMap) return
|
||
|
if (!result.coverage) return
|
||
|
|
||
|
coverageMap.merge(result.coverage)
|
||
|
}
|
||
|
|
||
|
let checkedCoverage = {}
|
||
|
let promiseComplete = null
|
||
|
|
||
|
this.executeReport = async function (reporterConfig, browser) {
|
||
|
const results = { exitCode: 0 }
|
||
|
const coverageMap = coverageMaps[browser.id]
|
||
|
if (!coverageMap) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const mainDir = reporterConfig.dir || config.dir
|
||
|
const subDir = reporterConfig.subdir || config.subdir
|
||
|
const outputPath = generateOutputPath(basePath, browser.name, mainDir, subDir)
|
||
|
const remappedCoverageMap = await sourceMapStore.transformCoverage(coverageMap)
|
||
|
|
||
|
const options = helper.merge(config, reporterConfig, {
|
||
|
dir: outputPath,
|
||
|
subdir: '',
|
||
|
browser: browser,
|
||
|
emitter: emitter,
|
||
|
coverageMap: remappedCoverageMap
|
||
|
})
|
||
|
|
||
|
// If config.check is defined, check coverage levels for each browser
|
||
|
if (hasOwnProperty.call(config, 'check') && !checkedCoverage[browser.id]) {
|
||
|
checkedCoverage[browser.id] = true
|
||
|
var coverageFailed = checkCoverage(browser, remappedCoverageMap)
|
||
|
if (coverageFailed && results) {
|
||
|
results.exitCode = 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const context = istanbulLibReport.createContext(options)
|
||
|
const report = reports.create(reporterConfig.type || 'html', options)
|
||
|
|
||
|
// // If reporting to console or in-memory skip directory creation
|
||
|
const toDisk = !reporterConfig.type || !reporterConfig.type.match(/^(text|text-summary|in-memory)$/)
|
||
|
|
||
|
if (!toDisk && reporterConfig.file === undefined) {
|
||
|
report.execute(context)
|
||
|
return results
|
||
|
}
|
||
|
|
||
|
const mkdirIfNotExists = promisify(helper.mkdirIfNotExists)
|
||
|
await mkdirIfNotExists(outputPath)
|
||
|
|
||
|
log.debug('Writing coverage to %s', outputPath)
|
||
|
report.execute(context)
|
||
|
|
||
|
return results
|
||
|
}
|
||
|
|
||
|
this.onRunComplete = function (browsers) {
|
||
|
checkedCoverage = {}
|
||
|
let results = { exitCode: 0 }
|
||
|
|
||
|
const promiseCollection = reporters.map(reporterConfig =>
|
||
|
Promise.all(browsers.map(async (browser) => {
|
||
|
const res = await this.executeReport(reporterConfig, browser)
|
||
|
if (res && res.exitCode === 1) {
|
||
|
results = res
|
||
|
}
|
||
|
})))
|
||
|
promiseComplete = Promise.all(promiseCollection).then(() => results)
|
||
|
return promiseComplete
|
||
|
}
|
||
|
|
||
|
this.onExit = async function (done) {
|
||
|
try {
|
||
|
const results = await promiseComplete
|
||
|
if (results && results.exitCode === 1) {
|
||
|
done(results.exitCode)
|
||
|
return
|
||
|
}
|
||
|
if (typeof config._onExit === 'function') {
|
||
|
config._onExit(done)
|
||
|
} else {
|
||
|
done()
|
||
|
}
|
||
|
} catch (e) {
|
||
|
log.error('Unexpected error while generating coverage report.\n', e)
|
||
|
done(1)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CoverageReporter.$inject = ['config', 'helper', 'logger', 'emitter']
|
||
|
|
||
|
// PUBLISH
|
||
|
module.exports = CoverageReporter
|