@vitest/coverage-v8 1.0.0-beta.5 → 1.0.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/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { V8CoverageProvider } from './provider.js';
2
- import 'node:inspector';
3
2
  import 'vitest/coverage';
4
3
  import 'vitest';
5
4
  import 'vitest/node';
@@ -1,4 +1,3 @@
1
- import { Profiler } from 'node:inspector';
2
1
  import { BaseCoverageProvider } from 'vitest/coverage';
3
2
  import { CoverageProvider, AfterSuiteRunMeta, ReportContext, ResolvedCoverageOptions } from 'vitest';
4
3
  import { Vitest } from 'vitest/node';
@@ -17,8 +16,8 @@ interface TestExclude {
17
16
  };
18
17
  }
19
18
  type Options = ResolvedCoverageOptions<'v8'>;
20
- type RawCoverage = Profiler.TakePreciseCoverageReturnType;
21
- type CoverageByTransformMode = Record<AfterSuiteRunMeta['transformMode'], RawCoverage[]>;
19
+ type Filename = string;
20
+ type CoverageFilesByTransformMode = Record<AfterSuiteRunMeta['transformMode'], Filename[]>;
22
21
  type ProjectName = NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT;
23
22
  declare const DEFAULT_PROJECT: unique symbol;
24
23
  declare class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider {
@@ -26,7 +25,9 @@ declare class V8CoverageProvider extends BaseCoverageProvider implements Coverag
26
25
  ctx: Vitest;
27
26
  options: Options;
28
27
  testExclude: InstanceType<TestExclude>;
29
- coverages: Map<ProjectName, CoverageByTransformMode>;
28
+ coverageFiles: Map<ProjectName, CoverageFilesByTransformMode>;
29
+ coverageFilesDirectory: string;
30
+ pendingPromises: Promise<void>[];
30
31
  initialize(ctx: Vitest): void;
31
32
  resolveOptions(): Options;
32
33
  clean(clean?: boolean): Promise<void>;
@@ -34,7 +35,7 @@ declare class V8CoverageProvider extends BaseCoverageProvider implements Coverag
34
35
  reportCoverage({ allTestsRun }?: ReportContext): Promise<void>;
35
36
  private getUntestedFiles;
36
37
  private getSources;
37
- private mergeAndTransformCoverage;
38
+ private convertCoverage;
38
39
  }
39
40
 
40
41
  export { V8CoverageProvider };
package/dist/provider.js CHANGED
@@ -11,6 +11,7 @@ import { parseModule } from 'magicast';
11
11
  import remapping from '@ampproject/remapping';
12
12
  import c from 'picocolors';
13
13
  import { provider } from 'std-env';
14
+ import createDebug from 'debug';
14
15
  import { builtinModules } from 'node:module';
15
16
  import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config';
16
17
  import { BaseCoverageProvider } from 'vitest/coverage';
