UNPKG

@plugjs/cov8

Version:

V8 Coverage Plugin for the PlugJS Build System ==============================================

210 lines (209 loc) 7.4 kB
// analysis.ts import { fileURLToPath, pathToFileURL } from "node:url"; import { assert } from "@plugjs/plug/asserts"; import { readFile } from "@plugjs/plug/fs"; import { $gry, $p } from "@plugjs/plug/logging"; import { SourceMapConsumer } from "source-map"; var CoverageAnalyserImpl = class { constructor(_log) { this._log = _log; } }; var CoverageResultAnalyser = class extends CoverageAnalyserImpl { constructor(log, _result) { super(log); this._result = _result; const _coverage = []; for (const coveredFunction of _result.functions) { for (const range of coveredFunction.ranges) { for (let i = range.startOffset; i < range.endOffset; i++) { _coverage[i] = range.count; } } } this._coverage = _coverage; } /** Number of passes at each character in the result */ _coverage; /** Internal private field for init/_lineLengths getter */ _lineLengths; async init() { const filename = fileURLToPath(this._result.url); const source = await readFile(filename, "utf-8"); this._lineLengths = source.split("\n").map((line) => line.length); return this; } destroy() { } /** Return the number of coverage passes for the given location */ coverage(source, line, column) { assert(this._lineLengths, "Analyser not initialized"); assert(source === this._result.url, `Wrong source ${source} (should be ${this._result.url})`); const { _lineLengths, _coverage } = this; let offset = 0; for (let l = line - 2; l >= 0; l--) offset += _lineLengths[l] + 1; return _coverage[offset + column] || 0; } }; var CoverageSitemapAnalyser = class extends CoverageResultAnalyser { constructor(log, result, _sourceMapCache, _sourceMapBias) { super(log, result); this._sourceMapCache = _sourceMapCache; this._sourceMapBias = _sourceMapBias; this._lineLengths = _sourceMapCache.lineLengths; } _preciseMappings = /* @__PURE__ */ new Map(); _sourceMap; _key(source, line, column) { return `${line}:${column}:${source}`; } async init() { const sourceMap = this._sourceMapCache.data; assert(sourceMap, "Missing source map data from cache"); this._sourceMap = await new SourceMapConsumer(sourceMap); if (this._sourceMapBias === "none") { this._sourceMap.eachMapping((m) => { const location = { line: m.generatedLine, column: m.generatedColumn }; const key = this._key(m.source, m.originalLine, m.originalColumn); this._preciseMappings.set(key, location); }); } return this; } destroy() { this._sourceMap?.destroy(); } coverage(source, line, column) { assert(this._sourceMap, "Analyser not initialized"); if (this._sourceMapBias === "none") { const key = this._key(source, line, column); const location = this._preciseMappings.get(key); if (!location) { this._log.debug(`No precise mapping for ${source}:${line}:${column}`); return 0; } else { return super.coverage(this._result.url, location.line, location.column); } } const bias = mapBias(this._sourceMapBias); const generated = this._sourceMap.generatedPositionFor({ source, line, column, bias }); if (!generated) { this._log.debug(`No position generated for ${source}:${line}:${column}`); return 0; } if (generated.line == null) { this._log.debug(`No line generated for ${source}:${line}:${column}`); return 0; } if (generated.column == null) { this._log.debug(`No column generated for ${source}:${line}:${column}`); return 0; } return super.coverage(this._result.url, generated.line, generated.column); } }; function mapBias(bias) { if (bias === "greatest_lower_bound") return SourceMapConsumer.GREATEST_LOWER_BOUND; if (bias === "least_upper_bound") return SourceMapConsumer.LEAST_UPPER_BOUND; return void 0; } function combineCoverage(analysers, source, line, column) { let coverage = 0; if (!analysers) return coverage; for (const analyser of analysers) { coverage += analyser.coverage(source, line, column); } return coverage; } var SourcesCoverageAnalyser = class extends CoverageAnalyserImpl { constructor(log, _filename) { super(log); this._filename = _filename; } _mappings = /* @__PURE__ */ new Map(); hasMappings() { return this._mappings.size > 0; } add(source, analyser) { const analysers = this._mappings.get(source) || /* @__PURE__ */ new Set(); analysers.add(analyser); this._mappings.set(source, analysers); } async init() { this._log.debug("SourcesCoverageAnalyser", $p(this._filename), $gry(`(${this._mappings.size} mappings)`)); for (const analysers of this._mappings.values()) { for (const analyser of analysers) { await analyser.init(); } } return this; } destroy() { for (const analysers of this._mappings.values()) { for (const analyser of analysers) { analyser.destroy(); } } } coverage(source, line, column) { const analysers = this._mappings.get(source); return combineCoverage(analysers, source, line, column); } }; var CombiningCoverageAnalyser = class extends CoverageAnalyserImpl { _analysers = /* @__PURE__ */ new Set(); add(analyser) { this._analysers.add(analyser); } async init() { this._log.debug("CombiningCoverageAnalyser", $gry(`(${this._analysers.size} analysers)`)); this._log.enter(); try { for (const analyser of this._analysers) await analyser.init(); return this; } finally { this._log.leave(); } } destroy() { for (const analyser of this._analysers) analyser.destroy(); } coverage(source, line, column) { return combineCoverage(this._analysers, source, line, column); } }; async function createAnalyser(sourceFiles, coverageFiles, sourceMapBias, log) { const urls = sourceFiles.map((path) => pathToFileURL(path).toString()); const analyser = new CombiningCoverageAnalyser(log); for await (const coverageFile of coverageFiles) { const coverageFileAnalyser = new SourcesCoverageAnalyser(log, coverageFile); log.info("Parsing coverage file", $p(coverageFile)); const contents = await readFile(coverageFile, "utf-8"); const coverage = JSON.parse(contents); for (const result of coverage.result) { if (!result.url.startsWith("node:")) { log.debug("Found coverage data for", result.url); } const mapping = coverage["source-map-cache"]?.[result.url]; if (mapping) { log.debug("Found source mapping for", result.url, mapping.data); const matches = urls.filter((url) => mapping.data?.sources.includes(url)); if (matches.length) { log.debug("Found source mapping matches", matches); const sourceAnalyser = new CoverageSitemapAnalyser(log, result, mapping, sourceMapBias); for (const match of matches) coverageFileAnalyser.add(match, sourceAnalyser); } } else if (urls.includes(result.url)) { coverageFileAnalyser.add(result.url, new CoverageResultAnalyser(log, result)); } } if (coverageFileAnalyser.hasMappings()) analyser.add(coverageFileAnalyser); } return await analyser.init(); } export { CombiningCoverageAnalyser, SourcesCoverageAnalyser, createAnalyser }; //# sourceMappingURL=analysis.mjs.map