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