gren-coverage-node
v1.0.0
Published
Code coverage (line, function, and when/if branch) for Gren node applications
Maintainers
Readme
gren-coverage-node
Code coverage for Gren applications that run on Node. It reports coverage at
three levels of detail — per line, per function, and per when / if branch.
Instead of a single percentage, the goal is to answer a more useful question: "where are my tests missing?" — which functions and branches your code can reach but no test actually runs. Those are the tests you still need to write.
This tool works with any Gren node project. The examples below use the
gren-format-lib test suite because that is what it was first built against, but
nothing here is specific to it — point it at your own sources and your own app.
What it measures: four states
Every function and every when / if branch in your source is put into one of
four states. Telling them apart is the whole point of the tool:
| state | meaning | |-------|---------| | hit | ran at least once | | never-called | present in the compiled JavaScript, but ran 0 times — these are the tests to write | | eliminated | dropped by dead-code elimination (never compiled into JavaScript at all) | | absent | the module never showed up in the source map |
A tool that only looked at the source map would quietly leave eliminated code out of the totals, hiding it. This tool also reads your parsed source, so eliminated code stays visible in the report.
How coverage works, in short
Gren compiles to JavaScript. When you build with gren make --sourcemaps, it
also writes a source map that says which JavaScript came from which line of
your Gren source. Node has coverage built in: set NODE_V8_COVERAGE=<dir> and
it records how many times each piece of the JavaScript ran.
This tool combines the two: it takes the run counts from Node, follows the source map back to your Gren code, and compares that against every function and branch in your source (which it finds by reading your project). The result is the four-state report above.

Install
npm install -g gren-coverage-node
gren-coverage-node --helpgren-coverage-node is the command used everywhere below. See
DEPLOY.md if you'd rather build from source and install the
tarball yourself (./build.sh → npm pack → npm install -g
./gren-coverage-node-*.tgz).
If you're hacking on this tool itself rather than just using it, you need
devbox (it pins Node and the Gren compiler
for you) — ./build.sh produces ./app, runnable directly as ./app
<command> or node app <command>.
Using it in your project
There are four steps. You can run them by hand once to understand them, then wrap them in a script (see the full example at the end).
Assume your project has an entry-point module called Main (for a test suite,
that is usually your test runner's Main), and that you stay in your
project root for all four steps — even where Main itself lives in a
subdirectory with its own gren.json, like a tests/ test harness. Working
from the root keeps coverage.json's file paths clean and lets join index
your actual sources rather than the test harness's own (usually trivial)
gren.json.
1. Build your app with a source map
Compile the app you want to measure, asking for a source map. Do not name the
output *.js — a .js output builds a module that defines your program but
never starts it, so nothing runs. Use any other name (here, cov-app). If
Main lives in a subdirectory, cd there just for the build:
( cd tests && gren make Main --sourcemaps --output=cov-app )(No subdirectory? Drop the cd tests && and run gren make Main --sourcemaps
--output=cov-app directly.)
For a test suite, Main is your test harness's entry point. The source map is
embedded in cov-app, so there is nothing extra to keep track of.
2. Run it under Node's coverage
Run the app you just built with NODE_V8_COVERAGE pointing at an empty
directory. Node writes raw coverage data there:
mkdir -p v8cov
( cd tests && NODE_V8_COVERAGE="$PWD/../v8cov" node cov-app )This is a normal run of your app or test suite — let it do whatever it normally
does. When it finishes, v8cov/ holds the run counts.
3. Join it into a coverage report
Combine the run counts and the source map with your project's sources. Still from the project root:
gren-coverage-node join \
--app tests/cov-app \
--cov v8cov \
--src . \
--out coverage.jsonjoin reads --src's gren.json (here, the project root — pass . or drop
--src entirely if join is already discovering the right one from the
current directory), reads its source-directories, and indexes every
function and branch itself — so you don't have to list your sources.
coverage.json is the source of truth — the four-state label for every region.
4. Render the report
Print a human-readable report to the terminal:
gren-coverage-node render text coverage.jsonFor example, running this against gren-format-lib's own test suite prints
(abridged):
gren-coverage-node — cov-app
functions 844 hit 1 never 13 elim 55 absent 913 total 99.9% of reachable
branches 1304 hit 284 never 40 elim 131 absent 1759 total 82.1% of reachable
Untested code — reachable but no fixture exercises it: (21 modules with gaps)
Formatter.Logical.InsertExpressions src/Formatter/Logical/InsertExpressions.gren
functions 107 hit 1 never 0 elim branches 118 hit 33 never
never let innerOpNode :114 innerOpNode =
never when Nothing in insertAccess :416
never if Array.isEmpty fieldNodes in insertUpdate :500
… and more (see --module Formatter.Logical.InsertExpressions)
… 20 more modules with gaps (use --all or --top N)
annotate one module fully: gren-coverage-node render text coverage.json --module <Name>The hit / never / elim / absent columns are the four states from the top of
this README: never is code that ran zero times — the tests worth writing —
while elim (dead-code-eliminated) stays counted instead of silently vanishing.
Or produce a standard LCOV file, which editors and genhtml understand:
gren-coverage-node render lcov coverage.json > coverage.lcov
genhtml coverage.lcov -o html --branch-coverage # browsable HTML reportgenhtml gives you an overview page with the overall rates and a per-directory
table:

