reporter.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. // Coverage Reporter
  2. // Part of this code is based on [1], which is licensed under the New BSD License.
  3. // For more information see the See the accompanying LICENSE-istanbul file for terms.
  4. //
  5. // [1]: https://github.com/gotwarlost/istanbul/blob/master/lib/command/check-coverage.js
  6. // =====================
  7. //
  8. // Generates the report
  9. // Dependencies
  10. // ------------
  11. var path = require('path')
  12. const { promisify } = require('util')
  13. var istanbulLibCoverage = require('istanbul-lib-coverage')
  14. var istanbulLibReport = require('istanbul-lib-report')
  15. var minimatch = require('minimatch')
  16. var globalSourceMapStore = require('./source-map-store')
  17. var globalCoverageMap = require('./coverage-map')
  18. var reports = require('./report-creator')
  19. const hasOwnProperty = Object.prototype.hasOwnProperty
  20. // TODO(vojta): inject only what required (config.basePath, config.coverageReporter)
  21. var CoverageReporter = function (rootConfig, helper, logger, emitter) {
  22. var log = logger.create('coverage')
  23. // Instance variables
  24. // ------------------
  25. this.adapters = []
  26. // Options
  27. // -------
  28. var config = rootConfig.coverageReporter || {}
  29. var basePath = rootConfig.basePath
  30. var reporters = config.reporters
  31. var sourceMapStore = globalSourceMapStore.get(basePath)
  32. var includeAllSources = config.includeAllSources === true
  33. if (config.watermarks) {
  34. config.watermarks = helper.merge({}, istanbulLibReport.getDefaultWatermarks(), config.watermarks)
  35. }
  36. if (!helper.isDefined(reporters)) {
  37. reporters = [config]
  38. }
  39. var coverageMaps
  40. function normalize (key) {
  41. // Exclude keys will always be relative, but covObj keys can be absolute or relative
  42. var excludeKey = path.isAbsolute(key) ? path.relative(basePath, key) : key
  43. // Also normalize for files that start with `./`, etc.
  44. excludeKey = path.normalize(excludeKey)
  45. return excludeKey
  46. }
  47. function getTrackedFiles (coverageMap, patterns) {
  48. var files = []
  49. coverageMap.files().forEach(function (key) {
  50. // Do any patterns match the resolved key
  51. var found = patterns.some(function (pattern) {
  52. return minimatch(normalize(key), pattern, { dot: true })
  53. })
  54. // if no patterns match, keep the key
  55. if (!found) {
  56. files.push(key)
  57. }
  58. })
  59. return files
  60. }
  61. function overrideThresholds (key, overrides) {
  62. var thresholds = {}
  63. // First match wins
  64. Object.keys(overrides).some(function (pattern) {
  65. if (minimatch(normalize(key), pattern, { dot: true })) {
  66. thresholds = overrides[pattern]
  67. return true
  68. }
  69. })
  70. return thresholds
  71. }
  72. function checkCoverage (browser, coverageMap) {
  73. var defaultThresholds = {
  74. global: {
  75. statements: 0,
  76. branches: 0,
  77. lines: 0,
  78. functions: 0,
  79. excludes: []
  80. },
  81. each: {
  82. statements: 0,
  83. branches: 0,
  84. lines: 0,
  85. functions: 0,
  86. excludes: [],
  87. overrides: {}
  88. }
  89. }
  90. var thresholds = helper.merge({}, defaultThresholds, config.check)
  91. var globalTrackedFiles = getTrackedFiles(coverageMap, thresholds.global.excludes)
  92. var eachTrackedFiles = getTrackedFiles(coverageMap, thresholds.each.excludes)
  93. var globalResults = istanbulLibCoverage.createCoverageSummary()
  94. var eachResults = {}
  95. globalTrackedFiles.forEach(function (f) {
  96. var fileCoverage = coverageMap.fileCoverageFor(f)
  97. var summary = fileCoverage.toSummary()
  98. globalResults.merge(summary)
  99. })
  100. eachTrackedFiles.forEach(function (f) {
  101. var fileCoverage = coverageMap.fileCoverageFor(f)
  102. var summary = fileCoverage.toSummary()
  103. eachResults[f] = summary
  104. })
  105. var coverageFailed = false
  106. const { emitWarning = false } = thresholds
  107. function check (name, thresholds, actuals) {
  108. var keys = [
  109. 'statements',
  110. 'branches',
  111. 'lines',
  112. 'functions'
  113. ]
  114. keys.forEach(function (key) {
  115. var actual = actuals[key].pct
  116. var actualUncovered = actuals[key].total - actuals[key].covered
  117. var threshold = thresholds[key]
  118. if (threshold < 0) {
  119. if (threshold * -1 < actualUncovered) {
  120. coverageFailed = true
  121. log.error(browser.name + ': Uncovered count for ' + key + ' (' + actualUncovered +
  122. ') exceeds ' + name + ' threshold (' + -1 * threshold + ')')
  123. }
  124. } else if (actual < threshold) {
  125. const message = `${browser.name}: Coverage for ${key} (${actual}%) does not meet ${name} threshold (${threshold}%)`
  126. if (emitWarning) {
  127. log.warn(message)
  128. } else {
  129. coverageFailed = true
  130. log.error(message)
  131. }
  132. }
  133. })
  134. }
  135. check('global', thresholds.global, globalResults.toJSON())
  136. eachTrackedFiles.forEach(function (key) {
  137. var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides))
  138. check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key].toJSON())
  139. })
  140. return coverageFailed
  141. }
  142. // Generate the output path from the `coverageReporter.dir` and
  143. // `coverageReporter.subdir` options.
  144. function generateOutputPath (basePath, browserName, dir = 'coverage', subdir) {
  145. if (subdir && typeof subdir === 'function') {
  146. subdir = subdir(browserName)
  147. }
  148. if (browserName) {
  149. browserName = browserName.replace(':', '')
  150. }
  151. let outPutPath = path.join(dir, subdir || browserName)
  152. outPutPath = path.resolve(basePath, outPutPath)
  153. return helper.normalizeWinPath(outPutPath)
  154. }
  155. this.onRunStart = function (browsers) {
  156. coverageMaps = Object.create(null)
  157. // TODO(vojta): remove once we don't care about Karma 0.10
  158. if (browsers) {
  159. browsers.forEach(this.onBrowserStart.bind(this))
  160. }
  161. }
  162. this.onBrowserStart = function (browser) {
  163. var startingMap = {}
  164. if (includeAllSources) {
  165. startingMap = globalCoverageMap.get()
  166. }
  167. coverageMaps[browser.id] = istanbulLibCoverage.createCoverageMap(startingMap)
  168. }
  169. this.onBrowserComplete = function (browser, result) {
  170. var coverageMap = coverageMaps[browser.id]
  171. if (!coverageMap) return
  172. if (!result || !result.coverage) return
  173. coverageMap.merge(result.coverage)
  174. }
  175. this.onSpecComplete = function (browser, result) {
  176. var coverageMap = coverageMaps[browser.id]
  177. if (!coverageMap) return
  178. if (!result.coverage) return
  179. coverageMap.merge(result.coverage)
  180. }
  181. let checkedCoverage = {}
  182. let promiseComplete = null
  183. this.executeReport = async function (reporterConfig, browser) {
  184. const results = { exitCode: 0 }
  185. const coverageMap = coverageMaps[browser.id]
  186. if (!coverageMap) {
  187. return
  188. }
  189. const mainDir = reporterConfig.dir || config.dir
  190. const subDir = reporterConfig.subdir || config.subdir
  191. const outputPath = generateOutputPath(basePath, browser.name, mainDir, subDir)
  192. const remappedCoverageMap = await sourceMapStore.transformCoverage(coverageMap)
  193. const options = helper.merge(config, reporterConfig, {
  194. dir: outputPath,
  195. subdir: '',
  196. browser: browser,
  197. emitter: emitter,
  198. coverageMap: remappedCoverageMap
  199. })
  200. // If config.check is defined, check coverage levels for each browser
  201. if (hasOwnProperty.call(config, 'check') && !checkedCoverage[browser.id]) {
  202. checkedCoverage[browser.id] = true
  203. var coverageFailed = checkCoverage(browser, remappedCoverageMap)
  204. if (coverageFailed && results) {
  205. results.exitCode = 1
  206. }
  207. }
  208. const context = istanbulLibReport.createContext(options)
  209. const report = reports.create(reporterConfig.type || 'html', options)
  210. // // If reporting to console or in-memory skip directory creation
  211. const toDisk = !reporterConfig.type || !reporterConfig.type.match(/^(text|text-summary|in-memory)$/)
  212. if (!toDisk && reporterConfig.file === undefined) {
  213. report.execute(context)
  214. return results
  215. }
  216. const mkdirIfNotExists = promisify(helper.mkdirIfNotExists)
  217. await mkdirIfNotExists(outputPath)
  218. log.debug('Writing coverage to %s', outputPath)
  219. report.execute(context)
  220. return results
  221. }
  222. this.onRunComplete = function (browsers) {
  223. checkedCoverage = {}
  224. let results = { exitCode: 0 }
  225. const promiseCollection = reporters.map(reporterConfig =>
  226. Promise.all(browsers.map(async (browser) => {
  227. const res = await this.executeReport(reporterConfig, browser)
  228. if (res && res.exitCode === 1) {
  229. results = res
  230. }
  231. })))
  232. promiseComplete = Promise.all(promiseCollection).then(() => results)
  233. return promiseComplete
  234. }
  235. this.onExit = async function (done) {
  236. try {
  237. const results = await promiseComplete
  238. if (results && results.exitCode === 1) {
  239. done(results.exitCode)
  240. return
  241. }
  242. if (typeof config._onExit === 'function') {
  243. config._onExit(done)
  244. } else {
  245. done()
  246. }
  247. } catch (e) {
  248. log.error('Unexpected error while generating coverage report.\n', e)
  249. done(1)
  250. }
  251. }
  252. }
  253. CoverageReporter.$inject = ['config', 'helper', 'logger', 'emitter']
  254. // PUBLISH
  255. module.exports = CoverageReporter