Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

monocart-coverage-reports

Package Overview
Dependencies
Maintainers
1
Versions
91
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

monocart-coverage-reports

Monocart coverage reports

  • 2.5.7
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
241K
increased by1.55%
Maintainers
1
Weekly downloads
 
Created
Source

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

V8 build-in reports (V8 data only):

  • v8-json
  • codecov
  • console-details Show file coverage and uncovered lines in the console. Like text, but for V8. For Github actions, we can enforce color with env: FORCE_COLOR: true.

Istanbul build-in reports (both V8 and istanbul data):

Other reports:

  • console-summary shows coverage summary in the console

Multiple Reports:

const MCR = require('monocart-coverage-reports');
const options = {
    outputDir: './coverage-reports',
    reports: [
        // build-in reports
        ['console-summary'],
        ['v8'],
        ['html', {
            subdir: 'istanbul'
        }],
        ['json', {
            file: 'my-json-file.json'
        }],
        'lcovonly',

        // custom reports
        // Specify reporter name with the NPM package
        ["custom-reporter-1"],
        ["custom-reporter-2", {
            type: "istanbul",
            option: "value"
        }],
        // Specify reporter name with local path
        ['/absolute/path/to/custom-reporter.js']

    ]
}
const coverageReport = MCR(options);

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

For example:

const coverageOptions = {
    entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
    sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
};

Or minimatch pattern:

const coverageOptions = {
    entryFilter: "**/main.js",
    sourceFilter: "**/src/**"
};

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));
            // throw new Error(errMsg);
            // process.exit(1);
        }
    }
}

mcr CLI

The CLI will run the program as a child process with NODE_V8_COVERAGE=dir until it exits gracefully, and generate the coverage report with the coverage data from the dir.

  • Global mode
npm i monocart-coverage-reports -g
mcr "node ./test/test-node-env.js" -r v8,console-summary --lcov
  • Current working directory mode
npm i monocart-coverage-reports
npx mcr "node ./test/test-node-env.js" -r v8,console-summary --lcov
  • CLI Options
Usage: mcr [options] <command>

CLI to generate coverage reports

Arguments:
  command                      command to execute

Options:
  -V, --version                output the version number
  -c, --config <path>          custom config path
  -o, --outputDir <dir>        output dir for reports
  -r, --reports <name[,name]>  coverage reports to use
  -n, --name <name>            report name for title
  -i, --inputDir <dir>         input dir for merging raw files
  --entryFilter <pattern>      entry url filter
  --sourceFilter <pattern>     source path filter
  --outputFile <path>          output file for v8 report
  --inline                     inline html for v8 report
  --assetsPath <path>          assets path if not inline
  --lcov                       generate lcov.info file
  --logging <logging>          off, error, info, debug
  -h, --help                   display help for command
  • Supports loading default configuration file
    • .mcrrc
    • mcr.config.json
    • mcr.config.mjs
    • mcr.config.cjs
    • mcr.config.js
  • Specify custom configuration file
mcr "node ./test.js" -c path-to/my-custom-config.js

Compare Reports

IstanbulV8V8 to Istanbul
Coverage dataIstanbul (Object)V8 (Array)V8 (Array)
OutputIstanbul reportsV8 reportsIstanbul reports
- Bytes
- Statements
- Branches
- Functions
- Lines
- Execution counts
CSS coverage
Minified code

Compare Workflows

Collecting Istanbul Coverage Data

Collecting V8 Coverage Data

Manually Resolve the Sourcemap

Sometimes, the sourcemap file cannot be successfully loaded with the sourceMappingURL, you can try to manually read the sourcemap file before the coverage data is added to the report.

const jsCoverage = await page.coverage.stopJSCoverage();
jsCoverage.forEach((entry) => {
    // read sourcemap for the my-dist.js manually
    if (entry.url.endsWith('my-dist.js')) {
        entry.sourceMap = JSON.parse(fs.readFileSync('dist/my-dist.js.map').toString('utf-8'));
    }
});

await MCR(coverageOptions).add(jsCoverage);

Collecting Raw V8 Coverage Data with Puppeteer

Puppeteer does not provide raw v8 coverage data by default. A simple conversion is required, see example: ./test/test-puppeteer.js

await page.coverage.startJSCoverage({
    // provide raw v8 coverage data
    includeRawScriptCoverage: true
});

await page.goto(url);

