UNPKG

@vitest/coverage-istanbul

Version:
269 lines (264 loc) 9.7 kB
import { promises } from 'node:fs'; import { defaults } from '@istanbuljs/schema'; import createDebug from 'debug'; import libCoverage from 'istanbul-lib-coverage'; import { createInstrumenter } from 'istanbul-lib-instrument'; import libReport from 'istanbul-lib-report'; import libSourceMaps from 'istanbul-lib-source-maps'; import reports from 'istanbul-reports'; import { parseModule } from 'magicast'; import TestExclude from 'test-exclude'; import c from 'tinyrainbow'; import { BaseCoverageProvider } from 'vitest/coverage'; import { isCSSRequest } from 'vitest/node'; import { C as COVERAGE_STORE_KEY } from './constants-BCJfMgEg.js'; const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//; function normalizeWindowsPath(input = "") { if (!input) { return input; } return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase()); } const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/; function cwd() { if (typeof process !== "undefined" && typeof process.cwd === "function") { return process.cwd().replace(/\\/g, "/"); } return "/"; } const resolve = function(...arguments_) { arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument)); let resolvedPath = ""; let resolvedAbsolute = false; for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) { const path = index >= 0 ? arguments_[index] : cwd(); if (!path || path.length === 0) { continue; } resolvedPath = `${path}/${resolvedPath}`; resolvedAbsolute = isAbsolute(path); } resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute); if (resolvedAbsolute && !isAbsolute(resolvedPath)) { return `/${resolvedPath}`; } return resolvedPath.length > 0 ? resolvedPath : "."; }; function normalizeString(path, allowAboveRoot) { let res = ""; let lastSegmentLength = 0; let lastSlash = -1; let dots = 0; let char = null; for (let index = 0; index <= path.length; ++index) { if (index < path.length) { char = path[index]; } else if (char === "/") { break; } else { char = "/"; } if (char === "/") { if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) { if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") { if (res.length > 2) { const lastSlashIndex = res.lastIndexOf("/"); if (lastSlashIndex === -1) { res = ""; lastSegmentLength = 0; } else { res = res.slice(0, lastSlashIndex); lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); } lastSlash = index; dots = 0; continue; } else if (res.length > 0) { res = ""; lastSegmentLength = 0; lastSlash = index; dots = 0; continue; } } if (allowAboveRoot) { res += res.length > 0 ? "/.." : ".."; lastSegmentLength = 2; } } else { if (res.length > 0) { res += `/${path.slice(lastSlash + 1, index)}`; } else { res = path.slice(lastSlash + 1, index); } lastSegmentLength = index - lastSlash - 1; } lastSlash = index; dots = 0; } else if (char === "." && dots !== -1) { ++dots; } else { dots = -1; } } return res; } const isAbsolute = function(p) { return _IS_ABSOLUTE_RE.test(p); }; var version = "3.2.4"; const debug = createDebug("vitest:coverage"); class IstanbulCoverageProvider extends BaseCoverageProvider { name = "istanbul"; version = version; instrumenter; testExclude; initialize(ctx) { this._initialize(ctx); this.testExclude = new TestExclude({ cwd: ctx.config.root, include: this.options.include, exclude: this.options.exclude, excludeNodeModules: true, extension: this.options.extension, relativePath: !this.options.allowExternal }); this.instrumenter = createInstrumenter({ produceSourceMap: true, autoWrap: false, esModules: true, compact: false, coverageVariable: COVERAGE_STORE_KEY, coverageGlobalScope: "globalThis", coverageGlobalScopeFunc: false, ignoreClassMethods: this.options.ignoreClassMethods, parserPlugins: [...defaults.instrumenter.parserPlugins, ["importAttributes", { deprecatedAssertSyntax: true }]], generatorOpts: { importAttributesKeyword: "with" } }); } onFileTransform(sourceCode, id, pluginCtx) { // Istanbul/babel cannot instrument CSS - e.g. Vue imports end up here. // File extension itself is .vue, but it contains CSS. // e.g. "Example.vue?vue&type=style&index=0&scoped=f7f04e08&lang.css" if (isCSSRequest(id)) { return; } if (!this.testExclude.shouldInstrument(removeQueryParameters(id))) { return; } const sourceMap = pluginCtx.getCombinedSourcemap(); sourceMap.sources = sourceMap.sources.map(removeQueryParameters); sourceCode = sourceCode.replaceAll("_ts_decorate", "/* istanbul ignore next */_ts_decorate").replaceAll(/(if +\(import\.meta\.vitest\))/g, "/* istanbul ignore next */ $1"); const code = this.instrumenter.instrumentSync(sourceCode, id, sourceMap); const map = this.instrumenter.lastSourceMap(); return { code, map }; } createCoverageMap() { return libCoverage.createCoverageMap({}); } async generateCoverage({ allTestsRun }) { const start = debug.enabled ? performance.now() : 0; const coverageMap = this.createCoverageMap(); let coverageMapByTransformMode = this.createCoverageMap(); await this.readCoverageFiles({ onFileRead(coverage) { coverageMapByTransformMode.merge(coverage); }, onFinished: async () => { // Source maps can change based on projectName and transform mode. // Coverage transform re-uses source maps so we need to separate transforms from each other. const transformedCoverage = await transformCoverage(coverageMapByTransformMode); coverageMap.merge(transformedCoverage); coverageMapByTransformMode = this.createCoverageMap(); }, onDebug: debug }); // Include untested files when all tests were run (not a single file re-run) // or if previous results are preserved by "cleanOnRerun: false" if (this.options.all && (allTestsRun || !this.options.cleanOnRerun)) { const coveredFiles = coverageMap.files(); const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles); coverageMap.merge(await transformCoverage(uncoveredCoverage)); } if (this.options.excludeAfterRemap) { coverageMap.filter((filename) => this.testExclude.shouldInstrument(filename)); } if (debug.enabled) { debug("Generate coverage total time %d ms", (performance.now() - start).toFixed()); } return coverageMap; } async generateReports(coverageMap, allTestsRun) { const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, watermarks: this.options.watermarks }); if (this.hasTerminalReporter(this.options.reporter)) { this.ctx.logger.log(c.blue(" % ") + c.dim("Coverage report from ") + c.yellow(this.name)); } for (const reporter of this.options.reporter) { // Type assertion required for custom reporters reports.create(reporter[0], { skipFull: this.options.skipFull, projectRoot: this.ctx.config.root, ...reporter[1] }).execute(context); } if (this.options.thresholds) { await this.reportThresholds(coverageMap, allTestsRun); } } async parseConfigModule(configFilePath) { return parseModule(await promises.readFile(configFilePath, "utf8")); } async getCoverageMapForUncoveredFiles(coveredFiles) { const allFiles = await this.testExclude.glob(this.ctx.config.root); let includedFiles = allFiles.map((file) => resolve(this.ctx.config.root, file)); if (this.ctx.config.changed) { includedFiles = (this.ctx.config.related || []).filter((file) => includedFiles.includes(file)); } const uncoveredFiles = includedFiles.filter((file) => !coveredFiles.includes(file)).sort(); const cacheKey = new Date().getTime(); const coverageMap = this.createCoverageMap(); const transform = this.createUncoveredFileTransformer(this.ctx); // Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage // returns the coverage of the last transformed file for (const [index, filename] of uncoveredFiles.entries()) { let timeout; let start; if (debug.enabled) { start = performance.now(); timeout = setTimeout(() => debug(c.bgRed(`File "${filename}" is taking longer than 3s`)), 3e3); debug("Uncovered file %d/%d", index, uncoveredFiles.length); } // Make sure file is not served from cache so that instrumenter loads up requested file coverage await transform(`${filename}?cache=${cacheKey}`); const lastCoverage = this.instrumenter.lastFileCoverage(); coverageMap.addFileCoverage(lastCoverage); if (debug.enabled) { clearTimeout(timeout); const diff = performance.now() - start; const color = diff > 500 ? c.bgRed : c.bgGreen; debug(`${color(` ${diff.toFixed()} ms `)} ${filename}`); } } return coverageMap; } } async function transformCoverage(coverageMap) { const sourceMapStore = libSourceMaps.createSourceMapStore(); return await sourceMapStore.transformCoverage(coverageMap); } /** * Remove possible query parameters from filenames * - From `/src/components/Header.component.ts?vue&type=script&src=true&lang.ts` * - To `/src/components/Header.component.ts` */ function removeQueryParameters(filename) { return filename.split("?")[0]; } export { IstanbulCoverageProvider };