@vitest/coverage-istanbul
Version:
Istanbul coverage provider for Vitest
269 lines (264 loc) • 9.7 kB
JavaScript
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 };