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 25, 2024
1 parent 22029ba commit c44c4cf
Show file tree
Hide file tree
Showing 15 changed files with 459 additions and 40 deletions.
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 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);

Check failure on line 35 in e2e/__tests__/coverageProviderODZ.test.ts

View workflow job for this annotation

GitHub Actions / Windows with shard 1/4 / Node v22.x

prints coverage with empty sourcemaps

expect(received).toBe(expected) // Object.is equality Expected: 0 Received: 1 at Object.toBe (e2e/__tests__/coverageProviderODZ.test.ts:35:20)
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.13",
"slash": "^3.0.0",
"string-length": "^4.0.1",
"strip-ansi": "^6.0.0",
Expand Down
Loading

0 comments on commit c44c4cf

Please sign in to comment.