@hyperfrontend/testing/coverage

coverage

The coverage side of a run: lcov reports merged per file, the threshold gate, the per-file table, and the completeness check for source files no test loaded.

Overview

Node's test runner can write an lcov report, and this entry is what turns one or more of those reports into a verdict.

mergeLcov reads the reports and returns one FileCoverage per path, holding execution counts per line, per function and per branch. fileTotals and overallTotals reduce those counters to found-and-hit pairs, percentOf turns a pair into a percentage, and uncoveredRanges lists the lines a file never ran as ranges. renderLcov writes the merged files back out as a single report for a coverage service to read.

Why reports are merged

Node measures each evaluation of a module as a script of its own, so after jest.resetModules a later import re-runs the module body and the report carries one record per evaluation. Treating those as separate files counts every line of the module once per evaluation while crediting only the evaluation that ran it, which drags the percentages down in proportion to how often a suite reset. Combining the records per file is what Jest's provider did implicitly. Reports from different environments merge the same way, so a file exercised by one environment and merely imported by another is judged on both.

The merge keys are chosen to hold still between runs. Node numbers anonymous functions by discovery order and numbers branch blocks in a way that shifts between runs and environments, so functions are keyed by declaration line and name with the anonymous numbering folded, and branches by line and branch index rather than block. Anything a node:coverage ignore comment covers is dropped rather than measured: Node omits an ignored line from the report but still writes the branches and functions sitting on it, marked untaken, and dropping them is what istanbul ignore used to mean.

What the gate judges

findThresholdShortfalls compares the merged totals of the whole project against the CoverageThresholds a project declares for lines, branches and functions, which is what a Jest global threshold meant, and returns one message per threshold missed. A metric with nothing to measure counts as complete, which is what Jest reported for a file holding no branches at all. There is no statements metric: Node does not measure one.

findUnmeasuredFiles restores the guarantee a full-coverage threshold is supposed to give. Node's report only names files the run loaded, so a source file no test imports vanishes instead of landing at zero percent, and a new, wholly untested file would not fail the build. Comparing the report against the include globs, less the exclude globs, names every file that should have been measured and was not.

renderCoverageTable is the human-readable form: one row per file with line, branch and function percentages and the uncovered line ranges, closed by the project's totals.

Usage

The runner calls all of this for you. Reach for the entry directly to judge reports another tool wrote, or to read a merged report off disk:

import { mergeLcov, overallTotals, percentOf, renderCoverageTable } from '@hyperfrontend/testing/coverage'

const reports = ['coverage/lcov.node.info', 'coverage/lcov.browser.info']
const files = [...mergeLcov(reports).values()]

for (const row of renderCoverageTable(files)) console.log(row)
console.log(`${percentOf(overallTotals(files).lines).toFixed(2)}% of lines ran`)

API Reference§

ƒ Functions

§function

fileTotals(file: FileCoverage): MetricTotals

Reduces one file's counters to a found and hit total per metric.
Parameters
NameTypeDescription
§file
FileCoverageThe file's merged counters.
Returns
MetricTotals
How many lines, branches, and functions it has and how many ran.
Example

Reducing a file's counters

const file = {
  path: 'src/double.ts',
  lines: new Map([[1, 5], [2, 4], [3, 0]]),
  branches: new Map([['2,0', 4], ['2,1', 0]]),
  functions: new Map([['1,double', 5]]),
}
fileTotals(file) // => { lines: { found: 3, hit: 2 }, branches: { found: 2, hit: 1 }, functions: { found: 1, hit: 1 } }
§function

findThresholdShortfalls(files: unknown, thresholds: CoverageThresholds): string[]

Reports which declared thresholds a report falls short of.
The comparison is against the project as a whole, which is what a Jest global threshold meant.
Parameters
NameTypeDescription
§files
unknownThe merged files.
§thresholds
CoverageThresholdsThe percentages the project requires.
Returns
string[]
One message per threshold missed, empty when all are met.
Example

Judging the merged report against a project's thresholds

const measured = [...mergeLcov(reports).values()]
findThresholdShortfalls(measured, { lines: 99, branches: 96, functions: 97 })
// => ['80.00% line coverage does not meet threshold of 99%', '50.00% branch coverage does not meet threshold of 96%']
findThresholdShortfalls(measured, undefined) // => [], nothing declared means nothing to miss
§function

findUnmeasuredFiles(measured: unknown, projectRoot: string, include: string[], exclude: string[]): CompletenessReport

Finds source files that should have been measured but are absent from the report.
Node's coverage only reports files the run actually loaded, so a source file no test imports simply vanishes instead of landing at zero percent. That silently removes the guarantee a full-coverage threshold is supposed to give: a new, wholly untested file would not fail the build. Comparing the report against the include globs restores it.
Parameters
NameTypeDescription
§measured
unknownThe merged report, already unioned across the project's environments.
§projectRoot
stringDirectory the include globs and report paths are anchored to.
§include
string[]Globs naming every file that must be measured.
§exclude
string[]Globs naming files that are exempt.
Returns
CompletenessReport
The files missing from the report, and how many were accounted for.
Example

