Skip to content

Commit

Permalink
Add One Double Zero as coverage provider
Browse files Browse the repository at this point in the history
  • Loading branch information
ericmorand committed Oct 24, 2024
1 parent 0a0a9f7 commit 6f9e982
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 14 deletions.
143 changes: 143 additions & 0 deletions e2e/__tests__/coverageProviderODZ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import * as path from 'path';
import runJest from '../runJest';

const DIR = path.resolve(__dirname, '../coverage-provider-v8');

test('prints coverage with missing sourcemaps', () => {
const sourcemapDir = path.join(DIR, 'no-sourcemap');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz'],
{stripAnsi: true},
);

expect(exitCode).toBe(0);
expect(stdout).toEqual(``);
});

test('prints coverage with empty sourcemaps', () => {
const sourcemapDir = path.join(DIR, 'empty-sourcemap');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz'],
{stripAnsi: true},
);

expect(exitCode).toBe(0);
expect(stdout).toEqual(``);
});

test('reports coverage with `resetModules`', () => {
const sourcemapDir = path.join(DIR, 'with-resetModules');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz'],
{stripAnsi: true},
);

expect(stderr).toEqual(``);
expect(exitCode).toBe(0);
expect(stdout).toEqual(``);
});

test('prints correct coverage report, if a CJS module is put under test without transformation', () => {
const sourcemapDir = path.join(DIR, 'cjs-native-without-sourcemap');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{stripAnsi: true},
);

expect(stderr).toEqual(``);
expect(exitCode).toBe(0);
expect(stdout).toEqual(``);
});

test('prints correct coverage report, if a TS module is transpiled by Babel to CJS and put under test', () => {
const sourcemapDir = path.join(DIR, 'cjs-with-babel-transformer');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{stripAnsi: true},
);

expect(stderr).toEqual(``);
expect(exitCode).toBe(0);
expect(stdout).toEqual(``);
});

test('prints correct coverage report, if an ESM module is put under test without transformation', () => {
const sourcemapDir = path.join(DIR, 'esm-native-without-sourcemap');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{
nodeOptions: '--experimental-vm-modules --no-warnings',
stripAnsi: true,
},
);

expect(stderr).toEqual(``);
expect(exitCode).toBe(0);
expect(stdout).toEqual(``);
});

test('prints correct coverage report, if a TS module is transpiled by custom transformer to ESM put under test', () => {
const sourcemapDir = path.join(DIR, 'esm-with-custom-transformer');

const {stdout, stderr, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{
nodeOptions: '--experimental-vm-modules --no-warnings',
stripAnsi: true,
},
);

expect(stderr).toEqual(``);
expect(exitCode).toBe(0);
expect(stdout).toEqual(` console.log
this will print
at covered (module.ts:13:11)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 62.5 | 50 | 50 | 62.5 |
module.ts | 0 | 0 | 0 | 0 | 14-15,19
types.ts | 0 | 0 | 0 | 0 |
uncovered.ts | 0 | 0 | 0 | 0 |
--------------|---------|----------|---------|---------|-------------------`);
});

test('vm script coverage generator', () => {
const dir = path.resolve(__dirname, '../vmscript-coverage');
const {stdout, stderr, exitCode} = runJest(
dir,
['--coverage', '--coverage-provider', 'odz'],
{stripAnsi: true},
);

expect(stderr).toEqual(``);
expect(exitCode).toBe(0);
expect(stdout).toEqual(`-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s·
-------------|---------|----------|---------|---------|-------------------
All files | 80 | 75 | 66.66 | 80 |
vmscript.js | 80 | 75 | 66.66 | 80 | 20-21
-------------|---------|----------|---------|---------|-------------------`);
});

