@vitest/coverage-v8 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-Present Anthony Fu <https://github.com/antfu>
4
+ Copyright (c) 2021-Present Matias Capeletto <https://github.com/patak-dev>
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ import { V8CoverageProvider } from './provider.js';
2
+ import 'node:inspector';
3
+ import 'vitest/coverage';
4
+ import 'vitest';
5
+ import 'vitest/node';
6
+
7
+ declare const _default: {
8
+ getProvider(): Promise<V8CoverageProvider>;
9
+ startCoverage(): void;
10
+ takeCoverage(): Promise<unknown>;
11
+ stopCoverage(): void;
12
+ };
13
+
14
+ export { _default as default };
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import inspector from 'node:inspector';
2
+ import { provider } from 'std-env';
3
+
4
+ const session = new inspector.Session();
5
+ function startCoverage() {
6
+ session.connect();
7
+ session.post("Profiler.enable");
8
+ session.post("Profiler.startPreciseCoverage", {
9
+ callCount: true,
10
+ detailed: true
11
+ });
12
+ }
13
+ async function takeCoverage() {
14
+ return new Promise((resolve, reject) => {
15
+ session.post("Profiler.takePreciseCoverage", async (error, coverage) => {
16
+ if (error)
17
+ return reject(error);
18
+ const result = coverage.result.filter(filterResult);
19
+ resolve({ result });
20
+ });
21
+ if (provider === "stackblitz")
22
+ resolve({ result: [] });
23
+ });
24
+ }
25
+ function stopCoverage() {
26
+ session.post("Profiler.stopPreciseCoverage");
27
+ session.post("Profiler.disable");
28
+ session.disconnect();
29
+ }
30
+ function filterResult(coverage) {
31
+ if (!coverage.url.startsWith("file://"))
32
+ return false;
33
+ if (coverage.url.includes("/node_modules/"))
34
+ return false;
35
+ return true;
36
+ }
37
+
38
+ var coverage = /*#__PURE__*/Object.freeze({
39
+ __proto__: null,
40
+ startCoverage: startCoverage,
41
+ stopCoverage: stopCoverage,
42
+ takeCoverage: takeCoverage
43
+ });
44
+
45
+ var index = {
46
+ ...coverage,
47
+ async getProvider() {
48
+ const name = "./provider.js";
49
+ const { V8CoverageProvider } = await import(name);
50
+ return new V8CoverageProvider();
51
+ }
52
+ };
53
+
54
+ export { index as default };
@@ -0,0 +1,34 @@
1
+ import { Profiler } from 'node:inspector';
2
+ import { BaseCoverageProvider } from 'vitest/coverage';
3
+ import { CoverageProvider, AfterSuiteRunMeta, ReportContext, ResolvedCoverageOptions } from 'vitest';
4
+ import { Vitest } from 'vitest/node';
5
+
6
+ interface TestExclude {
7
+ new (opts: {
8
+ cwd?: string | string[];
9
+ include?: string | string[];
10
+ exclude?: string | string[];
11
+ extension?: string | string[];
12
+ excludeNodeModules?: boolean;
13
+ }): {
14
+ shouldInstrument(filePath: string): boolean;
15
+ glob(cwd: string): Promise<string[]>;
16
+ };
17
+ }
18
+ type Options = ResolvedCoverageOptions<'v8'>;
19
+ declare class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider {
20
+ name: string;
21
+ ctx: Vitest;
22
+ options: Options;
23
+ testExclude: InstanceType<TestExclude>;
24
+ coverages: Profiler.TakePreciseCoverageReturnType[];
25
+ initialize(ctx: Vitest): void;
26
+ resolveOptions(): Options;
27
+ clean(clean?: boolean): Promise<void>;
28
+ onAfterSuiteRun({ coverage }: AfterSuiteRunMeta): void;
29
+ reportCoverage({ allTestsRun }?: ReportContext): Promise<void>;
30
+ private getUntestedFiles;
31
+ private getSources;
32
+ }
33
+
34
+ export { V8CoverageProvider };
@@ -0,0 +1,313 @@
1
+ import { existsSync, promises } from 'fs';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import v8ToIstanbul from 'v8-to-istanbul';
4
+ import { mergeProcessCovs } from '@bcoe/v8-coverage';
5
+ import libReport from 'istanbul-lib-report';
6
+ import reports from 'istanbul-reports';
7
+ import libCoverage from 'istanbul-lib-coverage';
8
+ import libSourceMaps from 'istanbul-lib-source-maps';
9
+ import MagicString from 'magic-string';
10
+ import remapping from '@ampproject/remapping';
11
+ import c from 'picocolors';
12
+ import { provider } from 'std-env';
13
+ import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config';
14
+ import { BaseCoverageProvider } from 'vitest/coverage';
15
+ import _TestExclude from 'test-exclude';
16
+
17
+ function normalizeWindowsPath(input = "") {
18
+ if (!input || !input.includes("\\")) {
19
+ return input;
20
+ }
21
+ return input.replace(/\\/g, "/");
22
+ }
23
+
24
+ const _UNC_REGEX = /^[/\\]{2}/;
25
+ const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
26
+ const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
27
+ const normalize = function(path) {
28
+ if (path.length === 0) {
29
+ return ".";
30
+ }
31
+ path = normalizeWindowsPath(path);
32
+ const isUNCPath = path.match(_UNC_REGEX);
33
+ const isPathAbsolute = isAbsolute(path);
34
+ const trailingSeparator = path[path.length - 1] === "/";
35
+ path = normalizeString(path, !isPathAbsolute);
36
+ if (path.length === 0) {
37
+ if (isPathAbsolute) {
38
+ return "/";
39
+ }
40
+ return trailingSeparator ? "./" : ".";
41
+ }
42
+ if (trailingSeparator) {
43
+ path += "/";
44
+ }
45
+ if (_DRIVE_LETTER_RE.test(path)) {
46
+ path += "/";
47
+ }
48
+ if (isUNCPath) {
49
+ if (!isPathAbsolute) {
50
+ return `//./${path}`;
51
+ }
52
+ return `//${path}`;
53
+ }
54
+ return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;
55
+ };
56
+ function cwd() {
57
+ if (typeof process !== "undefined") {
58
+ return process.cwd().replace(/\\/g, "/");
59
+ }
60
+ return "/";
61
+ }
62
+ const resolve = function(...arguments_) {
63
+ arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
64
+ let resolvedPath = "";
65
+ let resolvedAbsolute = false;
66
+ for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
67
+ const path = index >= 0 ? arguments_[index] : cwd();
68
+ if (!path || path.length === 0) {
69
+ continue;
70
+ }
71
+ resolvedPath = `${path}/${resolvedPath}`;
72
+ resolvedAbsolute = isAbsolute(path);
73
+ }
74
+ resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
75
+ if (resolvedAbsolute && !isAbsolute(resolvedPath)) {
76
+ return `/${resolvedPath}`;
77
+ }
78
+ return resolvedPath.length > 0 ? resolvedPath : ".";
79
+ };
80
+ function normalizeString(path, allowAboveRoot) {
81
+ let res = "";
82
+ let lastSegmentLength = 0;
83
+ let lastSlash = -1;
84
+ let dots = 0;
85
+ let char = null;
86
+ for (let index = 0; index <= path.length; ++index) {
87
+ if (index < path.length) {
88
+ char = path[index];
89
+ } else if (char === "/") {
90
+ break;
91
+ } else {
92
+ char = "/";
93
+ }
94
+ if (char === "/") {
95
+ if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) {
96
+ if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
97
+ if (res.length > 2) {
98
+ const lastSlashIndex = res.lastIndexOf("/");
99
+ if (lastSlashIndex === -1) {
100
+ res = "";
101
+ lastSegmentLength = 0;
102
+ } else {
103
+ res = res.slice(0, lastSlashIndex);
104
+ lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
105
+ }
106
+ lastSlash = index;
107
+ dots = 0;
108
+ continue;
109
+ } else if (res.length > 0) {
110
+ res = "";
111
+ lastSegmentLength = 0;
112
+ lastSlash = index;
113
+ dots = 0;
114
+ continue;
115
+ }
116
+ }
117
+ if (allowAboveRoot) {
118
+ res += res.length > 0 ? "/.." : "..";
119
+ lastSegmentLength = 2;
120
+ }
121
+ } else {
122
+ if (res.length > 0) {
123
+ res += `/${path.slice(lastSlash + 1, index)}`;
124
+ } else {
125
+ res = path.slice(lastSlash + 1, index);
126
+ }
127
+ lastSegmentLength = index - lastSlash - 1;
128
+ }
129
+ lastSlash = index;
130
+ dots = 0;
131
+ } else if (char === "." && dots !== -1) {
132
+ ++dots;
133
+ } else {
134
+ dots = -1;
135
+ }
136
+ }
137
+ return res;
138
+ }
139
+ const isAbsolute = function(p) {
140
+ return _IS_ABSOLUTE_RE.test(p);
141
+ };
142
+
143
+ const WRAPPER_LENGTH = 185;
144
+ const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g;
145
+ class V8CoverageProvider extends BaseCoverageProvider {
146
+ constructor() {
147
+ super(...arguments);
148
+ this.name = "v8";
149
+ this.coverages = [];
150
+ }
151
+ initialize(ctx) {
152
+ const config = ctx.config.coverage;
153
+ this.ctx = ctx;
154
+ this.options = {
155
+ ...coverageConfigDefaults,
156
+ // User's options
157
+ ...config,
158
+ // Resolved fields
159
+ provider: "v8",
160
+ reporter: this.resolveReporters(config.reporter || coverageConfigDefaults.reporter),
161
+ reportsDirectory: resolve(ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory),
162
+ lines: config["100"] ? 100 : config.lines,
163
+ functions: config["100"] ? 100 : config.functions,
164
+ branches: config["100"] ? 100 : config.branches,
165
+ statements: config["100"] ? 100 : config.statements
166
+ };
167
+ this.testExclude = new _TestExclude({
168
+ cwd: ctx.config.root,
169
+ include: typeof this.options.include === "undefined" ? void 0 : [...this.options.include],
170
+ exclude: [...defaultExclude, ...defaultInclude, ...this.options.exclude],
171
+ excludeNodeModules: true,
172
+ extension: this.options.extension
173
+ });
174
+ }
175
+ resolveOptions() {
176
+ return this.options;
177
+ }
178
+ async clean(clean = true) {
179
+ if (clean && existsSync(this.options.reportsDirectory))
180
+ await promises.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 });
181
+ this.coverages = [];
182
+ }
183
+ onAfterSuiteRun({ coverage }) {
184
+ this.coverages.push(coverage);
185
+ }
186
+ async reportCoverage({ allTestsRun } = {}) {
187
+ if (provider === "stackblitz")
188
+ this.ctx.logger.log(c.blue(" % ") + c.yellow("@vitest/coverage-v8 does not work on Stackblitz. Report will be empty."));
189
+ const merged = mergeProcessCovs(this.coverages);
190
+ const scriptCoverages = merged.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url)));
191
+ if (this.options.all && allTestsRun) {
192
+ const coveredFiles = Array.from(scriptCoverages.map((r) => r.url));
193
+ const untestedFiles = await this.getUntestedFiles(coveredFiles);
194
+ scriptCoverages.push(...untestedFiles);
195
+ }
196
+ const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
197
+ const sources = await this.getSources(url);
198
+ const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0;
199
+ const converter = v8ToIstanbul(url, wrapperLength, sources);
200
+ await converter.load();
201
+ converter.applyCoverage(functions);
202
+ return converter.toIstanbul();
203
+ }));
204
+ const mergedCoverage = converted.reduce((coverage, previousCoverageMap) => {
205
+ const map = libCoverage.createCoverageMap(coverage);
206
+ map.merge(previousCoverageMap);
207
+ return map;
208
+ }, libCoverage.createCoverageMap({}));
209
+ const sourceMapStore = libSourceMaps.createSourceMapStore();
210
+ const coverageMap = await sourceMapStore.transformCoverage(mergedCoverage);
211
+ const context = libReport.createContext({
212
+ dir: this.options.reportsDirectory,
213
+ coverageMap,
214
+ sourceFinder: sourceMapStore.sourceFinder,
215
+ watermarks: this.options.watermarks
216
+ });
217
+ for (const reporter of this.options.reporter) {
218
+ reports.create(reporter[0], {
219
+ skipFull: this.options.skipFull,
220
+ projectRoot: this.ctx.config.root,
221
+ ...reporter[1]
222
+ }).execute(context);
223
+ }
224
+ if (this.options.branches || this.options.functions || this.options.lines || this.options.statements) {
225
+ this.checkThresholds({
226
+ coverageMap,
227
+ thresholds: {
228
+ branches: this.options.branches,
229
+ functions: this.options.functions,
230
+ lines: this.options.lines,
231
+ statements: this.options.statements
232
+ },
233
+ perFile: this.options.perFile
234
+ });
235
+ }
236
+ if (this.options.thresholdAutoUpdate && allTestsRun) {
237
+ this.updateThresholds({
238
+ coverageMap,
239
+ thresholds: {
240
+ branches: this.options.branches,
241
+ functions: this.options.functions,
242
+ lines: this.options.lines,
243
+ statements: this.options.statements
244
+ },
245
+ perFile: this.options.perFile,
246
+ configurationFile: this.ctx.server.config.configFile
247
+ });
248
+ }
249
+ }
250
+ async getUntestedFiles(testedFiles) {
251
+ const includedFiles = await this.testExclude.glob(this.ctx.config.root);
252
+ const uncoveredFiles = includedFiles.map((file) => pathToFileURL(resolve(this.ctx.config.root, file))).filter((file) => !testedFiles.includes(file.href));
253
+ return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => {
254
+ const { source } = await this.getSources(uncoveredFile.href);
255
+ return {
256
+ url: uncoveredFile.href,
257
+ scriptId: "0",
258
+ // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps.
259
+ functions: [{
260
+ ranges: [{
261
+ startOffset: 0,
262
+ endOffset: source.length,
263
+ count: 0
264
+ }],
265
+ isBlockCoverage: true,
266
+ // This is magical value that indicates an empty report: https://github.com/istanbuljs/v8-to-istanbul/blob/fca5e6a9e6ef38a9cdc3a178d5a6cf9ef82e6cab/lib/v8-to-istanbul.js#LL131C40-L131C40
267
+ functionName: "(empty-report)"
268
+ }]
269
+ };
270
+ }));
271
+ }
272
+ async getSources(url) {
273
+ var _a;
274
+ const filePath = normalize(fileURLToPath(url));
275
+ const transformResult = this.ctx.projects.map((project) => {
276
+ var _a2;
277
+ return (_a2 = project.vitenode.fetchCache.get(filePath)) == null ? void 0 : _a2.result;
278
+ }).filter(Boolean).shift();
279
+ const map = transformResult == null ? void 0 : transformResult.map;
280
+ const code = transformResult == null ? void 0 : transformResult.code;
281
+ const sourcesContent = ((_a = map == null ? void 0 : map.sourcesContent) == null ? void 0 : _a[0]) || await promises.readFile(filePath, "utf-8");
282
+ if (!map)
283
+ return { source: code || sourcesContent };
284
+ return {
285
+ originalSource: sourcesContent,
286
+ source: code || sourcesContent,
287
+ sourceMap: {
288
+ sourcemap: removeViteHelpersFromSourceMaps(code, {
289
+ ...map,
290
+ version: 3,
291
+ sources: [url],
292
+ sourcesContent: [sourcesContent]
293
+ })
294
+ }
295
+ };
296
+ }
297
+ }
298
+ function removeViteHelpersFromSourceMaps(source, map) {
299
+ if (!source || !source.match(VITE_EXPORTS_LINE_PATTERN))
300
+ return map;
301
+ const sourceWithoutHelpers = new MagicString(source);
302
+ sourceWithoutHelpers.replaceAll(VITE_EXPORTS_LINE_PATTERN, "\n");
303
+ const mapWithoutHelpers = sourceWithoutHelpers.generateMap({
304
+ hires: true
305
+ });
306
+ const combinedMap = remapping(
307
+ [{ ...mapWithoutHelpers, version: 3 }, map],
308
+ () => null
309
+ );
310
+ return combinedMap;
311
+ }
312
+
313
+ export { V8CoverageProvider };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@vitest/coverage-v8",
3
+ "type": "module",
4
+ "version": "0.32.0",
5
+ "description": "V8 coverage provider for Vitest",
6
+ "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
+ "license": "MIT",
8
+ "funding": "https://opencollective.com/vitest",
9
+ "homepage": "https://github.com/vitest-dev/vitest/tree/main/packages/coverage-v8#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/vitest-dev/vitest.git",
13
+ "directory": "packages/coverage-v8"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/vitest-dev/vitest/issues"
17
+ },
18
+ "keywords": [
19
+ "vite",
20
+ "vitest",
21
+ "test",
22
+ "coverage",
23
+ "v8"
24
+ ],
25
+ "sideEffects": false,
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./*": "./*"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "peerDependencies": {
40
+ "vitest": ">=0.32.0 <1"
41
+ },
42
+ "dependencies": {
43
+ "@ampproject/remapping": "^2.2.1",
44
+ "@bcoe/v8-coverage": "^0.2.3",
45
+ "istanbul-lib-coverage": "^3.2.0",
46
+ "istanbul-lib-report": "^3.0.0",
47
+ "istanbul-lib-source-maps": "^4.0.1",
48
+ "istanbul-reports": "^3.1.5",
49
+ "magic-string": "^0.30.0",
50
+ "picocolors": "^1.0.0",
51
+ "std-env": "^3.3.2",
52
+ "test-exclude": "^6.0.0",
53
+ "v8-to-istanbul": "^9.1.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/istanbul-lib-coverage": "^2.0.4",
57
+ "@types/istanbul-lib-report": "^3.0.0",
58
+ "@types/istanbul-lib-source-maps": "^4.0.1",
59
+ "@types/istanbul-reports": "^3.0.1",
60
+ "pathe": "^1.1.0",
61
+ "vite-node": "0.32.0",
62
+ "vitest": "0.32.0"
63
+ },
64
+ "scripts": {
65
+ "build": "rimraf dist && rollup -c",
66
+ "dev": "rollup -c --watch --watch.include 'src/**'"
67
+ }
68
+ }