Catching a source file no spec imported

const measured = [...mergeLcov(['/repo/coverage/libs/example/lcov.node.info']).values()]
findUnmeasuredFiles(measured, '/repo/libs/example', ['src/**/*.ts'], ['**/*.spec.ts'])
// => { missing: ['src/orphan.ts'], covered: 4 }
§function

mergeLcov(lcovPaths: unknown): Map<string, FileCoverage>

Reads lcov reports and combines every record for the same file into one.
A file appears more than once whenever a module was evaluated more than once: after jest.resetModules a later import re-runs the module body, and V8 measures each evaluation as a script of its own. Treating those as separate files counts every line of the module once per evaluation while crediting only the evaluation that ran it, which drags the percentages down in proportion to how often a suite reset. Combining them is what Jest's provider did implicitly, by accumulating into counters keyed by file rather than by script.
Reports from different environments merge the same way, so a file exercised by one environment and merely imported by another is judged on both.
Anything a node:coverage ignore comment covers is dropped rather than measured. Node omits an ignored line from the report but still writes out the branches and functions sitting on it, marked untaken, while counting them as covered in its own totals. Dropping them is both what makes the two agree and what istanbul ignore used to mean.
Parameters
NameTypeDescription
§lcovPaths
unknownPaths to the emitted lcov files.
Returns
Map<string, FileCoverage>
One entry per file, keyed by the path the report names.
Example

Combining every environment's report into one entry per file

const files = mergeLcov(['/repo/coverage/libs/example/lcov.node.info', '/repo/coverage/libs/example/lcov.browser.info'])
files.get('src/double.ts')?.lines // => Map { 1 => 5, 2 => 4, 3 => 0 }, each count summed across records and reports
§function

overallTotals(files: Iterable<FileCoverage>): MetricTotals

Adds up the totals of every file in a report.
Parameters
NameTypeDescription
§files
Iterable<FileCoverage>The merged files.
Returns
MetricTotals
The report's totals.
Example

Adding up a whole report

overallTotals(mergeLcov(reports).values()).lines // => { found: 5, hit: 4 }, the sum over every file
§function

percentOf(metric: Metric): number

Renders a metric as a percentage.
A metric with nothing to measure is complete, which is what Jest reported for a file holding no branches at all.
Parameters
NameTypeDescription
§metric
MetricThe metric's totals.
Returns
number
The percentage covered.
Example

Rendering a metric as a percentage

percentOf({ found: 4, hit: 3 }) // => 75
percentOf({ found: 0, hit: 0 }) // => 100, nothing to measure counts as complete
§function

renderCoverageTable(files: unknown): string[]

Renders the per-file coverage table, closing with the project's totals.
Parameters
NameTypeDescription
§files
unknownThe merged files, in any order.
Returns
string[]
The table, one string per line.
Example

Printing the table a run ends with

console.log(renderCoverageTable([...mergeLcov(reports).values()]).join('\n'))
// file          | line % | branch % | funcs % | uncovered lines
// -------------------------------------------------------------
// src/double.ts |  66.67 |    50.00 |  100.00 |               3
// src/other.ts  | 100.00 |   100.00 |  100.00 |
// -------------------------------------------------------------
// all files     |  80.00 |    50.00 |  100.00 |
// -------------------------------------------------------------
§function

renderLcov(files: Iterable<FileCoverage>): string

Renders merged files back out as one lcov report.
The raw reports are what Node measured, one per environment and one record per evaluation. A consumer reading coverage off disk wants what the gate judged instead: one record per file, with the counts combined. Paths are written as given, so a caller that needs them anchored somewhere other than the project root rebases them first.
Parameters
NameTypeDescription
§files
Iterable<FileCoverage>The merged files, in any order.
Returns
string
The report's text, ending in a newline.
Example

Writing the merged report back out

const merged = mergeLcov(['coverage/lcov.node.info', 'coverage/lcov.browser.info'])
writeFileSync('coverage/lcov.info', renderLcov(merged.values()))
// one record per file, its counts combined, in the shape a coverage service reads
§function

uncoveredRanges(file: FileCoverage): string[]

Lists the lines a file never executed, collapsing runs into ranges.
Parameters
NameTypeDescription
§file
FileCoverageThe file's merged counters.
Returns
string[]
The uncovered ranges, in source order.
Example

Collapsing missed lines into ranges

const file = { path: 'src/a.ts', lines: new Map([[1, 1], [5, 0], [6, 0], [9, 0]]), branches: new Map(), functions: new Map() }
uncoveredRanges(file) // => ['5-6', '9']

◆ Types

§type

CompletenessReport

The outcome of comparing the coverage report against the files that should be in it.
§type

FileCoverage

One source file's merged counters.
The counters stay as maps rather than totals because merging is the whole point: two records for the same file have to be combined per line, per function, and per branch before any percentage means anything.
§type

Metric

What one metric's counters add up to.
§type

MetricTotals

The three metrics a run measures, each reduced to found and hit.