123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- // 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
|