Monocart Coverage Reports
Code coverage tool to generate native V8 reports or Istanbul reports.
Usage
const MCR = require('monocart-coverage-reports');
const options = {
outputDir: './coverage-reports',
reports: "v8"
}
const coverageReport = MCR(options);
await coverageReport.add(coverageData1);
await coverageReport.add(coverageData2);
const coverageResults = await coverageReport.generate();
console.log(coverageResults.summary);
Default Options
Available Reports
Following are istanbul reports
clover
cobertura
html
html-spa
json
json-summary
lcov
lcovonly
none
teamcity
text
text-lcov
text-summary
const MCR = require('monocart-coverage-reports');
const options = {
outputDir: './coverage-reports',
reports: [
['console-summary'],
['v8'],
['html', {
subdir: 'istanbul'
}],
['json', {
file: 'my-json-file.json'
}],
'lcovonly'
]
}
const coverageReport = MCR(options);
Integration
Multiprocessing Support
The data will be added to [outputDir]/.cache
, and the cache will be removed after reports generated.
const MCR = require('monocart-coverage-reports');
const options = require('path-to/same-options.js');
const coverageReport = MCR(options);
await coverageReport.add(coverageData1);
const MCR = require('monocart-coverage-reports');
const options = require('path-to/same-options.js');
const coverageReport = MCR(options);
await coverageReport.add(coverageData2);
const MCR = require('monocart-coverage-reports');
const options = require('path-to/same-options.js');
const coverageReport = MCR(options);
const coverageResults = await coverageReport.generate();
console.log(coverageResults.summary);
onEnd Hook
For example, checking thresholds:
const EC = require('eight-colors');
const coverageOptions = {
name: 'My Coverage Report',
outputDir: './coverage-reports',
onEnd: (coverageResults) => {
const thresholds = {
bytes: 80,
lines: 60
};
console.log('check thresholds ...', thresholds);
const errors = [];
const { summary } = coverageResults;
Object.keys(thresholds).forEach((k) => {
const pct = summary[k].pct;
if (pct < thresholds[k]) {
errors.push(`Coverage threshold for ${k} (${pct} %) not met: ${thresholds[k]} %`);
}
});
if (errors.length) {
const errMsg = errors.join('\n');
console.log(EC.red(errMsg));
}
}
}
Compare Reports
❔ - Partial or conditional support
Compare Workflows
-
Istanbul Workflows
-
V8 Workflows
Collecting Istanbul Coverage Data
- Instrumenting source code
Before collecting Istanbul coverage data, It requires your source code is instrumented with Istanbul
- Browser
Collecting coverage data from window.__coverage__
, example: test-istanbul.js
- Node.js
Collecting coverage data from global.__coverage__
Collecting V8 Coverage Data
Node.js V8 Coverage Report for Server Side
-
Using Node.js env NODE_V8_COVERAGE=dir
- Before running your Node.js application, set env
NODE_V8_COVERAGE
=dir
. After the application runs and exits, the coverage data will be saved to the dir
directory in JSON file format - Read the json file(s) from the
dir
and generate coverage report - example:
cross-env NODE_V8_COVERAGE=.temp/v8-coverage-env
node ./test/test-node-env.js && node ./test/generate-node-report.js
-
Using V8 API + NODE_V8_COVERAGE
- Writing the coverage started by NODE_V8_COVERAGE to disk on demand with
v8.takeCoverage()
and stopping with v8.stopCoverage()
. - example:
cross-env NODE_V8_COVERAGE=.temp/v8-coverage-api
node ./test/test-node-api.js
-
Using Inspector API (or module collect-v8-coverage)
- Connecting to the V8 inspector and enable V8 coverage.
- Taking coverage data and adding to the report after your application runs.
- example: ./test/test-node-ins.js
-
Using CDP API
Using entryFilter
and sourceFilter
to filter the results for V8 report
When V8 coverage data collected, it actually contains the data of all entry files, for example:
dist/main.js
dist/vendor.js
dist/something-else.js
We can use entryFilter
to filter the entry files. For example, we should remove vendor.js
and something-else.js
if they are not in our coverage scope.
dist/main.js
When inline or linked sourcemap exists to the entry file, the source files will be extracted from the sourcemap for the entry file, and the entry file will be removed if logging
is not debug
.
> src/index.js
> src/components/app.js
> node_modules/dependency/dist/dependency.js
We can use sourceFilter
to filter the source files. For example, we should remove dependency.js
if it is not in our coverage scope.
> src/index.js
> src/components/app.js
Example:
const coverageOptions = {
entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
};
How to convert V8 to Istanbul
It is a popular library which is used to convert V8 coverage format to istanbul's coverage format. Most test frameworks are using it, such as Jest, Vitest, but it has two major problems:
- 1, The source mapping does not work well if the position is between the two consecutive mappings. for example:
const a = tf ? 'true' : 'false';
^ ^ ^
m1 p m2
m1
and m2
are two consecutive mappings, p
is the position we looking for. However, we can only get the position of the m1
if we don't fix it to p
. Especially the generated code is different from the original code, such as minified, compressed or converted, then it becomes very difficult to find the middle position between two mappings.
- 2, The coverage of functions and branches is incorrect. V8 only provided coverage at functions and it's blocks. But if a function is uncovered (count = 0), there is no information for it's blocks and sub-level functions.
And also there are some problems about counting the functions and branches:
functions.forEach(block => {
block.ranges.forEach((range, i) => {
if (block.isBlockCoverage) {
if (block.functionName && i === 0) {
}
} else if (block.functionName) {
}
}
});
see source code v8-to-istanbul.js
How Monocart Works
We have removed v8-to-istanbul
because of the two major problems and implemented new converter:
- 1, Trying to fix the middle position if not found the exact mapping.
- 2, Finding all functions and branches by parsing the source code AST, however the V8 cannot provide effective branch coverage information, so the branches is still not perfect.
AST | V8 |
---|
AssignmentPattern | 🛇 Not Support |
ConditionalExpression | ✔ Not Perfect |
IfStatement | ✔ Not Perfect |
LogicalExpression | ✔ Not Perfect |
SwitchStatement | ✔ Not Perfect |
Ignoring Uncovered Codes
To ignore codes, use the special comment which starts with v8 ignore
:
function uncovered() {
}
- ignoring the next line or next N lines
const os = platform === 'wind32' ? 'Windows' : 'Other';
const os = platform === 'wind32' ? 'Windows' : 'Other';
if (platform === 'linux') {
console.info('hello linux');
}
Chromium Coverage API
V8 Coverage Data Format
export interface CoverageRange {
startOffset: integer;
endOffset: integer;
count: integer;
}
export interface FunctionCoverage {
functionName: string;
ranges: CoverageRange[];
isBlockCoverage: boolean;
}
export interface ScriptCoverage {
scriptId: Runtime.ScriptId;
url: string;
functions: FunctionCoverage[];
}
export type V8CoverageData = ScriptCoverage[];
see devtools-protocol ScriptCoverage and v8-coverage
Istanbul Introduction
Thanks