UNPKG

4.07 kBJavaScriptView Raw
1// @ts-check
2
3import v8Coverage from "@bcoe/v8-coverage";
4import fs from "fs";
5import { join } from "path";
6import { fileURLToPath } from "url";
7
8import sourceRange from "./sourceRange.mjs";
9
10/**
11 * Analyzes
12 * [Node.js generated V8 JavaScript code coverage data](https://nodejs.org/api/cli.html#cli_node_v8_coverage_dir)
13 * in a directory; useful for reporting.
14 * @param {string} coverageDirPath Code coverage data directory path.
15 * @returns {Promise<CoverageAnalysis>} Resolves the coverage analysis.
16 */
17export default async function analyseCoverage(coverageDirPath) {
18 if (typeof coverageDirPath !== "string")
19 throw new TypeError("Argument 1 `coverageDirPath` must be a string.");
20
21 const coverageDirFileNames = await fs.promises.readdir(coverageDirPath);
22 const filteredProcessCoverages = [];
23
24 for (const fileName of coverageDirFileNames)
25 if (fileName.startsWith("coverage-"))
26 filteredProcessCoverages.push(
27 fs.promises
28 .readFile(join(coverageDirPath, fileName), "utf8")
29 .then((coverageFileJson) => {
30 /** @type {import("@bcoe/v8-coverage").ProcessCov} */
31 const { result } = JSON.parse(coverageFileJson);
32 return {
33 // For performance, filtering happens as early as possible.
34 result: result.filter(
35 ({ url }) =>
36 // Exclude Node.js internals, keeping only files.
37 url.startsWith("file://") &&
38 // Exclude `node_modules` directory files.
39 !url.includes("/node_modules/") &&
40 // Exclude `test` directory files.
41 !url.includes("/test/") &&
42 // Exclude files with `.test` prefixed before the extension.
43 !/\.test\.\w+$/u.test(url) &&
44 // Exclude files named `test` (regardless of extension).
45 !/\/test\.\w+$/u.test(url)
46 ),
47 };
48 })
49 );
50
51 const mergedCoverage = v8Coverage.mergeProcessCovs(
52 await Promise.all(filteredProcessCoverages)
53 );
54
55 /** @type {CoverageAnalysis} */
56 const analysis = {
57 filesCount: 0,
58 covered: [],
59 ignored: [],
60 uncovered: [],
61 };
62
63 for (const { url, functions } of mergedCoverage.result) {
64 analysis.filesCount++;
65
66 const path = fileURLToPath(url);
67 const uncoveredRanges = [];
68
69 for (const { ranges } of functions)
70 for (const range of ranges) if (!range.count) uncoveredRanges.push(range);
71
72 if (uncoveredRanges.length) {
73 const source = await fs.promises.readFile(path, "utf8");
74 const ignored = [];
75 const uncovered = [];
76
77 for (const range of uncoveredRanges) {
78 const sourceCodeRange = sourceRange(
79 source,
80 range.startOffset,
81 // The coverage data end offset is the first character after the
82 // range. For reporting to a user, it’s better to show the range as
83 // only the included characters.
84 range.endOffset - 1
85 );
86
87 if (sourceCodeRange.ignore) ignored.push(sourceCodeRange);
88 else uncovered.push(sourceCodeRange);
89 }
90
91 if (ignored.length) analysis.ignored.push({ path, ranges: ignored });
92 if (uncovered.length)
93 analysis.uncovered.push({ path, ranges: uncovered });
94 } else analysis.covered.push(path);
95 }
96
97 return analysis;
98}
99
100/**
101 * [Node.js generated V8 JavaScript code coverage data](https://nodejs.org/api/cli.html#cli_node_v8_coverage_dir)
102 * analysis; useful for reporting.
103 * @typedef {object} CoverageAnalysis
104 * @prop {number} filesCount Number of files analyzed.
105 * @prop {Array<string>} covered Covered file absolute paths.
106 * @prop {Array<SourceCodeRanges>} ignored Ignored source code ranges.
107 * @prop {Array<SourceCodeRanges>} uncovered Uncovered source code ranges.
108 */
109
110/**
111 * A source code file with ranges of interest.
112 * @typedef {object} SourceCodeRanges
113 * @prop {string} path File absolute path.
114 * @prop {Array<import("./sourceRange.mjs").SourceCodeRange>} ranges Ranges of
115 * interest.
116 */