@@ -170,12 +171,16 @@ function cleanUrl(url) {
170
171
  const WRAPPER_LENGTH = 185;
171
172
  const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g;
172
173
  const DEFAULT_PROJECT = Symbol.for("default-project");
174
+ const debug = createDebug("vitest:coverage");
175
+ let uniqueId = 0;
173
176
  class V8CoverageProvider extends BaseCoverageProvider {
174
177
  name = "v8";
175
178
  ctx;
176
179
  options;
177
180
  testExclude;
178
- coverages = /* @__PURE__ */ new Map();
181
+ coverageFiles = /* @__PURE__ */ new Map();
182
+ coverageFilesDirectory;
183
+ pendingPromises = [];
179
184
  initialize(ctx) {
180
185
  const config = ctx.config.coverage;
181
186
  this.ctx = ctx;
@@ -203,6 +208,7 @@ class V8CoverageProvider extends BaseCoverageProvider {
203
208
  extension: this.options.extension,
204
209
  relativePath: !this.options.allowExternal
205
210
  });
211
+ this.coverageFilesDirectory = resolve(this.options.reportsDirectory, ".tmp");
206
212
  }
207
213
  resolveOptions() {
208
214
  return this.options;
@@ -210,7 +216,11 @@ class V8CoverageProvider extends BaseCoverageProvider {
210
216
  async clean(clean = true) {
211
217
  if (clean && existsSync(this.options.reportsDirectory))
212
218
  await promises.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 });
213
- this.coverages = /* @__PURE__ */ new Map();
219
+ if (existsSync(this.coverageFilesDirectory))
220
+ await promises.rm(this.coverageFilesDirectory, { recursive: true, force: true, maxRetries: 10 });
221
+ await promises.mkdir(this.coverageFilesDirectory, { recursive: true });
222
+ this.coverageFiles = /* @__PURE__ */ new Map();
223
+ this.pendingPromises = [];
214
224
  }
215
225
  /*
216
226
  * Coverage and meta information passed from Vitest runners.
@@ -220,29 +230,49 @@ class V8CoverageProvider extends BaseCoverageProvider {
220
230
  onAfterSuiteRun({ coverage, transformMode, projectName }) {
221
231
  if (transformMode !== "web" && transformMode !== "ssr")
222
232
  throw new Error(`Invalid transform mode: ${transformMode}`);
223
- let entry = this.coverages.get(projectName || DEFAULT_PROJECT);
233
+ let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT);
224
234
  if (!entry) {
225
235
  entry = { web: [], ssr: [] };
226
- this.coverages.set(projectName || DEFAULT_PROJECT, entry);
236
+ this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry);
227
237
  }
228
- entry[transformMode].push(coverage);
238
+ const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`);
239
+ entry[transformMode].push(filename);
240
+ const promise = promises.writeFile(filename, JSON.stringify(coverage), "utf-8");
241
+ this.pendingPromises.push(promise);
229
242
  }
230
243
  async reportCoverage({ allTestsRun } = {}) {
231
244
  if (provider === "stackblitz")
232
245
  this.ctx.logger.log(c.blue(" % ") + c.yellow("@vitest/coverage-v8 does not work on Stackblitz. Report will be empty."));
233
- const coverageMaps = await Promise.all(
234
- Array.from(this.coverages.entries()).map(([projectName, coverages]) => [
235
- this.mergeAndTransformCoverage(coverages.ssr, projectName, "ssr"),
236
- this.mergeAndTransformCoverage(coverages.web, projectName, "web")
237
- ]).flat()
238
- );
246
+ const coverageMap = libCoverage.createCoverageMap({});
247
+ let index = 0;
248
+ const total = this.pendingPromises.length;
249
+ await Promise.all(this.pendingPromises);
250
+ this.pendingPromises = [];
251
+ for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) {
252
+ for (const [transformMode, filenames] of Object.entries(coveragePerProject)) {
253
+ let merged = { result: [] };
254
+ for (const chunk of toSlices(filenames, this.options.processingConcurrency)) {
255
+ if (debug.enabled) {
256
+ index += chunk.length;
257
+ debug("Covered files %d/%d", index, total);
258
+ }
259
+ await Promise.all(chunk.map(async (filename) => {
260
+ const contents = await promises.readFile(filename, "utf-8");
261
+ const coverage = JSON.parse(contents);
262
+ merged = mergeProcessCovs([merged, coverage]);
263
+ }));
264
+ }
265
+ const converted = await this.convertCoverage(merged, projectName, transformMode);
266
+ const transformedCoverage = await transformCoverage(converted);
267
+ coverageMap.merge(transformedCoverage);
268
+ }
269
+ }
239
270
  if (this.options.all && allTestsRun) {
240
- const coveredFiles = coverageMaps.map((map) => map.files()).flat();
271
+ const coveredFiles = coverageMap.files();
241
272
  const untestedCoverage = await this.getUntestedFiles(coveredFiles);
242
- const untestedCoverageResults = untestedCoverage.map((files) => ({ result: [files] }));
243
- coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults));
273
+ const converted = await this.convertCoverage(untestedCoverage);
274
+ coverageMap.merge(await transformCoverage(converted));
244
275
  }
245
- const coverageMap = mergeCoverageMaps(...coverageMaps);
246
276
  const context = libReport.createContext({
247
277
  dir: this.options.reportsDirectory,
248
278
  coverageMap,
@@ -281,30 +311,43 @@ class V8CoverageProvider extends BaseCoverageProvider {
281
311
  }
282
312
  });
283
313
  }
314
+ this.coverageFiles = /* @__PURE__ */ new Map();
315
+ await promises.rm(this.coverageFilesDirectory, { recursive: true });
284
316
  }
