Code Coverage
Behave can track which lines of your application code execute during a spec run
and report what percentage was covered. Pass --coverage to bin/behave, and
Behave re-runs your specs as a subprocess with MoarVM's per-line coverage
logging enabled, parses the resulting log, and prints a report.
behave --coverage specs/
By default, only files under lib/ (relative to the working directory) are
tracked. Use --coverage-include / --coverage-exclude to change the scope.
Output formats#
--coverage-format=FORMAT selects the report format. Available formats:
| Format | Best for |
|---|---|
html |
Browsable per-file report with green/red line highlighting (default). |
text |
Colored per-file coverage summary printed to the terminal. |
json |
Machine-readable. Also the baseline format for --coverage-baseline. |
lcov |
genhtml, Codecov, Coveralls. |
cobertura |
Jenkins, GitLab CI, Azure DevOps. |
behave --coverage specs/ # writes ./coverage/ (html)
behave --coverage --coverage-format=text specs/ # terminal summary
behave --coverage --coverage-format=json --coverage-output=coverage.json specs/
behave --coverage --coverage-format=lcov --coverage-output=coverage.lcov specs/
--coverage-output=PATH writes the report to PATH instead of stdout.
html is special: PATH is a directory (default ./coverage/) that
Behave fills with index.html, style.css, and one HTML page per source
file. The index is the entry point: open coverage/index.html.
Hit counts#
By default behave records only whether each line ran, using the lighter
MVM_COVERAGE_CONTROL=0 mode whose log deduplicates repeated hits at the
source. Pass --coverage-counts to record how many times each line executed
instead. This runs the heavier MVM_COVERAGE_CONTROL=2 mode (every execution
is logged) and adds hit counts to the report:
behave --coverage --coverage-counts specs/
- text and html index: a
Hitscolumn showing each file's total executions across its covered lines. - html file page: a per-line count gutter beside the source.
- json: a
total-hitsvalue per file plus aline-hitsmap of line number to count.
--coverage-counts is slower than the default because the raw log grows with
every line transition rather than collapsing to one row per line. Use it when
you want an execution heatmap, not just covered/uncovered.
Filtering tracked files#
--coverage-include=PATH restricts the report to files whose path starts
with PATH. Repeatable. Multiple includes combine with OR. The prefix
semantics are deliberate: they mean a default of lib/ matches the relative
paths your specs load via -Ilib while excluding Rakudo/NQP internals
(/Users/.../rakudo/share/nqp/lib/...) that happen to contain lib/ in the
middle of an absolute path.
--coverage-exclude=PATH removes files whose path contains PATH
(substring match). Repeatable. An exclude match wins over an include match.
behave --coverage --coverage-include=lib/BDD --coverage-exclude=lib/BDD/Vendor specs/
When no --coverage-include is passed, Behave includes lib/ by default.
Excluding lines with nocov markers#
To drop a region of a source file from coverage, wrap it in a pair of
# :nocov: comment markers. The marker must be the only content on its line
(a #, optional whitespace, then :nocov:). Lines between the opening and
closing marker count as neither covered nor missed, so they do not lower the
percentage and render neutral in the HTML report.
sub parse-record($line) {
my $parsed = decode($line);
# :nocov:
unless $parsed.defined {
die "unreachable: decode never returns an undefined value";
}
# :nocov:
$parsed;
}
An opening marker with no matching closer excludes everything through the end
of the file. The markers belong in the implementation files being measured,
not in specs. They apply to both line coverage and --coverage-branch, so a
branch line inside a nocov block is not counted as a branch.
Thresholds#
--coverage-minimum=PCT fails the run when overall line coverage is below PCT
(a number between 0 and 100). The behave exit code is non-zero when the
threshold is not met, even if every spec passed. Useful as a CI gate.
behave --coverage --coverage-minimum=85 specs/
Branch coverage#
--coverage-branch adds a second line to the summary that reports how many
branching constructs (if, elsif, unless, with, without, when,
while, until, given, for) were exercised by the run.
behave --coverage --coverage-branch specs/
The branch metric is also emitted in JSON, LCOV (BRF / BRH / BRDA), and
Cobertura output.
Comparing against a baseline#
A previous JSON coverage report can be passed via --coverage-baseline=PATH,
and Behave will print a per-file diff (newly covered lines, newly uncovered
lines, regressed files, improved files).
# Capture a baseline once
behave --coverage --coverage-format=json --coverage-output=baseline.json specs/
# Later: compare current state against it
behave --coverage --coverage-baseline=baseline.json specs/
This pairs naturally with --coverage-minimum: in CI, keep baseline.json
checked in and surface a regression when coverage drops in a PR.
Configuration file#
Coverage flags can also live in a .behave config file (project or user):
use BDD::Behave::Configuration;
configure-behave -> $c {
$c.coverage = True;
$c.coverage-minimum = 80;
$c.coverage-format = 'text';
$c.coverage-branch = True;
$c.coverage-counts = True;
$c.coverage-include-path('lib/');
$c.coverage-exclude-path('lib/vendor');
}
CLI flags override config-file values. List-style options
(coverage-include-path, coverage-exclude-path) accumulate across config
layers and the CLI.
How it works#
--coverage sets MVM_COVERAGE_LOG and MVM_COVERAGE_CONTROL in a child
process that runs the same bin/behave invocation. MoarVM writes a HIT line
to a temp file under $TMPDIR for each executed source line. The default
control mode 0 deduplicates at the source, so each line appears about once;
--coverage-counts switches to mode 2, which logs every execution (a hot
line can then appear millions of times). When the child exits, the parent runs
grep -F over that file to keep only lines matching the include patterns, then
awk tallies the matches into one row per unique line prefixed with its
occurrence count. This keeps the filtered log small. The parent parses the
tallied result and renders the report.
On a full suite the raw log can take a while to filter. When stderr is a terminal, Behave streams the raw bytes through the filter itself and draws a single continuously updated progress line with a percent, a bar, and a rough ETA based on bytes processed over elapsed time. Off a terminal (CI, piped output) the filter reads the files directly and prints nothing.
Under --parallel, the wrapper is skipped: the parallel parent gives each
worker its own MVM_COVERAGE_LOG path
($TMPDIR/behave-coverage-parallel-<pid>-<stamp>/worker-N.raw), and after
the workers exit it tallies every per-worker log together in a single awk
pass, unioning each line's hits across workers (and summing the counts when
--coverage-counts is in effect). The same report
pipeline then renders one combined report against the merged data.
--coverage-minimum is gated on the merged percentage. See
Coverage under --parallel in the parallel
guide for the full description.
Executable line counts are derived from a static read of each source file:
blank lines, comments, lines that contain only closing punctuation (},
), etc.), lines inside =begin pod / =end pod blocks, and lines between
# :nocov: markers are ignored.
Limitations#
- macOS and Linux only. Relies on
grepinPATH. - Coverage runs are slower than ordinary runs because MoarVM logs every
executed line in user code (including Rakudo's startup path). The raw
log can reach several GB under
$TMPDIRfor a full-suite run. It is unlinked once the report is generated. - Branch coverage is line-based: a single
ifline that contains both branches inline counts as one branch.