const jsCoverage = await page.coverage.stopJSCoverage();
const rawV8CoverageData = jsCoverage.map((it) => {
    // Convert to raw v8 coverage format
    return {
        source: it.text,
        ... it.rawScriptCoverage
    };
}

Node.js V8 Coverage Report for Server Side

Possible solutions:

  • NODE_V8_COVERAGE=dir

    • Sets Node.js env NODE_V8_COVERAGE=dir before the program running, the coverage data will be saved to the dir after the program exits gracefully.
    • 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-report.js

  • V8 API + NODE_V8_COVERAGE

    • Writing the coverage started by NODE_V8_COVERAGE to disk on demand with v8.takeCoverage(), it does not require waiting until the program exits gracefully.
    • Example:

    cross-env NODE_V8_COVERAGE=.temp/v8-coverage-api node ./test/test-node-api.js

  • Inspector API

    • Connecting to the V8 inspector and enable V8 coverage.
    • Taking coverage data and adding it to the report.
    • Example:

    node ./test/test-node-ins.js

  • CDP API

    • Enabling Node Debugging.
    • Collecting coverage data with CDP API.
    • Example:

    node --inspect=9229 ./test/test-node-cdp.js

  • Node Debugging + CDP + NODE_V8_COVERAGE + V8 API

    • When the program starts a server, it will not exit on its own, thus requiring a manual invocation of the v8.takeCoverage() interface to manually collect coverage data. Remote invocation of the v8.takeCoverage() interface can be accomplished through the Runtime.evaluate of the CDP.
    • Example for koa web server:

    node ./test/test-node-koa.js

  • Child Process + NODE_V8_COVERAGE

Multiprocessing Support

The data will be added to [outputDir]/.cache, After the generation of the report, this data will be removed unless debugging has been enabled or a raw report has been used, see Debug for Coverage and Sourcemap

  • Main process, before the start of testing
// clean previous cache before the start of testing (option)
const MCR = require('monocart-coverage-reports');
const options = require('path-to/same-options.js');
MCR(options).cleanCache();
  • Sub process 1, testing stage 1
const MCR = require('monocart-coverage-reports');
const options = require('path-to/same-options.js');
await MCR(options).add(coverageData1);
  • Sub process 2, testing stage 2
const MCR = require('monocart-coverage-reports');
const options = require('path-to/same-options.js');
await MCR(options).add(coverageData2);
  • Main process, after the completion of testing
// generate coverage reports after the completion of testing
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);

Merge Coverage Reports

The following usage scenarios may require merging coverage reports:

  • When the code is executed in different environments, like Node.js Server Side and browser Client Side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage. see example nextjs-with-playwright for automatic report merging.
  • When the code is subjected to different kinds of testing. For example, unit tests with Jest might cover certain parts of the code, while end-to-end tests with Playwright might cover other parts. Merging these different coverage reports can provide a holistic view of what code has been tested.
  • When tests are run on different machines or different shards, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards.

If the reports cannot be merged automatically, then here is how to manually merge the reports. First, using the raw report to export the original coverage data to the specified directory.

const coverageOptions = {
    name: 'My Unit Test Coverage Report',
    outputDir: "./coverage-reports/unit",
    reports: [
        ['raw', {
            // relative path will be "./coverage-reports/unit/raw"
            outputDir: "raw"
        }],
        ['v8'],
        ['console-summary']
    ]
};

Then, after all the tests are completed, generate a merged report with option inputDir:

import fs from "fs";
import { CoverageReport } from 'monocart-coverage-reports';
const coverageOptions = {
    name: 'My Merged Coverage Report',
    inputDir: [
        './coverage-reports/unit/raw',
        './coverage-reports/e2e/raw'
    ],
    outputDir: './coverage-reports/merged',
    reports: [
        ['v8'],
        ['console-summary']
    ],
    onEnd: () => {
        // remove the raw files if it useless
        fs.rmSync('./coverage-reports/unit/raw', {
            recursive: true,
            force: true
        })
    }
};
await new CoverageReport(coverageOptions).generate();

Resolve sourcePath for the Source Files

If the source file comes from the sourcemap, then its path is a virtual path. Using the sourcePath option to resolve a custom path. For example, we have tested multiple dist files, which contain some common files. We hope to merge the coverage of the same files, so we need to unify the sourcePath in order to be able to merge the coverage data.

const coverageOptions = {
    sourcePath: (filePath) => {
        // Remove the virtual prefix
        const list = ['my-dist-file1/', 'my-dist-file2/'];
        for (const str of list) {
            if (filePath.startsWith(str)) {
                return filePath.slice(str.length);
            }
        }
        return filePath;
    }
};

It also supports simple key/value replacement:

const coverageOptions = {
    sourcePath: {
        'my-dist-file1/': '', 
        'my-dist-file2/': ''
    }
};

Adding Empty Coverage for Untested Files

By default the untested files will not be included in the coverage report, we can first add empty coverage data for all files, so the files with coverage data will be merged, and untested files will retain 0% coverage.

const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const MCR = require('monocart-coverage-reports');

const dir = "./src";
const coverageData = [];
const fileList = fs.readdirSync(dir);
for (const filename of fileList) {
    const filePath = path.resolve(dir, filename);
    const source = fs.readFileSync(filePath).toString('utf-8');
    const sourcePath = path.relative(process.cwd(), filePath);
    const url = pathToFileURL(sourcePath).toString();
    const extname = path.extname(filename);
    if (['.css'].includes(extname)) {
        coverageData.push({
            empty: true,
            type: 'css',
            url,
            text: source
        });
    } else {
        coverageData.push({
            empty: true,
            type: 'js',
            url,
            source
        });
    }
}
const options = require('path-to/same-options.js');
await MCR(options).add(coverageData);