Large diffs are not rendered by default.

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,8 @@
"psl": "patch:psl@npm:^1.9.0#./.yarn/patches/psl-npm-1.9.0-a546edad1a.patch",
"ts-node@^10.5.0": "patch:ts-node@npm:^10.5.0#./.yarn/patches/ts-node-npm-10.9.1-6c268be7f4.patch"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"one-double-zero": "^1.0.0-beta.11"
}
}
4 changes: 2 additions & 2 deletions packages/jest-cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ export const options: {[key: string]: Options} = {
type: 'array',
},
coverageProvider: {
choices: ['babel', 'v8'],
description: 'Select between Babel and V8 to collect coverage',
choices: ['babel', 'v8', 'odz'],
description: 'Select between Babel, V8 and One Double Zero to collect coverage',
requiresArg: true,
},
coverageReporters: {
Expand Down
104 changes: 101 additions & 3 deletions packages/jest-reporters/src/CoverageReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import * as path from 'path';
import {mergeProcessCovs} from '@bcoe/v8-coverage';
import {mergeProcessCovs, ProcessCov} from '@bcoe/v8-coverage';
import type {EncodedSourceMap} from '@jridgewell/trace-mapping';
import chalk = require('chalk');
import {glob} from 'glob';
Expand All @@ -15,6 +15,7 @@ import istanbulCoverage = require('istanbul-lib-coverage');
import istanbulReport = require('istanbul-lib-report');
import libSourceMaps = require('istanbul-lib-source-maps');
import istanbulReports = require('istanbul-reports');
import {createOneDoubleZero, type ProcessCoverage, type SourceMap} from 'one-double-zero';
import v8toIstanbul = require('v8-to-istanbul');
import type {
AggregatedResult,
Expand All @@ -30,6 +31,7 @@ import {type JestWorkerFarm, Worker} from 'jest-worker';
import BaseReporter from './BaseReporter';
import getWatermarks from './getWatermarks';
import type {ReporterContext} from './types';
import {pathToFileURL} from 'url';

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

Expand Down Expand Up @@ -70,7 +72,19 @@ export default class CoverageReporter extends BaseReporter {
aggregatedResults: AggregatedResult,
): Promise<void> {
await this._addUntestedFiles(testContexts);
const {map, reportContext} = await this._getCoverageResult();

const sourceFiles: Array<string> = [];

for (const testContext of testContexts) {
for (const filePath of testContext.hasteFS.matchFilesWithGlob(
this._globalConfig.collectCoverageFrom,
testContext.config.rootDir,
)) {
sourceFiles.push(filePath);
}
}

const {map, reportContext} = await this._getCoverageResult(sourceFiles);

try {
const coverageReporters = this._globalConfig.coverageReporters || [];
Expand Down Expand Up @@ -432,7 +446,7 @@ export default class CoverageReporter extends BaseReporter {
}
}

private async _getCoverageResult(): Promise<{
private async _getCoverageResult(sourceFiles: Array<string>): Promise<{
map: istanbulCoverage.CoverageMap;
reportContext: istanbulReport.Context;
}> {
Expand Down Expand Up @@ -502,6 +516,90 @@ export default class CoverageReporter extends BaseReporter {
return {map, reportContext};
}

if (this._globalConfig.coverageProvider === 'odz') {
const mergedCoverages = mergeProcessCovs(
this._v8CoverageResults.map(coverageResult => {
return {
result: coverageResult.map(result => {
return result.result;
})
};
})
);

const fileTransforms = new Map<string, RuntimeTransformResult>();

for (const v8CoverageResult of this._v8CoverageResults) {
for (const entry of v8CoverageResult) {
if (entry.codeTransformResult && !fileTransforms.has(entry.result.url)) {
fileTransforms.set(entry.result.url, entry.codeTransformResult);
}
}
}

const loadProcessCoverage = (): ProcessCoverage => {
const sourceMaps = new Map<string, SourceMap>();

for (const scriptCoverage of mergedCoverages.result) {
const fileTransform = fileTransforms.get(scriptCoverage.url);

// the v8 coverage collector is transforming the URLS into paths; we need to restore the original URLs.
console.error(scriptCoverage.url);
scriptCoverage.url = pathToFileURL(scriptCoverage.url).href;
console.error('>>>', scriptCoverage.url);

if (
fileTransform?.sourceMapPath &&
fs.existsSync(fileTransform.sourceMapPath)
) {
const sourceMapContent = JSON.parse(
fs.readFileSync(fileTransform.sourceMapPath, 'utf8'),
);

sourceMapContent.sources = sourceMapContent.sources.map((source) => {
console.error('SOURCE', source);

return pathToFileURL(source).href;
});

const lineLengths = fileTransform.code.split(/\r?\n/).map((line) => {
return line.length;
});

console.error(fileTransform.code, lineLengths);

sourceMaps.set(scriptCoverage.url, {
...sourceMapContent,
lineLengths
});
}
}

return {
scriptCoverages: mergedCoverages.result,
sourceMaps,
};
};

const oneDoubleZero = createOneDoubleZero(console.info, fs.readFileSync);
const processCoverage = loadProcessCoverage();

console.log('sourceFiles', sourceFiles);


return oneDoubleZero.getCoverageMap(sourceFiles, processCoverage)
.then(coverageMap => {
return {
map: coverageMap,
reportContext: istanbulReport.createContext({
coverageMap,
dir: this._globalConfig.coverageDirectory,
watermarks: getWatermarks(this._globalConfig),
}),
}
});
}

const map = await this._sourceMapStore.transformCoverage(this._coverageMap);
const reportContext = istanbulReport.createContext({
coverageMap: map,
Expand Down
8 changes: 6 additions & 2 deletions packages/jest-reporters/src/generateEmptyCoverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export type CoverageWorkerResult =
| {
kind: 'V8Coverage';
result: SingleV8Coverage;
};
}
;

export default async function generateEmptyCoverage(
source: string,
Expand All @@ -41,7 +42,10 @@ export default async function generateEmptyCoverage(
};
let coverageWorkerResult: CoverageWorkerResult | null = null;
if (shouldInstrument(filename, coverageOptions, config)) {
if (coverageOptions.coverageProvider === 'v8') {
if (
coverageOptions.coverageProvider === 'v8' ||
coverageOptions.coverageProvider === 'odz'
) {
const stat = fs.statSync(filename);
return {
kind: 'V8Coverage',
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-runner/src/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ async function runTestInternal(
// if we don't have `getVmContext` on the env skip coverage
const collectV8Coverage =
globalConfig.collectCoverage &&
globalConfig.coverageProvider === 'v8' &&
(globalConfig.coverageProvider === 'v8' || globalConfig.coverageProvider === 'odz') &&
typeof environment.getVmContext === 'function';

// Node's error-message stack size is limited at 10, but it's pretty useful
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-types/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {InitialOptions, SnapshotFormat} from '@jest/schemas';

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

type CoverageProvider = 'babel' | 'v8';
type CoverageProvider = 'babel' | 'v8' | 'odz';

export type FakeableAPI =
| 'Date'
Expand Down
Loading

0 comments on commit 6f9e982

Please sign in to comment.