Skip to content

Commit 6f9e982

Browse files
committed
Add One Double Zero as coverage provider
1 parent 0a0a9f7 commit 6f9e982

File tree

10 files changed

+319
-14
lines changed

10 files changed

+319
-14
lines changed
+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import * as path from 'path';
9+
import runJest from '../runJest';
10+
11+
const DIR = path.resolve(__dirname, '../coverage-provider-v8');
12+
13+
test('prints coverage with missing sourcemaps', () => {
14+
const sourcemapDir = path.join(DIR, 'no-sourcemap');
15+
16+
const {stdout, stderr, exitCode} = runJest(
17+
sourcemapDir,
18+
['--coverage', '--coverage-provider', 'odz'],
19+
{stripAnsi: true},
20+
);
21+
22+
expect(exitCode).toBe(0);
23+
expect(stdout).toEqual(``);
24+
});
25+
26+
test('prints coverage with empty sourcemaps', () => {
27+
const sourcemapDir = path.join(DIR, 'empty-sourcemap');
28+
29+
const {stdout, stderr, exitCode} = runJest(
30+
sourcemapDir,
31+
['--coverage', '--coverage-provider', 'odz'],
32+
{stripAnsi: true},
33+
);
34+
35+
expect(exitCode).toBe(0);
36+
expect(stdout).toEqual(``);
37+
});
38+
39+
test('reports coverage with `resetModules`', () => {
40+
const sourcemapDir = path.join(DIR, 'with-resetModules');
41+
42+
const {stdout, stderr, exitCode} = runJest(
43+
sourcemapDir,
44+
['--coverage', '--coverage-provider', 'odz'],
45+
{stripAnsi: true},
46+
);
47+
48+
expect(stderr).toEqual(``);
49+
expect(exitCode).toBe(0);
50+
expect(stdout).toEqual(``);
51+
});
52+
53+
test('prints correct coverage report, if a CJS module is put under test without transformation', () => {
54+
const sourcemapDir = path.join(DIR, 'cjs-native-without-sourcemap');
55+
56+
const {stdout, stderr, exitCode} = runJest(
57+
sourcemapDir,
58+
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
59+
{stripAnsi: true},
60+
);
61+
62+
expect(stderr).toEqual(``);
63+
expect(exitCode).toBe(0);
64+
expect(stdout).toEqual(``);
65+
});
66+
67+
test('prints correct coverage report, if a TS module is transpiled by Babel to CJS and put under test', () => {
68+
const sourcemapDir = path.join(DIR, 'cjs-with-babel-transformer');
69+
70+
const {stdout, stderr, exitCode} = runJest(
71+
sourcemapDir,
72+
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
73+
{stripAnsi: true},
74+
);
75+
76+
expect(stderr).toEqual(``);
77+
expect(exitCode).toBe(0);
78+
expect(stdout).toEqual(``);
79+
});
80+
81+
test('prints correct coverage report, if an ESM module is put under test without transformation', () => {
82+
const sourcemapDir = path.join(DIR, 'esm-native-without-sourcemap');
83+
84+
const {stdout, stderr, exitCode} = runJest(
85+
sourcemapDir,
86+
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
87+
{
88+
nodeOptions: '--experimental-vm-modules --no-warnings',
89+
stripAnsi: true,
90+
},
91+
);
92+
93+
expect(stderr).toEqual(``);
94+
expect(exitCode).toBe(0);
95+
expect(stdout).toEqual(``);
96+
});
97+
98+
test('prints correct coverage report, if a TS module is transpiled by custom transformer to ESM put under test', () => {
99+
const sourcemapDir = path.join(DIR, 'esm-with-custom-transformer');
100+
101+
const {stdout, stderr, exitCode} = runJest(
102+
sourcemapDir,
103+
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
104+
{
105+
nodeOptions: '--experimental-vm-modules --no-warnings',
106+
stripAnsi: true,
107+
},
108+
);
109+
110+
expect(stderr).toEqual(``);
111+
expect(exitCode).toBe(0);
112+
expect(stdout).toEqual(` console.log
113+
this will print
114+
115+
at covered (module.ts:13:11)
116+
117+
--------------|---------|----------|---------|---------|-------------------
118+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
119+
--------------|---------|----------|---------|---------|-------------------
120+
All files | 62.5 | 50 | 50 | 62.5 |
121+
module.ts | 0 | 0 | 0 | 0 | 14-15,19
122+
types.ts | 0 | 0 | 0 | 0 |
123+
uncovered.ts | 0 | 0 | 0 | 0 |
124+
--------------|---------|----------|---------|---------|-------------------`);
125+
});
126+
127+
test('vm script coverage generator', () => {
128+
const dir = path.resolve(__dirname, '../vmscript-coverage');
129+
const {stdout, stderr, exitCode} = runJest(
130+
dir,
131+
['--coverage', '--coverage-provider', 'odz'],
132+
{stripAnsi: true},
133+
);
134+
135+
expect(stderr).toEqual(``);
136+
expect(exitCode).toBe(0);
137+
expect(stdout).toEqual(`-------------|---------|----------|---------|---------|-------------------
138+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s·
139+
-------------|---------|----------|---------|---------|-------------------
140+
All files | 80 | 75 | 66.66 | 80 |
141+
vmscript.js | 80 | 75 | 66.66 | 80 | 20-21
142+
-------------|---------|----------|---------|---------|-------------------`);
143+
});

e2e/coverage-provider-v8/esm-with-custom-transformer/.odz_output/.coverage/coverage-13419-1729711730560-0.json

+1
Large diffs are not rendered by default.

e2e/coverage-provider-v8/no-sourcemap/.odz_output/.coverage/coverage-12604-1729707983038-0.json

+1
Large diffs are not rendered by default.

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -204,5 +204,8 @@
204204
"psl": "patch:psl@npm:^1.9.0#./.yarn/patches/psl-npm-1.9.0-a546edad1a.patch",
205205
"ts-node@^10.5.0": "patch:ts-node@npm:^10.5.0#./.yarn/patches/ts-node-npm-10.9.1-6c268be7f4.patch"
206206
},
207-
"packageManager": "[email protected]"
207+
"packageManager": "[email protected]",
208+
"dependencies": {
209+
"one-double-zero": "^1.0.0-beta.11"
210+
}
208211
}

packages/jest-cli/src/args.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ export const options: {[key: string]: Options} = {
213213
type: 'array',
214214
},
215215
coverageProvider: {
216-
choices: ['babel', 'v8'],
217-
description: 'Select between Babel and V8 to collect coverage',
216+
choices: ['babel', 'v8', 'odz'],
217+
description: 'Select between Babel, V8 and One Double Zero to collect coverage',
218218
requiresArg: true,
219219
},
220220
coverageReporters: {

packages/jest-reporters/src/CoverageReporter.ts

+101-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import * as path from 'path';
9-
import {mergeProcessCovs} from '@bcoe/v8-coverage';
9+
import {mergeProcessCovs, ProcessCov} from '@bcoe/v8-coverage';
1010
import type {EncodedSourceMap} from '@jridgewell/trace-mapping';
1111
import chalk = require('chalk');
1212
import {glob} from 'glob';
@@ -15,6 +15,7 @@ import istanbulCoverage = require('istanbul-lib-coverage');
1515
import istanbulReport = require('istanbul-lib-report');
1616
import libSourceMaps = require('istanbul-lib-source-maps');
1717
import istanbulReports = require('istanbul-reports');
18+
import {createOneDoubleZero, type ProcessCoverage, type SourceMap} from 'one-double-zero';
1819
import v8toIstanbul = require('v8-to-istanbul');
1920
import type {
2021
AggregatedResult,
@@ -30,6 +31,7 @@ import {type JestWorkerFarm, Worker} from 'jest-worker';
3031
import BaseReporter from './BaseReporter';
3132
import getWatermarks from './getWatermarks';
3233
import type {ReporterContext} from './types';
34+
import {pathToFileURL} from 'url';
3335

3436
type CoverageWorker = typeof import('./CoverageWorker');
3537

@@ -70,7 +72,19 @@ export default class CoverageReporter extends BaseReporter {
7072
aggregatedResults: AggregatedResult,
7173
): Promise<void> {
7274
await this._addUntestedFiles(testContexts);
73-
const {map, reportContext} = await this._getCoverageResult();
75+
76+
const sourceFiles: Array<string> = [];
77+
78+
for (const testContext of testContexts) {
79+
for (const filePath of testContext.hasteFS.matchFilesWithGlob(
80+
this._globalConfig.collectCoverageFrom,
81+
testContext.config.rootDir,
82+
)) {
83+
sourceFiles.push(filePath);
84+
}
85+
}
86+
87+
const {map, reportContext} = await this._getCoverageResult(sourceFiles);
7488

7589
try {
7690
const coverageReporters = this._globalConfig.coverageReporters || [];
@@ -432,7 +446,7 @@ export default class CoverageReporter extends BaseReporter {
432446
}
433447
}
434448

435-
private async _getCoverageResult(): Promise<{
449+
private async _getCoverageResult(sourceFiles: Array<string>): Promise<{
436450
map: istanbulCoverage.CoverageMap;
437451
reportContext: istanbulReport.Context;
438452
}> {
@@ -502,6 +516,90 @@ export default class CoverageReporter extends BaseReporter {
502516
return {map, reportContext};
503517
}
504518

519+
if (this._globalConfig.coverageProvider === 'odz') {
520+
const mergedCoverages = mergeProcessCovs(
521+
this._v8CoverageResults.map(coverageResult => {
522+
return {
523+
result: coverageResult.map(result => {
524+
return result.result;
525+
})
526+
};
527+
})
528+
);
529+
530+
const fileTransforms = new Map<string, RuntimeTransformResult>();
531+
532+
for (const v8CoverageResult of this._v8CoverageResults) {
533+
for (const entry of v8CoverageResult) {
534+
if (entry.codeTransformResult && !fileTransforms.has(entry.result.url)) {
535+
fileTransforms.set(entry.result.url, entry.codeTransformResult);
536+
}
537+
}
538+
}
539+
540+
const loadProcessCoverage = (): ProcessCoverage => {
541+
const sourceMaps = new Map<string, SourceMap>();
542+
543+
for (const scriptCoverage of mergedCoverages.result) {
544+
const fileTransform = fileTransforms.get(scriptCoverage.url);
545+
546+
// the v8 coverage collector is transforming the URLS into paths; we need to restore the original URLs.
547+
console.error(scriptCoverage.url);
548+
scriptCoverage.url = pathToFileURL(scriptCoverage.url).href;
549+
console.error('>>>', scriptCoverage.url);
550+
551+
if (
552+
fileTransform?.sourceMapPath &&
553+
fs.existsSync(fileTransform.sourceMapPath)
554+
) {
555+
const sourceMapContent = JSON.parse(
556+
fs.readFileSync(fileTransform.sourceMapPath, 'utf8'),
557+
);
558+
559+
sourceMapContent.sources = sourceMapContent.sources.map((source) => {
560+
console.error('SOURCE', source);
561+
562+
return pathToFileURL(source).href;
563+
});
564+
565+
const lineLengths = fileTransform.code.split(/\r?\n/).map((line) => {
566+
return line.length;
567+
});
568+
569+
console.error(fileTransform.code, lineLengths);
570+
571+
sourceMaps.set(scriptCoverage.url, {
572+
...sourceMapContent,
573+
lineLengths
574+
});
575+
}
576+
}
577+
578+
return {
579+
scriptCoverages: mergedCoverages.result,
580+
sourceMaps,
581+
};
582+
};
583+
584+
const oneDoubleZero = createOneDoubleZero(console.info, fs.readFileSync);
585+
const processCoverage = loadProcessCoverage();
586+
587+
console.log('sourceFiles', sourceFiles);
588+
589+
590+
return oneDoubleZero.getCoverageMap(sourceFiles, processCoverage)
591+
.then(coverageMap => {
592+
return {
593+
map: coverageMap,
594+
reportContext: istanbulReport.createContext({
595+
coverageMap,
596+
dir: this._globalConfig.coverageDirectory,
597+
watermarks: getWatermarks(this._globalConfig),
598+
}),
599+
}
600+
});
601+
}
602+
505603
const map = await this._sourceMapStore.transformCoverage(this._coverageMap);
506604
const reportContext = istanbulReport.createContext({
507605
coverageMap: map,

packages/jest-reporters/src/generateEmptyCoverage.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export type CoverageWorkerResult =
2222
| {
2323
kind: 'V8Coverage';
2424
result: SingleV8Coverage;
25-
};
25+
}
26+
;
2627

2728
export default async function generateEmptyCoverage(
2829
source: string,
@@ -41,7 +42,10 @@ export default async function generateEmptyCoverage(
4142
};
4243
let coverageWorkerResult: CoverageWorkerResult | null = null;
4344
if (shouldInstrument(filename, coverageOptions, config)) {
44-
if (coverageOptions.coverageProvider === 'v8') {
45+
if (
46+
coverageOptions.coverageProvider === 'v8' ||
47+
coverageOptions.coverageProvider === 'odz'
48+
) {
4549
const stat = fs.statSync(filename);
4650
return {
4751
kind: 'V8Coverage',

packages/jest-runner/src/runTest.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ async function runTestInternal(
297297
// if we don't have `getVmContext` on the env skip coverage
298298
const collectV8Coverage =
299299
globalConfig.collectCoverage &&
300-
globalConfig.coverageProvider === 'v8' &&
300+
(globalConfig.coverageProvider === 'v8' || globalConfig.coverageProvider === 'odz') &&
301301
typeof environment.getVmContext === 'function';
302302

303303
// Node's error-message stack size is limited at 10, but it's pretty useful

packages/jest-types/src/Config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {InitialOptions, SnapshotFormat} from '@jest/schemas';
1313

1414
export type {InitialOptions} from '@jest/schemas';
1515

16-
type CoverageProvider = 'babel' | 'v8';
16+
type CoverageProvider = 'babel' | 'v8' | 'odz';
1717

1818
export type FakeableAPI =
1919
| 'Date'

0 commit comments

Comments
 (0)