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.coverablesfile 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.coverablesfiles, and any number ofcoverage.datfiles, and produces a machine-readablereport.jsonfile, 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 idThis 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 acoverablesoutput to a Haskell package.addCoverage: Add acoverageoutput to a Haskell package.addCoverablesAndCoverage: both of the aboveaddCoverageReport: Add a coveragereportoutput 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
--exceptionfor 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