lcov-utils
parse, format and merge in one place

Status
Public beta
Install
yarn add lcov-utils
Usage
import fs from 'node:fs/promises'
import { parse, format, merge, sum, badge, LCOV } from 'lcov-utils'
const contents = await fs.readFile('lcov.info', 'utf8')
const lcov = parse(contents)
const str = format(lcov)
const lcov2 = parse(await fs.readFile('lcov2.info', 'utf8'))
const lcov3 = merge(lcov, lcov2)
const output = format(lcov3)
await fs.writeFile('lcov-merged.info', output)
const digest = sum(lcov3)
const covbadge = badge(lcov3)
LCOV.stringify === format
LCOV.parse === parse
parse
Converts LCOV-string input to JSON.
import { parse } from 'lcov-utils'
const cov = parse(lcov, {
prefix: '',
})
const cov1 = parse(lcov, {
prefix: (sf) => sf.replace('packages/foo', 'packages/bar')
})
format
Converts JSON back to LCOV-string.
import { format } from 'lcov-utils'
const lcov = format(cov)
merge
Assembles several lcov reports into one.
import { merge } from 'lcov-utils'
const lcov1 = parse(await fs.readFile('lcov1.info', 'utf8'))
const lcov2 = parse(await fs.readFile('lcov2.info', 'utf8'))
const lcov3 = merge(lcov1, lcov2)
collide
Joins lcov reports, but operates with entire (prefixed) scope blocks.
Makes sense when you are updating a previous monorepo report with coverage changes for certain packages.
import { collide } from 'lcov-utils'
const prev = parse(await fs.readFile('lcov.info', 'utf8'))
const prefix = 'packages/foo'
const delta = parse(await fs.readFile('packages/foo/lcov.info', 'utf8'), {prefix})
const lcov = collide(prev, [delta, prefix])
💡 use merge
to assemble reports for the same scope obtained from different tasks (unit, it, e2e).
And invoke collide
to build or update the entire monorepo coverage report.
sum
Calculates coverage metrics.
import {sum} from 'lcov-utils'
const digest = sum(lcov)
Pass prefix
as the second argument to calculate coverage for a specific scope.
const digestFiltered = sum(lcov, prefix)
badge
Returns a string that creates a custom shields.io badge.

import { badge } from 'lcov-utils'
const covbadge = badge(lcov, {
color: 'auto',
style: 'flat',
title: 'coverage',
pick: 'max',
url: '',
gaps: [
[95, 'brightgreen'],
[90, 'green'],
[80, 'yellowgreen'],
[70, 'yellow'],
[60, 'orange'],
[0, 'red']
]
})
Or you can just render a json to use endpoint-badge API.
import {badgeJson} from 'lcov-utils'
const json = badgeJson(lcov, {
})
See also: stevenhair/lcov-badge2
Monorepo snippet
import fs from 'node:fs/promises'
import path from 'node:path'
import glob from 'fast-glob'
import minimist from 'minimist'
import { merge, parse, format, sum } from 'lcov-utils'
const {_: patterns, cwd = process.cwd(), output = 'lcov.info'} = minimist(process.argv.slice(2), {
string: ['cwd', 'output']
})
const paths = patterns.length > 0
? patterns
: await getWsCoveragePaths(cwd)
const outFile = path.resolve(cwd, output)
const files = (await glob(paths, {
cwd,
absolute: true,
onlyFiles: true
}))
const lcovs = await Promise.all(
files.map(async f => {
const contents = await fs.readFile(f, 'utf8')
const prefix = path.relative(cwd, path.resolve(path.dirname(f), '../..')) + '/'
return parse(contents, {prefix})
})
)
const lcov = merge(...lcovs)
await fs.writeFile(outFile, format(lcov), 'utf8')
async function getWsCoveragePaths(cwd) {
const workspaces = JSON.parse(await fs.readFile(path.resolve(cwd, 'package.json'), 'utf8'))?.workspaces || []
return workspaces.map(w => [`${w}/coverage/lcov.info`, `${w}/target/coverage/lcov.info`]).flat()
}
console.log(sum(lcov))
Data structures
export type LcovEntry = {
tn: string
sf: string
fn: [number, string][]
fnf: number
fnh: number
fnda: [number, string][]
da: [number, number][]
lf: number
lh: number
brda: [number, number, number, number][]
brf: number
brh: number
}
export type Lcov = Record<string, LcovEntry>
https://manpages.debian.org/stretch/lcov/geninfo.1.en.html#FILES
Caveats
- If the original input has duplicates, they will be squashed.
TN:
SF:src/test/js/test.mjs
FN:76,assert.throws.message
FN:77,assert.throws.message
FNF:2
FNH:2
FNDA:1,assert.throws.message
FNDA:1,assert.throws.message
DA:1,1
DA:2,1
DA:3,1
- Transpilers may bring their own artifacts (wrapper fragments, polyfills, etc) to lcov:
FN:1,topoconfig
FNF:8
FNH:5
FNDA:0,v
FNDA:0,O
FNDA:1,V
FNDA:1,W
FNDA:1,get
FNDA:1,B
FNDA:0,d
- If a function is marked with
FNDA
in multiple lcov reports (unit, it, e2e), we cannot determine with certainty whether these hits should be summed (module caching), so we just use the known max.
- The lib follows an optimistic approach: no validation built-in, it will try to parse anything until failure.
Refs
Parsers
Mergers
Etc
License
MIT