285
317
  }
286
318
  async getUntestedFiles(testedFiles) {
287
319
  const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache);
288
320
  const includedFiles = await this.testExclude.glob(this.ctx.config.root);
289
321
  const uncoveredFiles = includedFiles.map((file) => pathToFileURL(resolve(this.ctx.config.root, file))).filter((file) => !testedFiles.includes(file.pathname));
290
- return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => {
291
- const { source } = await this.getSources(uncoveredFile.href, transformResults);
292
- return {
293
- url: uncoveredFile.href,
294
- scriptId: "0",
295
- // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps.
296
- functions: [{
297
- ranges: [{
298
- startOffset: 0,
299
- endOffset: source.length,
300
- count: 0
301
- }],
302
- isBlockCoverage: true,
303
- // 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
304
- functionName: "(empty-report)"
305
- }]
306
- };
307
- }));
322
+ let merged = { result: [] };
323
+ let index = 0;
324
+ for (const chunk of toSlices(uncoveredFiles, this.options.processingConcurrency)) {
325
+ if (debug.enabled) {
326
+ index += chunk.length;
327
+ debug("Uncovered files %d/%d", index, uncoveredFiles.length);
328
+ }
329
+ const coverages = await Promise.all(chunk.map(async (filename) => {
330
+ const { source } = await this.getSources(filename.href, transformResults);
331
+ const coverage = {
332
+ url: filename.href,
333
+ scriptId: "0",
334
+ // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps.
335
+ functions: [{
336
+ ranges: [{
337
+ startOffset: 0,
338
+ endOffset: source.length,
339
+ count: 0
340
+ }],
341
+ isBlockCoverage: true,
342
+ // 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
343
+ functionName: "(empty-report)"
344
+ }]
345
+ };
346
+ return { result: [coverage] };
347
+ }));
348
+ merged = mergeProcessCovs([merged, ...coverages]);
349
+ }
350
+ return merged;
308
351
  }
309
352
  async getSources(url, transformResults, functions = []) {
310
353
  const filePath = normalize(fileURLToPath(url));
@@ -330,31 +373,33 @@ class V8CoverageProvider extends BaseCoverageProvider {
330
373
  }
331
374
  };
332
375
  }
333
- async mergeAndTransformCoverage(coverages, projectName, transformMode) {
376
+ async convertCoverage(coverage, projectName, transformMode) {
334
377
  const viteNode = this.ctx.projects.find((project) => project.getName() === projectName)?.vitenode || this.ctx.vitenode;
335
378
  const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache;
336
379
  const transformResults = normalizeTransformResults(fetchCache);
337
- const merged = mergeProcessCovs(coverages);
338
- const scriptCoverages = merged.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url)));
339
- const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
340
- const sources = await this.getSources(url, transformResults, functions);
341
- const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0;
342
- const converter = v8ToIstanbul(url, wrapperLength, sources);
343
- await converter.load();
344
- converter.applyCoverage(functions);
345
- return converter.toIstanbul();
346
- }));
347
- const mergedCoverage = mergeCoverageMaps(...converted);
348
- const sourceMapStore = libSourceMaps.createSourceMapStore();
349
- return sourceMapStore.transformCoverage(mergedCoverage);
380
+ const scriptCoverages = coverage.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url)));
381
+ const coverageMap = libCoverage.createCoverageMap({});
382
+ let index = 0;
383
+ for (const chunk of toSlices(scriptCoverages, this.options.processingConcurrency)) {
384
+ if (debug.enabled) {
385
+ index += chunk.length;
386
+ debug("Converting %d/%d", index, scriptCoverages.length);
387
+ }
388
+ await Promise.all(chunk.map(async ({ url, functions }) => {
389
+ const sources = await this.getSources(url, transformResults, functions);
390
+ const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0;
391
+ const converter = v8ToIstanbul(url, wrapperLength, sources);
392
+ await converter.load();
393
+ converter.applyCoverage(functions);
394
+ coverageMap.merge(converter.toIstanbul());
395
+ }));
396
+ }
397
+ return coverageMap;
350
398
  }
