版博士V2.0程序
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

341 řádky
10 KiB

  1. const Exclude = require('test-exclude')
  2. const libCoverage = require('istanbul-lib-coverage')
  3. const libReport = require('istanbul-lib-report')
  4. const reports = require('istanbul-reports')
  5. const { readdirSync, readFileSync, statSync } = require('fs')
  6. const { isAbsolute, resolve, extname } = require('path')
  7. const { pathToFileURL, fileURLToPath } = require('url')
  8. const getSourceMapFromFile = require('./source-map-from-file')
  9. // TODO: switch back to @c88/v8-coverage once patch is landed.
  10. const v8toIstanbul = require('v8-to-istanbul')
  11. const isCjsEsmBridgeCov = require('./is-cjs-esm-bridge')
  12. const util = require('util')
  13. const debuglog = util.debuglog('c8')
  14. class Report {
  15. constructor ({
  16. exclude,
  17. extension,
  18. excludeAfterRemap,
  19. include,
  20. reporter,
  21. reporterOptions,
  22. reportsDirectory,
  23. tempDirectory,
  24. watermarks,
  25. omitRelative,
  26. wrapperLength,
  27. resolve: resolvePaths,
  28. all,
  29. src,
  30. allowExternal = false,
  31. skipFull,
  32. excludeNodeModules
  33. }) {
  34. this.reporter = reporter
  35. this.reporterOptions = reporterOptions || {}
  36. this.reportsDirectory = reportsDirectory
  37. this.tempDirectory = tempDirectory
  38. this.watermarks = watermarks
  39. this.resolve = resolvePaths
  40. this.exclude = new Exclude({
  41. exclude: exclude,
  42. include: include,
  43. extension: extension,
  44. relativePath: !allowExternal,
  45. excludeNodeModules: excludeNodeModules
  46. })
  47. this.excludeAfterRemap = excludeAfterRemap
  48. this.shouldInstrumentCache = new Map()
  49. this.omitRelative = omitRelative
  50. this.sourceMapCache = {}
  51. this.wrapperLength = wrapperLength
  52. this.all = all
  53. this.src = this._getSrc(src)
  54. this.skipFull = skipFull
  55. }
  56. _getSrc (src) {
  57. if (typeof src === 'string') {
  58. return [src]
  59. } else if (Array.isArray(src)) {
  60. return src
  61. } else {
  62. return [process.cwd()]
  63. }
  64. }
  65. async run () {
  66. const context = libReport.createContext({
  67. dir: this.reportsDirectory,
  68. watermarks: this.watermarks,
  69. coverageMap: await this.getCoverageMapFromAllCoverageFiles()
  70. })
  71. for (const _reporter of this.reporter) {
  72. reports.create(_reporter, {
  73. skipEmpty: false,
  74. skipFull: this.skipFull,
  75. maxCols: process.stdout.columns || 100,
  76. ...this.reporterOptions[_reporter]
  77. }).execute(context)
  78. }
  79. }
  80. async getCoverageMapFromAllCoverageFiles () {
  81. // the merge process can be very expensive, and it's often the case that
  82. // check-coverage is called immediately after a report. We memoize the
  83. // result from getCoverageMapFromAllCoverageFiles() to address this
  84. // use-case.
  85. if (this._allCoverageFiles) return this._allCoverageFiles
  86. const map = libCoverage.createCoverageMap()
  87. const v8ProcessCov = this._getMergedProcessCov()
  88. const resultCountPerPath = new Map()
  89. const possibleCjsEsmBridges = new Map()
  90. for (const v8ScriptCov of v8ProcessCov.result) {
  91. try {
  92. const sources = this._getSourceMap(v8ScriptCov)
  93. const path = resolve(this.resolve, v8ScriptCov.url)
  94. const converter = v8toIstanbul(path, this.wrapperLength, sources, (path) => {
  95. if (this.excludeAfterRemap) {
  96. return !this._shouldInstrument(path)
  97. }
  98. })
  99. await converter.load()
  100. if (resultCountPerPath.has(path)) {
  101. resultCountPerPath.set(path, resultCountPerPath.get(path) + 1)
  102. } else {
  103. resultCountPerPath.set(path, 0)
  104. }
  105. if (isCjsEsmBridgeCov(v8ScriptCov)) {
  106. possibleCjsEsmBridges.set(converter, {
  107. path,
  108. functions: v8ScriptCov.functions
  109. })
  110. } else {
  111. converter.applyCoverage(v8ScriptCov.functions)
  112. map.merge(converter.toIstanbul())
  113. }
  114. } catch (err) {
  115. debuglog(`file: ${v8ScriptCov.url} error: ${err.stack}`)
  116. }
  117. }
  118. for (const [converter, { path, functions }] of possibleCjsEsmBridges) {
  119. if (resultCountPerPath.get(path) <= 1) {
  120. converter.applyCoverage(functions)
  121. map.merge(converter.toIstanbul())
  122. }
  123. }
  124. this._allCoverageFiles = map
  125. return this._allCoverageFiles
  126. }
  127. /**
  128. * Returns source-map and fake source file, if cached during Node.js'
  129. * execution. This is used to support tools like ts-node, which transpile
  130. * using runtime hooks.
  131. *
  132. * Note: requires Node.js 13+
  133. *
  134. * @return {Object} sourceMap and fake source file (created from line #s).
  135. * @private
  136. */
  137. _getSourceMap (v8ScriptCov) {
  138. const sources = {}
  139. const sourceMapAndLineLengths = this.sourceMapCache[pathToFileURL(v8ScriptCov.url).href]
  140. if (sourceMapAndLineLengths) {
  141. // See: https://github.com/nodejs/node/pull/34305
  142. if (!sourceMapAndLineLengths.data) return
  143. sources.sourceMap = {
  144. sourcemap: sourceMapAndLineLengths.data
  145. }
  146. if (sourceMapAndLineLengths.lineLengths) {
  147. let source = ''
  148. sourceMapAndLineLengths.lineLengths.forEach(length => {
  149. source += `${''.padEnd(length, '.')}\n`
  150. })
  151. sources.source = source
  152. }
  153. }
  154. return sources
  155. }
  156. /**
  157. * Returns the merged V8 process coverage.
  158. *
  159. * The result is computed from the individual process coverages generated
  160. * by Node. It represents the sum of their counts.
  161. *
  162. * @return {ProcessCov} Merged V8 process coverage.
  163. * @private
  164. */
  165. _getMergedProcessCov () {
  166. const { mergeProcessCovs } = require('@bcoe/v8-coverage')
  167. const v8ProcessCovs = []
  168. const fileIndex = new Set() // Set<string>
  169. for (const v8ProcessCov of this._loadReports()) {
  170. if (this._isCoverageObject(v8ProcessCov)) {
  171. if (v8ProcessCov['source-map-cache']) {
  172. Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(v8ProcessCov['source-map-cache']))
  173. }
  174. v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex))
  175. }
  176. }
  177. if (this.all) {
  178. const emptyReports = []
  179. v8ProcessCovs.unshift({
  180. result: emptyReports
  181. })
  182. const workingDirs = this.src
  183. const { extension } = this.exclude
  184. for (const workingDir of workingDirs) {
  185. this.exclude.globSync(workingDir).forEach((f) => {
  186. const fullPath = resolve(workingDir, f)
  187. if (!fileIndex.has(fullPath)) {
  188. const ext = extname(fullPath)
  189. if (extension.includes(ext)) {
  190. const stat = statSync(fullPath)
  191. const sourceMap = getSourceMapFromFile(fullPath)
  192. if (sourceMap) {
  193. this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap }
  194. }
  195. emptyReports.push({
  196. scriptId: 0,
  197. url: resolve(fullPath),
  198. functions: [{
  199. functionName: '(empty-report)',
  200. ranges: [{
  201. startOffset: 0,
  202. endOffset: stat.size,
  203. count: 0
  204. }],
  205. isBlockCoverage: true
  206. }]
  207. })
  208. }
  209. }
  210. })
  211. }
  212. }
  213. return mergeProcessCovs(v8ProcessCovs)
  214. }
  215. /**
  216. * Make sure v8ProcessCov actually contains coverage information.
  217. *
  218. * @return {boolean} does it look like v8ProcessCov?
  219. * @private
  220. */
  221. _isCoverageObject (maybeV8ProcessCov) {
  222. return maybeV8ProcessCov && Array.isArray(maybeV8ProcessCov.result)
  223. }
  224. /**
  225. * Returns the list of V8 process coverages generated by Node.
  226. *
  227. * @return {ProcessCov[]} Process coverages generated by Node.
  228. * @private
  229. */
  230. _loadReports () {
  231. const reports = []
  232. for (const file of readdirSync(this.tempDirectory)) {
  233. try {
  234. reports.push(JSON.parse(readFileSync(
  235. resolve(this.tempDirectory, file),
  236. 'utf8'
  237. )))
  238. } catch (err) {
  239. debuglog(`${err.stack}`)
  240. }
  241. }
  242. return reports
  243. }
  244. /**
  245. * Normalizes a process coverage.
  246. *
  247. * This function replaces file URLs (`url` property) by their corresponding
  248. * system-dependent path and applies the current inclusion rules to filter out
  249. * the excluded script coverages.
  250. *
  251. * The result is a copy of the input, with script coverages filtered based
  252. * on their `url` and the current inclusion rules.
  253. * There is no deep cloning.
  254. *
  255. * @param v8ProcessCov V8 process coverage to normalize.
  256. * @param fileIndex a Set<string> of paths discovered in coverage
  257. * @return {v8ProcessCov} Normalized V8 process coverage.
  258. * @private
  259. */
  260. _normalizeProcessCov (v8ProcessCov, fileIndex) {
  261. const result = []
  262. for (const v8ScriptCov of v8ProcessCov.result) {
  263. // https://github.com/nodejs/node/pull/35498 updates Node.js'
  264. // builtin module filenames:
  265. if (/^node:/.test(v8ScriptCov.url)) {
  266. v8ScriptCov.url = `${v8ScriptCov.url.replace(/^node:/, '')}.js`
  267. }
  268. if (/^file:\/\//.test(v8ScriptCov.url)) {
  269. try {
  270. v8ScriptCov.url = fileURLToPath(v8ScriptCov.url)
  271. fileIndex.add(v8ScriptCov.url)
  272. } catch (err) {
  273. debuglog(`${err.stack}`)
  274. continue
  275. }
  276. }
  277. if ((!this.omitRelative || isAbsolute(v8ScriptCov.url))) {
  278. if (this.excludeAfterRemap || this._shouldInstrument(v8ScriptCov.url)) {
  279. result.push(v8ScriptCov)
  280. }
  281. }
  282. }
  283. return { result }
  284. }
  285. /**
  286. * Normalizes a V8 source map cache.
  287. *
  288. * This function normalizes file URLs to a system-independent format.
  289. *
  290. * @param v8SourceMapCache V8 source map cache to normalize.
  291. * @return {v8SourceMapCache} Normalized V8 source map cache.
  292. * @private
  293. */
  294. _normalizeSourceMapCache (v8SourceMapCache) {
  295. const cache = {}
  296. for (const fileURL of Object.keys(v8SourceMapCache)) {
  297. cache[pathToFileURL(fileURLToPath(fileURL)).href] = v8SourceMapCache[fileURL]
  298. }
  299. return cache
  300. }
  301. /**
  302. * this.exclude.shouldInstrument with cache
  303. *
  304. * @private
  305. * @return {boolean}
  306. */
  307. _shouldInstrument (filename) {
  308. const cacheResult = this.shouldInstrumentCache.get(filename)
  309. if (cacheResult !== undefined) {
  310. return cacheResult
  311. }
  312. const result = this.exclude.shouldInstrument(filename)
  313. this.shouldInstrumentCache.set(filename, result)
  314. return result
  315. }
  316. }
  317. module.exports = function (opts) {
  318. return new Report(opts)
  319. }