mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-02 18:11:04 +08:00
323 lines
8.9 KiB
JavaScript
323 lines
8.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')
|
|
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
|