see example: ./test/cli-options.js

Ignoring Uncovered Codes

To ignore codes, use the special comment which starts with v8 ignore :

  • Ignoring all until stop
/* v8 ignore start */
function uncovered() {
}
/* v8 ignore stop */
  • Ignoring the next line or next N lines
/* v8 ignore next */
const os = platform === 'wind32' ? 'Windows' : 'Other';

const os = platform === 'wind32' ? 'Windows' /* v8 ignore next */ : 'Other';

// v8 ignore next 3
if (platform === 'linux') {
    console.log('hello linux');
}

Chromium Coverage API

V8 Coverage Data Format

// Coverage data for a source range.
export interface CoverageRange {
    // JavaScript script source offset for the range start.
    startOffset: integer;
    // JavaScript script source offset for the range end.
    endOffset: integer;
    // Collected execution count of the source range.
    count: integer;
}

// Coverage data for a JavaScript function.
/**
 * @functionName can be an empty string.
 * @ranges is always non-empty. The first range is called the "root range".
 * @isBlockCoverage indicates if the function has block coverage information.
    If this is false, it usually means that the functions was never called.
    It seems to be equivalent to ranges.length === 1 && ranges[0].count === 0.
*/
export interface FunctionCoverage {
    // JavaScript function name.
    functionName: string;
    // Source ranges inside the function with coverage data.
    ranges: CoverageRange[];
    // Whether coverage data for this function has block granularity.
    isBlockCoverage: boolean;
}

// Coverage data for a JavaScript script.
export interface ScriptCoverage {
    // JavaScript script id.
    scriptId: Runtime.ScriptId;
    // JavaScript script name or url.
    url: string;
    // Functions contained in the script that has coverage data.
    functions: FunctionCoverage[];
}

export type V8CoverageData = ScriptCoverage[];

see devtools-protocol ScriptCoverage and v8-coverage

How to convert V8 to Istanbul

Using 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 or m2 if we don't fix it to p. Especially the generated code is different from the original code, such as the code was minified, compressed or converted, it is difficult to find the exact position.

  • 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.

How Monocart Works

We implemented new converter:

  • 1, Trying to fix the middle position if not found the exact mapping for the position.
  • 2, Finding all functions, statements and branches by parsing the source code AST. However, there's a small issue, which is the V8 cannot provide effective branch coverage information for AssignmentPattern.
ASTV8
AssignmentPattern🛇 Not Support
ConditionalExpression
IfStatement
LogicalExpression
SwitchStatement

Debug for Coverage and Sourcemap

Sometimes, the coverage is not what we expect. The next step is to figure out why, and we can easily find out the answer step by step through debugging.

  • Start debugging for v8 report with option logging: 'debug'
const coverageOptions = {
    logging: 'debug',
    reports: [
        ['v8'],
        ['console-summary']
    ]
};

When logging is debug, the raw report data will be preserved in [outputDir]/.cache or [outputDir]/raw if raw report is used. And the dist file will be preserved in the V8 list, and by opening the browser's devtool, it makes data verification visualization effortless.

Integration

Playwright

Jest

Vitest

Codecov

codecov

const coverageOptions = {
    outputDir: "./coverage-reports",
    reports: [
        ['codecov']
    ]
};
  • Github actions example:
- name: Codecov
    uses: codecov/codecov-action@v3
    with:
        files: ./coverage-reports/codecov.json

Coveralls

Coverage Status

  • Using lcov report:
const coverageOptions = {
    outputDir: "./coverage-reports",
    lcov: true
};
  • Github actions example:
- name: Coveralls
    uses: coverallsapp/github-action@v2
    with:
        files: ./coverage-reports/lcov.info

Sonar Cloud

Coverage

  • Using lcov report. Github actions example:
- name: Analyze with SonarCloud
    uses: sonarsource/sonarcloud-github-action@master
    env: 
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
    with:
        projectBaseDir: ./
        args: >
        -Dsonar.organization=cenfun
        -Dsonar.projectKey=monocart-coverage-reports
        -Dsonar.projectName=monocart-coverage-reports
        -Dsonar.javascript.lcov.reportPaths=docs/mcr/lcov.info
        -Dsonar.sources=lib
        -Dsonar.tests=test
        -Dsonar.exclusions=dist/*,packages/*

Integration with Any Testing Framework

  • Collecting coverage data when any stage of the test is completed, and adding the coverage data to the coverage reporter.
  • Generating the coverage reports after the completion of all tests.
  • see Multiprocessing Support

Thanks

FAQs

Package last updated on 25 Feb 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc