Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add One Double Zero as coverage provider #15356

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ The directory where Jest should output its coverage files.

### `--coverageProvider=<provider>`

Indicates which provider should be used to instrument code for coverage. Allowed values are `babel` (default) or `v8`.
Indicates which provider should be used to instrument code for coverage. Allowed values are `babel` (default), `v8` or `odz`.

### `--debug`

Expand Down
30 changes: 27 additions & 3 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,35 @@ Default: `false`

Indicates whether the coverage information should be collected while executing the test. Because this retrofits all executed files with coverage collection statements, it may significantly slow down your tests.

Jest ships with two coverage providers: `babel` (default) and `v8`. See the [`coverageProvider`](#coverageprovider-string) option for more details.
Jest ships with three coverage providers: `babel` (default), `v8` and `odz`. See the [`coverageProvider`](#coverageprovider-string) option for more details.

:::info

The `babel` and `v8` coverage providers use `/* istanbul ignore next */` and `/* c8 ignore next */` comments to exclude lines from coverage reports, respectively. For more information, you can view the [`istanbuljs` documentation](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines) and the [`c8` documentation](https://github.com/bcoe/c8#ignoring-uncovered-lines-functions-and-blocks).
The `babel` and `v8` coverage providers use `/* istanbul ignore next */` and `/* c8 ignore next */` comments to exclude lines from coverage reports, respectively. For more information, you can view the [`istanbuljs` documentation](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines) and the [`c8` documentation](https://github.com/bcoe/c8#ignoring-uncovered-lines-functions-and-blocks). The `odz` coverage provider doesn't support exclusion comment.

The `v8` coverage provider comes with the following tradeoffs:

- It is not a 1:1 replacement for Babel/Istanbul coverage

- Switching between the two will usually change the reported coverage statistics, which can change whether coverage thresholds are reached or not
- Switching between the two can cause regions of uncovered code to be discovered or ignored (This is mostly just an overall summary of the other points)

- It works by taking a coverage report for output/transpiled code, and then using `v8-to-istanbul` and source maps to convert that report into an Istanbul-compatible format
- This is an inherently imprecise and heuristic-driven process, though in many cases it works well enough for practical purposes
- In some cases this can give confusing or misleading results, or fail to distinguish between user code and generated code (e.g. uncovered branches introduced by `__esModule` detection shims)
- Babel/Istanbul is able to be more precise because it usually operates directly on the user's original source code
- It tracks “blocks”, not individual statements
- In particular, if you have a sequence of statements, and the middle statements always throw an exception, V8 coverage will mark the later statements as “covered” even though they never ran
- This is a deliberate tradeoff made by the V8 developers who implemented coverage
- Babel/Istanbul is able to be more precise because it explicitly instruments every source statement
- It does not track the else branch of an if-statement without an explicit else

- So if a one-sided if-statement's condition is always true, V8 will not warn about an uncovered branch
- Babel/Istanbul is able to track these by artificially inserting an else with a branch counter

- It does not respect the `collectCoverageFrom` Jest configuration: regardless of the value of `collectCoverageFrom`, it emits coverage report for whatever file that was encountered during the execution of the tests.

The `odz` coverage provider also makes use of V8 coverage data, but doesn't come any of the `v8` provider tradeoffs because it operates at the AST level.

:::

Expand Down Expand Up @@ -314,7 +338,7 @@ These pattern strings match against the full path. Use the `<rootDir>` string to

### `coverageProvider` \[string]

Indicates which provider should be used to instrument code for coverage. Allowed values are `babel` (default) or `v8`.
Indicates which provider should be used to instrument code for coverage. Allowed values are `babel` (default), `v8` and `odz`.

### `coverageReporters` \[array&lt;string | \[string, options]&gt;]

Expand Down
116 changes: 116 additions & 0 deletions e2e/__tests__/__snapshots__/coverageProviderODZ.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`prints correct coverage report, if a CJS module is put under test without transformation 1`] = `
" console.log
this will print

at covered (module.js:11:11)

--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 60 | 50 | 50 | 60 |
module.js | 66.66 | 50 | 50 | 66.66 | 14-15,19
uncovered.js | 0 | 100 | 100 | 0 | 8
--------------|---------|----------|---------|---------|-------------------"
`;

exports[`prints correct coverage report, if a TS module is transpiled by Babel to CJS and put under test 1`] = `
" console.log
this will print

at log (module.ts:13:11)

--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 62.5 | 50 | 50 | 62.5 |
module.ts | 62.5 | 50 | 50 | 62.5 | 16-17,21
types.ts | 0 | 0 | 0 | 0 |
uncovered.ts | 0 | 0 | 0 | 0 |
--------------|---------|----------|---------|---------|-------------------"
`;

exports[`prints correct coverage report, if a TS module is transpiled by custom transformer to ESM put under test 1`] = `
" 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 | 62.5 | 50 | 50 | 62.5 | 16-17,21
types.ts | 0 | 0 | 0 | 0 |
uncovered.ts | 0 | 0 | 0 | 0 |
--------------|---------|----------|---------|---------|-------------------"
`;

exports[`prints correct coverage report, if an ESM module is put under test without transformation 1`] = `
" console.log
this will print

at covered (module.js:11:11)

--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 62.5 | 50 | 50 | 62.5 |
module.js | 62.5 | 50 | 50 | 62.5 | 14-15,19
uncovered.js | 0 | 0 | 0 | 0 |
--------------|---------|----------|---------|---------|-------------------"
`;

exports[`prints coverage with empty sourcemaps 1`] = `
"----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
types.ts | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------"
`;

exports[`prints coverage with missing sourcemaps 1`] = `
" console.log
42

at Object.log (__tests__/Thing.test.js:10:9)

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
Thing.js | 100 | 100 | 100 | 100 |
x.css | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------"
`;

exports[`reports coverage with \`resetModules\` 1`] = `
" console.log
this will print

at log (module.js:11:11)

console.log
this will print

at log (module.js:11:11)

--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 60 | 50 | 50 | 60 |
module.js | 66.66 | 50 | 50 | 66.66 | 14-15,19
uncovered.js | 0 | 100 | 100 | 0 | 8
--------------|---------|----------|---------|---------|-------------------"
`;

exports[`vm script coverage generator 1`] = `
"-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 80 | 75 | 66.66 | 80 |
vmscript.js | 80 | 75 | 66.66 | 80 | 20-21
-------------|---------|----------|---------|---------|-------------------"
`;
120 changes: 120 additions & 0 deletions e2e/__tests__/coverageProviderODZ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* 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, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz'],
{stripAnsi: true},
);

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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

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

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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

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

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{stripAnsi: true},
);

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{stripAnsi: true},
);

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{
nodeOptions: '--experimental-vm-modules --no-warnings',
stripAnsi: true,
},
);

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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, exitCode} = runJest(
sourcemapDir,
['--coverage', '--coverage-provider', 'odz', '--no-cache'],
{
nodeOptions: '--experimental-vm-modules --no-warnings',
stripAnsi: true,
},
);

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});

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

expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});
5 changes: 4 additions & 1 deletion e2e/coverage-provider-v8/empty-sourcemap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "empty-sourcemap",
"version": "1.0.0",
"jest": {
"testEnvironment": "node"
"testEnvironment": "node",
"collectCoverageFrom": [
"<rootDir>/types.ts"
]
}
}
10 changes: 9 additions & 1 deletion e2e/coverage-provider-v8/no-sourcemap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"transform": {
"\\.[jt]sx?$": "babel-jest",
"\\.css$": "<rootDir>/cssTransform.js"
}
},
"moduleFileExtensions": [
"js",
"css"
],
"collectCoverageFrom": [
"<rootDir>/Thing.js",
"<rootDir>/x.css"
]
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@
"jest": "workspace:*",
"jest-environment-node": "workspace:*",
"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"
"ts-node@^10.5.0": "patch:ts-node@npm:^10.5.0#./.yarn/patches/ts-node-npm-10.9.1-6c268be7f4.patch",
"typescript": "~5.5.4"
},
"packageManager": "[email protected]"
}
5 changes: 3 additions & 2 deletions packages/jest-cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,9 @@ 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
1 change: 1 addition & 0 deletions packages/jest-reporters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"jest-message-util": "workspace:*",
"jest-util": "workspace:*",
"jest-worker": "workspace:*",
"one-double-zero": "1.0.0-beta.14",
"slash": "^3.0.0",
"string-length": "^4.0.1",
"strip-ansi": "^6.0.0",
Expand Down
Loading
Loading