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