Test2::...::Cover(3)
π Test2::Plugin::Cover β the manual, but make it cute π
okay bestie, gather round, because we're talking about Test2::Plugin::Cover (that's section 3 of the perl manual, for the record) β and honestly? she's giving fast and minimal file coverage info, and she ate. β¨
πΈ NAME πΈ
Test2::Plugin::Cover β Fast and Minimal file coverage info. no cap, that's literally the whole name, she's efficient like that π
π¦ DESCRIPTION π¦
so here's the vibe: this plugin collects minimal file coverage info, and it does it with minimal performance impact. lowkey iconic behavior.
Every single time a subroutine gets called, this tool does its best to figure out what filename that subroutine was defined in, and adds it to a list. Also β and this is the fun part β anytime you try to open a file with open() or sysopen(), that file gets added to the list too. π
Then, right before the test exits, that list gets attached to a test2 event. In most formatters the event will only show up as a comment on STDOUT that looks like:
# This test covered N source files.
but tools like Test2::Harness::UI can make full use of the coverage information contained in that event. main character tools only, periodt. π
β¨ INTENDED USE CASE β¨
okay so first things first: this tool is not here to record comprehensive coverage information. If that's what you want, go use Devel::Cover, she's got you covered (pun fully intended, I'm sorry).
This tool is intended to obtain and maintain lists of files that were opened, or which define subs which were executed by any given test. That info is genuinely useful if you want to figure out what test files to run after any given code change. iykyk.
The collected coverage data lives inside test2 events. If you use Test2::Harness aka "yath", then this data can be logged and consumed by other tools such as Test2::Harness::UI. π«Ά
π₯ PERFORMANCE π₯
Unlike tools that need to record comprehensive coverage (looking at you, Devel::Cover), this module is only concerned about what files you open, or defined subs executed directly or indirectly by a given test file. Because of that, this module can get away with a tiny bit of XS code that only fires when a subroutine is called. Most coverage tools fire off XS for every statement. we're not doing all that, bestie. π§
π LIMITATIONS π
ngl, there are some. This tool uses XS to inject a little bit of C code that runs every time a subroutine is called, or every time open() or sysopen() is called. This C code grabs the next op that will be run and tries to pull the filename from it. "eval", XS, Moose, and other magic can sometimes mask the filename, and this module only makes a minimal attempt to find the filename in these cases. so like. she tries her best. π
Originally this module only collected the filenames touched by a test. Now, on top of that data, it can give you separate lists of files where subs were called, and files that were touched via open(). Additionally, the sub list includes info about what subs were called. And in all of these cases it's also possible to know what sections of your test called the subs or opened the files. glow-up, honestly. β
π« THINGS THAT WILL NOT SHOW UP π«
- goto β
"goto &sub"does not enter the sub the normal way, so the target sub (and its file, if different) will not be recorded. The sub doing the"goto"is recorded normally though. so at least there's that. π - constants β Constants created with
"use constant", and any other subs inlined at compile time, never trigger a runtime sub call. So the file defining them will not be recorded unless something else in it is called. they're just too fast for us, sorry. π - XS subs β XS subs have no perl source file, so calls to them are recorded against the file of the next perl statement that executes, which is usually the caller's file.
- threads β Coverage data is collected per-thread and is not merged. Data collected inside spawned ithreads is lost unless you merge it yourself. you're on your own for that one bestie π«
- exotic open() forms β Only 2 and 3 argument
open()calls are recorded. The list form for piping to programs ("open($fh, '-|', $prog, @args)") and 1-argopen()are ignored. we don't do those here.
π REAL EXAMPLES π
The following data was gathered using prove to run the full Moose test suite. look at the receipts, she's showing her work:
# Prove on its own
Files=478, Tests=17326, 64 wallclock secs ( 1.62 usr 0.46 sys + 57.27 cusr 4.92 csys = 64.27 CPU)
# Prove with Test2::Plugin::Cover (no coverage event)
Files=478, Tests=17326, 67 wallclock secs ( 1.61 usr 0.46 sys + 60.98 cusr 5.31 csys = 68.36 CPU)
# Prove with Devel::Cover
Files=478, Tests=17324, 963 wallclock secs ( 2.39 usr 0.58 sys + 929.12 cusr 31.98 csys = 964.07 CPU)
no coverage event β No report was generated. This was done to only measure the effect of the XS that adds the data collection overhead, and not the cost of the perl code that generates the report event at the end of every test. science! π¬
The Moose test suite was also run using Test2::Harness aka "yath":
# Without Test2::Plugin::Cover
Wall Time: 62.51 seconds CPU Time: 69.13 seconds (usr: 1.84s | sys: 0.08s | cusr: 60.77s | csys: 6.44s)
# With Test2::Plugin::Cover (no coverage event)
Wall Time: 75.46 seconds CPU Time: 82.00 seconds (usr: 1.96s | sys: 0.05s | cusr: 72.64s | csys: 7.35s)
As you can see, there is a performance hit, but it's fairly small β especially compared to Devel::Cover. This is not to say anything bad about Devel::Cover, which is amazing, but a bad choice for the use case Test2::Plugin::Cover was written to address. no shade, just different lanes. π
π SYNOPSIS π
INLINE
use Test2::Plugin::Cover;
...
# Arrayref of files covered so far
my $covered_files = Test2::Plugin::Cover->files;
COMMAND LINE
You can tell prove to use the module this way:
HARNESS_PERL_SWITCHES=-MTest2::Plugin::Cover prove ...
For yath:
yath test --cover-files ...
π SUPPRESS REPORT π
You can suppress the final report (only collect data, do not send the Test2 event). shy girl behavior, honestly.
CLI:
HARNESS_PERL_SWITCHES=-MTest2::Plugin::Cover=no_event,1 prove ...
INLINE:
use Test2::Plugin::Cover no_event => 1;
π EXCLUDE DIRECTORIES π
A directory under the root β such as a dependency tree installed inside the workspace β can be kept out of the coverage data entirely:
CLI:
HARNESS_PERL_SWITCHES=-MTest2::Plugin::Cover=exclude,deps prove ...
INLINE:
use Test2::Plugin::Cover exclude => 'deps';
# Or several at once
use Test2::Plugin::Cover exclude => ['deps', 'vendor'];
An excluded path and everything under it, at any depth, is dropped before the coverage event is sent, so excluded files never reach tools consuming that event. Paths are compared component by component, so excluding "lib" does not exclude a sibling "library". Relative paths are resolved against the current directory.
Wildcards are not supported β an exclusion is a literal path. Excluding a whole tree needs only the top of it. π«π
π΅οΈ KNOWING WHAT CALLED WHAT π΅οΈ
If you use a system like Test::Class, Test::Class::Moose, or Test2::Tools::Spec, then you divide your tests into subtests (or similar). In these cases it would be nice to track what subtest (or equivalent) touched what files. and guess what, bestie β you can. π
There are 3 methods related to this: set_from(), get_from(), and clear_from(), which you can use to manage this meta-data:
subtest foo => sub {
# Note, this is a simple string, but the 'from' data can also be a data
# structure.
Test2::Plugin::Cover->set_from("foo");
# subroutine() from Some.pm will be recorded as having been called by 'foo'.
Some::subroutine();
Test2::Plugin::Cover->clear_from();
};
Doing this manually for all blocks is not ideal. Ideally you would hook your tool, such as Test::Class, to call set_from() and clear_from() for you. Adding such a hook is left as an exercise to the reader, and if you make one for a popular tool, please upload it to cpan and add a ticket or send an email for me to link to it here. we love a community contribution moment π«Ά
Once you have these hooks in place, the data will not only show files and subs that were called, but what called them. chef's kiss. π
Please see the set_from() documentation for details on values. β¨
π CLASS METHODS π
Ok bestie, this is the good part. The actual methods. Sit down, hydrate, we're doing this. π
$class->touch_source_file($file)
$class->touch_source_file($file, $sub)
$class->touch_source_file($file, \@subs)
$class->touch_source_file($file, $subs, $from)
This one is your manual override, your "I'll do it myself" moment π . You use it to hand-add coverage data whenever you want.
- First argument: the source file you want to be "touched" by coverage. Required, obviously.
- Second argument: optional. Either a subroutine name, or an arrayref of subroutine names.
- Third argument: optional, and it overrides the default
"from"value β which normally gets figured out for you automatically.
If you don't specify any subroutines at all, it defaults to '*', which basically means the entire file is considered touched. No cap.
$class->touch_data_file($file)
$class->touch_data_file($file, $from)
Same energy, different flavour. This manually adds coverage data too. First argument is the file being "touched" by coverage data. Optionally you override the 'from' value, which β you guessed it β is normally determined automatically. β¨
This is literally the same as calling:
$class->touch_source_file($file, '<>')
β οΈ Heads up bestie: both touch methods are no-ops while coverage is disabled. See disable() below. So if you're wondering why nothing's happening... check that first. π
$class->enable()
$class->disable()
$bool = $class->enabled()
Toggle it or check it. When it's disabled, no coverage is recorded. That's the whole vibe. Periodt.
$class->reload()
Resets the filter if $0 or __FILE__ have changed. This is advanced usage, bestie β you will probably never need this. It's giving "only touch this if you know what you're doing". π§
$val = $class->get_from()
Grabs the current 'from' value. Default is '*' when nothing has set a from value. Simple as.
$class->set_from($val)
Sets a 'from' value. It can be anything β a string, a hashref, whatever your heart desires. But be advised: it usually gets serialized to JSON, so make sure whatever you put in there is actually json-serializable. π
If the value is β or contains β a CODE or GLOB reference, it can't be serialized into the final report. So a warning gets issued and the value gets ignored (the previous 'from' value stays in effect). This is not fatal, and that's on purpose: enabling coverage should never introduce new exceptions into the code being observed. Considerate queen behavior honestly. π«Ά
$class->clear_from()
Resets the clear value to '*'. Boom, done.
$bool = $class->was_from_modified()
Returns true if anything has called set_from() or set_from_manager(). You can flip it back to false using reset_from(), which also clears the 'from' and 'from_manager' values. Clean slate energy. π
$class->set_from_manager($module)
Set this to a module that implements the following method:
sub test_parameters {
my $class = shift;
my ($test_file, \@from_values) = @_;
...
return {
# If true - run the test
# If false - skip the test
# If not present or undef - run the test
run => $bool,
# The following are optional
argv => [ ... ],
env => { ... },
stdin => "...",
};
# OR
# If true - run the test
# If false - skip the test
# If undef or empty list - run the test
return $bool;
}
This gets used by Test2::Harness to figure out what data needs to be passed to a test, given a set of 'from' values β so the test knows to run the necessary parts/subtests/groups/methods/etc. Slay.
- The
'argv'data gets prepended before any other arguments provided to the test. - The
'env'hashref gets merged with any other env vars needed, with these taking priority. - The
'stdin'string gets used as STDIN for the test.
$arrayref = $class->files()
$arrayref = $class->files(root => $path)
$arrayref = $class->files(exclude => $path_or_arrayref)
Returns an arrayref of all files touched so far. β¨
- The list is sorted alphabetically, and duplicates get removed. Tidy.
- If a root path is provided, it may be a
Path::Tinyinstance or a plain string. That path filters out any files not under the root directory. - If an exclude path is provided, that path and everything under it is left out of the results. It may be a single path or an arrayref of them, each a
Path::Tinyinstance or a plain string.
π The running test file ($0) and this plugin's own file are always excluded from the results. Always. Non-negotiable.
$hashref = $class->data()
$hashref = $class->data(root => $path)
$hashref = $class->data(exclude => $path_or_arrayref)
This returns the processed coverage data that goes into the report event:
{
# Files where subs were called
'lib/Foo.pm' => {
some_sub => [ list of 'from' values that called it ],
# Called BEGIN/END/etc blocks, or subs whose name could not be
# determined, fall under '*'.
'*' => [ ... ],
},
# Files opened with open()/sysopen(), or touched as data files
'data.json' => { '<>' => [ ... ] },
}
Duplicate 'from' values get removed (compared by content, not by reference), and each list is sorted deterministically. The "root" and "exclude" parameters behave exactly like they do in files(). π
$event = $class->report(%options)
Sends a Test2 event containing coverage info. It also returns the event, so you get it both ways, icon behavior. π
Options, bestie:
root => Path::Tiny->new("...")β Normally set to the current directory at module load-time. Used to filter out any source files that don't live under the current directory. May be aPath::Tinyinstance or a plain string.exclude => $path_or_arrayrefβ Paths to leave out of the report entirely, along with everything under them. May be a single path or an arrayref of them, each aPath::Tinyinstance or a plain string. Relative paths get resolved against the current directory, and wildcards are not supported. When passed at import time this option may be given more than once instead of using an arrayref β that's how it survives the-Mcommand line form.verbose => $BOOLβ If true, the comment stating how many source files were touched gets printed as a diagnostics message instead, so it shows up without a verbose harness.ctx => DO NOT USEβ This is used ONLY when the Test2::API is doing its final book-keeping. Most users will never want to use this. Like, genuinely, don't. π«
$class->reset_coverage()
Completely clears all coverage data so far. Poof. Gone. π¨
$class->reset_from()
Clears the 'from' value, and also resets the 'was_from_modified' state to false. Fresh start fr fr.
$class->full_reset()
Calls both reset_coverage() and reset_from(). The combo meal. π
$file_or_undef = $class->filter($file)
$file_or_undef = $class->filter($file, root => Path::Tiny->new('...'))
$file_or_undef = $class->filter($file, exclude => ['...'])
This method is used as a callback when getting the final list of covered source files. The default implementation removes any files that aren't under the current directory β which lets you focus on files in the distribution you're actually testing. You may return a modified filename if you want to normalize it here; the default implementation turns it into a relative path. β¨
If you provide a custom "root" parameter, it may be a Path::Tiny instance or a plain string.
β οΈ Exclusions get applied here. So a subclass that replaces this method is responsible for honoring the "exclude" parameter if it needs exclusions to keep working. Don't forget or you'll be sad. π
A custom filter callback should look something like this:
sub {
my $class = shift;
my ($file, %params) = @_;
# clean_filename() does not exist, it is just an example
$file = clean_filename($file, %params);
# should_show() does not exist, it is just an example
return $file if should_show(%params);
# Return undef or an empty list if you do NOT want to show the file.
return;
}
Please take a look at the source to see what and how filter() is implemented if you want all the details on how it works. π
$file_or_undef = $class->extract($file)
$file_or_undef = $class->extract($file, %params)
This method is responsible for extracting a sensible filename from whatever the XS found. Some magic, like "eval" or Moose, can set the "filename" to strings like '(eval 123)' or 'foo bar (defined at FILE line LINE)' β or even nonsensical strings, or text with no filenames at all. It's giving chaos. π
If a sensible file name can be extracted, it gets returned. Otherwise undef (or an empty list) is returned.
The default implementation doesn't use any parameters, but they get passed in for custom implementations to use.
A custom extract callback should look something like this:
sub {
my $class = shift;
my ($file, %params) = @_;
# It is a valid file
return $file if -e $file;
# Do not use this, just an example
return $1 if $file =~ m/($VALID_FILE_REGEX)/;
# Cannot find a file here
return;
}
π TRACING OPENS π
For debugging, you can ask the plugin to record where every open() and sysopen() call happened:
$Test2::Plugin::Cover::TRACE_OPENS = 1;
Every recorded open pushes an arrayref onto @Test2::Plugin::Cover::OPENS:
[$filename, $file, $line, $package]
Where $filename is what was being opened, and $file, $line and $package describe the code doing the open. π΅οΈββοΈ
This is a debugging aid only. The data is not included in coverage events. So don't go looking for it in your reports, bestie.
π SEE ALSO π
Devel::Cover is by far the best and most complete coverage tool for perl. If you need comprehensive coverage, use Devel::Cover. Genuinely, it ate. π
Test2::Plugin::Cover is only better for a limited use case.
πΈ SOURCE πΈ
The source code repository for Test2-Plugin-Cover can be found at https://github.com/Test-More/Test2-Plugin-Cover.
π«Ά MAINTAINERS π«Ά
Chad Granum <exodist@cpan.org>
β¨ AUTHORS β¨
Chad Granum <exodist@cpan.org>
π COPYRIGHT π
Copyright 2020 Chad Granum <exodist@cpan.org>.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
See http://dev.perl.org/licenses/
and that's the whole page bestie!! π this one's from perl v5.42.3, dated 2026-09-09, and it's the Test2::...::Cover(3) page β go forth and cover some code, you absolute icon ππ
π¦ where this page comes from
- fedora 43: perl-Test2-Plugin-Cover 0.0.29-1.fc43 Β· GPL-1.0-or-later OR Artistic-1.0-Perl
- fedora 44: perl-Test2-Plugin-Cover 0.0.29-1.fc44 Β· GPL-1.0-or-later OR Artistic-1.0-Perl
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms