elm-coverage
consists of a fair number of moving parts. This is my attempt
to document how those parts work together, and which part is responsible for
what.
The runner or supervisor is the main entrypoint and is responsible for glueing all the pieces together to a coherent whole.
Its responsibilities are roughly these:
- Parse the commandline arguments
- Traverse the source-path, looking for Elm-files
- Create a backup of all of these and instrument the originals in-place using
elm-instrument
- Modify
tests/elm-package.json
to know where theCoverage
module is - Run
elm-test
- Restore all the backups (sources and
tests/elm-package.json
- Instruct the analyzer to analyze the generated coverage files and create a report
- Optionally, try to open the generated report in the user's browser
Instrumentation is handled by an AST->AST transformation implemented in a fork
of elm-format
- since that project happens to have the highest quality
parser+writer in the ecosystem, battle-tested on hundreds of projects.
The AST is traversed and modified while also keeping track of a few bit of
information. Certain expressions (specifically the bodies of declarations,
let-declarations, lambda's, if/else branches and case..of branches) are
instrumented with a let _ = Coverage.track <moduleIdentifier>
<expressionIdentifier> in
expression. Whenever instrumentation is added, some
information about the instrumented expression is tracked.
Source location | Cyclomatic complexity | Name | |
---|---|---|---|
Declaration | x | x | x |
Let declaration | x | x | |
Lambda body | x | x | |
if/else branch | x | ||
case..of branch | x |
The recorded information is accumulated for all instrumented modules and
persisted to .coverage/info.json
.
The Coverage
module, which is "linked in" by the runner, exposes a single
function:
Coverage.track : String -> Int -> Never -> a
It is passed the module-name and an offset in the total list of track expressions of a module, and returns a function that can never be called. When evaluated, the internal coverage-data is updated; incrementing a simple counter based on the module-name and offset.
When the active process signals elm-test that all of its tests have finished
running, the coverage data is persisted to disk in a coverage-{{pid}}.json
file.
The analyzer is a thin wrapper around an Elm module. The wrapper reads in the
info.json
file created by the instrumenter, all the coverage files created
by the elm-test
run, and all the sources of the referenced files. Once all
the data is read, it is bundled up and sent off to an Elm module for further
processing.
The Elm module parses all that data and creates the HTML report, returning the generated report as a String over a port.