351
399
  }
352
- function mergeCoverageMaps(...coverageMaps) {
353
- return coverageMaps.reduce((coverage, previousCoverageMap) => {
354
- const map = libCoverage.createCoverageMap(coverage);
355
- map.merge(previousCoverageMap);
356
- return map;
357
- }, libCoverage.createCoverageMap({}));
400
+ async function transformCoverage(coverageMap) {
401
+ const sourceMapStore = libSourceMaps.createSourceMapStore();
402
+ return await sourceMapStore.transformCoverage(coverageMap);
358
403
  }
359
404
  function removeViteHelpersFromSourceMaps(source, map) {
360
405
  if (!source || !source.match(VITE_EXPORTS_LINE_PATTERN))
@@ -388,5 +433,17 @@ function normalizeTransformResults(fetchCache) {
388
433
  function hasTerminalReporter(reporters) {
389
434
  return reporters.some(([reporter]) => reporter === "text" || reporter === "text-summary" || reporter === "text-lcov" || reporter === "teamcity");
390
435
  }
436
+ function toSlices(array, size) {
437
+ return array.reduce((chunks, item) => {
438
+ const index = Math.max(0, chunks.length - 1);
439
+ const lastChunk = chunks[index] || [];
440
+ chunks[index] = lastChunk;
441
+ if (lastChunk.length >= size)
442
+ chunks.push([item]);
443
+ else
444
+ lastChunk.push(item);
445
+ return chunks;
446
+ }, []);
447
+ }
391
448
 
392
449
  export { V8CoverageProvider };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/coverage-v8",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.5",
4
+ "version": "1.0.0",
5
5
  "description": "V8 coverage provider for Vitest",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "dependencies": {
43
43
  "@ampproject/remapping": "^2.2.1",
44
44
  "@bcoe/v8-coverage": "^0.2.3",
45
+ "debug": "^4.3.4",
45
46
  "istanbul-lib-coverage": "^3.2.2",
46
47
  "istanbul-lib-report": "^3.0.1",
47
48
  "istanbul-lib-source-maps": "^4.0.1",
@@ -49,18 +50,19 @@
49
50
  "magic-string": "^0.30.5",
50
51
  "magicast": "^0.3.2",
51
52
  "picocolors": "^1.0.0",
52
- "std-env": "^3.4.3",
53
+ "std-env": "^3.5.0",
53
54
  "test-exclude": "^6.0.0",
54
- "v8-to-istanbul": "^9.1.3"
55
+ "v8-to-istanbul": "^9.2.0"
55
56
  },
56
57
  "devDependencies": {
58
+ "@types/debug": "^4.1.12",
57
59
  "@types/istanbul-lib-coverage": "^2.0.6",
58
60
  "@types/istanbul-lib-report": "^3.0.3",
59
61
  "@types/istanbul-lib-source-maps": "^4.0.4",
60
62
  "@types/istanbul-reports": "^3.0.4",
61
63
  "pathe": "^1.1.1",
62
- "vite-node": "1.0.0-beta.5",
63
- "vitest": "1.0.0-beta.5"
64
+ "vite-node": "1.0.0",
65
+ "vitest": "1.0.0"
64
66
  },
65
67
  "scripts": {
66
68
  "build": "rimraf dist && rollup -c",