Click into a file to see it line by line — hit counts in the gutter, red for a
line that never ran, and branch markers for each when / if:

A note on genhtml/lcov versions
Different lcov releases don't all validate the same way. Newer genhtml
(2.1 and up) does a stricter internal consistency check than older versions
did: every line mentioned in an FN record also has to show up in a DA
record, or it fails outright with something like:
genhtml: ERROR: (category) unexpected category UNK for line <file>:<line>Older lcov (2.0 and earlier) just let that slide.
gren-coverage-node handles this; render lcov fills in a DA row for every function's
start line, even eliminated ones. We've checked the output against both
lcov 2.0 and lcov 2.4 and both render cleanly.
Commands
gren-coverage-node <command>:
| command | what it does |
|---------|--------------|
| join --app <app> --cov <v8dir> [--src <dir>] [--out <file>] | index your sources, then combine them with the run counts + source map into coverage.json |
| render text <coverage.json> [--top N] [--all] [--module <Name>] | terminal report; --module prints one module's full source annotated with run counts |
| render lcov <coverage.json> | print a standard LCOV file (for genhtml or editor gutters) |
render text adds color on a terminal and respects
NO_COLOR.
One detail worth knowing: regions are tracked by exact (row, column), never
rounded to whole lines. So a declaration whose last line spills over onto the
next line doesn't wrongly mark that next line as covered.
A complete example
run-coverage.sh in this repo wraps all the steps into
one script, run against gren-format-lib's own test suite. It's a good
template to copy for your own project. It assumes gren-coverage-node is already
on your PATH (it checks and bails with a pointer to DEPLOY.md if not), and
that the caller's working directory is already the root of the project being
measured — so you run it as:
( cd /path/to/gren-format-lib && /path/to/gren-coverage-node/run-coverage.sh )The core of it is:
# 1. build the test harness with a source map (output is NOT *.js)
( cd tests && gren make Main --sourcemaps --output=cov-app )
# 2. run the tests under Node coverage
rm -rf out/v8cov && mkdir -p out/v8cov
( cd tests && NODE_V8_COVERAGE="$PWD/../out/v8cov" node cov-app )
# 3. join — index the project (--src) and combine with the run counts
gren-coverage-node join --app "$PWD/tests/cov-app" --cov out/v8cov \
--src "$PWD" --out out/coverage.json
# 4. render a terminal report, an lcov file, and (if genhtml is on PATH) HTML
gren-coverage-node render lcov out/coverage.json > out/coverage.lcov
command -v genhtml && genhtml out/coverage.lcov -o out/html --branch-coverage
gren-coverage-node render text out/coverage.jsonThat project also lets you trigger the whole thing from its own test runner —
run-tests.sh --coverage simply execs run-coverage.sh. That is a convenience
of that project's setup, not a requirement; your project can wire it in
however you like.
Layout
build.sh builds the CLI into ./app (chmod +x'd)
run-coverage.sh the full worked example (build → run → join → render → html)
package.json npm packaging — exposes ./app as the `gren-coverage-node` bin
DEPLOY.md how to build, test the packaged tarball, and publish
gren.json / devbox.json the Gren app (platform: node)
src/
Main.gren command-line wiring + dispatch
Coverage/Schema.gren the coverage.json data format (decoders)
Coverage/Index.gren discovers + walks your parsed source (the denominator)
Command/Join.gren `join` — builds the index, then wraps gren-coverage.js
Command/RenderText.gren `render text`
Command/RenderLcov.gren `render lcov`
gren-coverage.js the join engine (stays JavaScript — see below)Why the join step is JavaScript
Part of the join step decodes the source map (base64 VLQ) and Node's coverage
data, which reports positions as UTF-16 code-unit offsets into the generated
JavaScript. Redoing that in Gren would mean re-matching Node's exact offset rules
— easy to get wrong on any non-ASCII character, for no benefit to you. So that
decode stays in gren-coverage.js. The Gren join command (Command/Join.gren)
does the rest natively: it indexes your sources, then runs gren-coverage.js
for the decode and passes its output along. Everything else is native Gren too.
join locates the script by looking in the same directory as the running
program, so gren-coverage.js must sit next to the built app. The build
puts both at the top level of this repo, so this works out of the box — but if
you move or copy app elsewhere, bring gren-coverage.js along with it. This
also holds for the npm package: app and gren-coverage.js are packaged and
installed side by side, and the lookup still resolves correctly through npm's
node_modules/.bin symlink (Node resolves the symlink to its real path before
join looks for its sibling).
