Dekking is a next-generation coverage report tool for Haskell. It is implemented as a GHC plugin, as opposed to HPC, which is built into GHC.
Current status: Used in Prod in all my products.
There are a few pieces of the puzzle. The relevant programs are:
dekking-plugin
: Modifies the parsed source file within GHC as a source-to-source transformation plugin. At compile-time, this plugin also outputs a.hs.coverables
file which contains information about which parts of the source file are coverable and where those pieces are within the source. The source is transformed such that, when compiled, the result will output coverage information incoverage.dat
.ghc
: Compiles the resulting modified source codedekking-report
: Takes the*.hs.coverables
files, and any number ofcoverage.dat
files, and produces a machine-readablereport.json
file, as well as human readable HTML files which can be viewed in a browser.
The source-to-source transformation works as follows;
We replace every expression e
by adaptValue "identifier for e" e
.
The identifier is generated by dekking-plugin
at parse-time.
To give an idea of what this looks like, we would transform this expression:
((a + b) * c)
into this expression (f = adaptValue "identifier for e"
):
((f a) + (f b)) * (f c)
The adaptValue
function mentioned above is implemented in the very small dekking-value
package, in the Dekking.ValueLevelAdapter
module.
It looks something like this:
{-# NOINLINE adaptValue #-}
adaptValue :: String -> (forall a. a -> a)
adaptValue logStr = unsafePerformIO $ do
hPutStrLn coverageHandle logStr
hFlush coverageHandle
pure id
This function uses the problem of unsafePerformIO
, namely that the IO is only executed once, as a way to make sure that each expression is only marked as covered once.
Each coverable comes with a location, which is a triple of a line number, a starting column and an ending column. This location specifies where the coverable can be found in the source code.
The *.hs.coverables
files are machine-readable JSON files.
The coverage.dat
files are text files with a line-by-line description of which pieces of the source have been covered.
Each line is split up into five pieces:
<PackageName> <ModuleName> <line> <start> <end>
For example:
dekking-test-0.0.0.0 Examples.Multi.A 4 1 5
Nix support is a strong requirement of the dekking
project.
A flake has been provided.
The default package contains the following passthru
attributes:
addCoverables
: Add acoverables
output to a Haskell package.addCoverage
: Add acoverage
output to a Haskell package.addCoverablesAndCoverage
: both of the aboveaddCoverageReport
: Add a coveragereport
output to a Haskell package, similar todoCoverage
.compileCoverageReport
: Compile a coverage report (internal, you probably won't need this.)makeCoverageReport
: Produce a coverage report from multiple Haskell packages. Example usage:{ fuzzy-time-report = dekking.makeCoverageReport { name = "fuzzy-time-coverage-report"; packages = [ "fuzzy-time" "fuzzy-time-gen" ]; }; }
See the e2e-test
directory for many more examples.
TODO
Only expressions are evaluated, so only expressions can be covered. Expression coverage also shows you alternative coverage because alternatives point to an expression. Top-level bindings are not somehow special either. They are a code organisation tool that need not have any impact on whether covering them is more important.
Making automated decisions using a coverage percentage is usually a
shortsighted way to use that number.
If you really want to automate such a thing, you can use the report.json
file
that dekking-report
outputs.
Because of RankNTypes
and limitations of ImpredicativeTypes
, sometimes the source-transformed version of a function does not type-check anymore.
(See [ref:ThePlanTM]
, [ref:-XImpredicativeTypes]
, and [ref:DisablingCoverage]
.)
A common example is Servant's hoistServerWithContext
, see ghc ticket 22543.
There are three ways to selectively turn off coverage:
- With an
--exception
for the plugin:-fplugin-opt=Dekking.Plugin:--exception=My.Module
- With a module-level annotation:
{-# ANN module "NOCOVER" #-}
- With a function-level annotation:
{-# ANN hoistServerWithContext "NOCOVER" #-}
- Strong nix support
- Multi-package coverage reports
- Coupling with GHC
TODO write these out