NAME
Test::NoShadow - Fail CI when two distributions provide the same Perl package, or when t/ exercises a copy other than the one under test
VERSION
Version 0.001.0
SYNOPSIS
# In your CI, over a directory of checkouts:
use Test::More;
use Test::NoShadow qw(shadowed_ok lib_ok find_shadows);
shadowed_ok('/home/njh'); # fails if any Perl package is provided by two checkouts
# In a distribution's own t/, after using its modules:
use My::Module;
lib_ok('My::Module'); # fails if My::Module came from anywhere but this distribution
# Or inspect the data yourself:
my $shadows = find_shadows('/home/njh');
foreach my $package (sort keys %$shadows) {
diag("$package: @{ $shadows->{$package} }");
}
DESCRIPTION
Test::NoShadow fails your test suite when two distributions provide the same Perl package, and when a distribution's tests exercise a copy other than the one under test (an older installed copy, or a vendored copy in t/).
It was written to police a workspace of many checkouts: pairs such as Schema::Validator in both schema-validator and Schema-Validator, or a vendored VWF::* inside another distribution, ship identical package names, so use can silently load the wrong code.
No other CPAN module checks for the multi-checkout variant; the closest relative, Test::CleanNamespaces, works on installed distributions only.
Two checks are provided:
"shadowed_ok" - a directory of checkouts (or the installed
@INC) must not contain a Perl package in more than one place"lib_ok" - each named module must have been loaded from the distribution's own
lib/orblib/lib
FUNCTIONS/METHODS
shadowed_ok [@dirs, [ \%options ]]
Scans each directory (recursively) for .pm files and extracts the package names they declare. Passes if no Perl package is declared in more than one file; otherwise fails, with a diagnostic naming both providers.
With no directories given, @INC is scanned - for example:
prove -Ilib t/shadow.t
The following options are recognised in the optional hash reference:
ignore- array reference of regexps. Each is matched against both the"provider/package"pair and the file's path; if either matches, the file is excluded, e.g.{ ignore => [qr{^WWW-Scrape-BillionGraves/}, qr{/t/lib/}] }. The file's path is also matched with backslashes converted to forward slashes, so patterns such asqr{/t/lib/}work on Windows as well
Each top-level directory beneath the scanned directory is treated as a "provider" (in this workspace, a checkout); files directly beneath the scanned directory are labelled with their own file name. To check a single checkout for vendored copies, pass the checkout itself: its top-level directory (usually lib) becomes the provider, and copies under t/ are reported alongside copies under lib.
Returns true on success, false on failure.
lib_ok @modules, [ \%options ]
For use in the distribution's own tests, after the modules have been used. Passes if each named module was loaded from this distribution's lib or blib/lib; fails if a module was loaded from anywhere else (a shadowing copy installed in @INC), or was not loaded at all.
One test is run per module.
The following options are recognised:
dir- the root of the distribution, default .lib- array reference of library directories beneathdirthat count as "the copy under test", default[qw(lib blib/lib)]
Returns true if all modules passed.
find_shadows $dir, [ \%options ]
The non-TAP interface: returns a hash reference mapping each shadowed package name to a sorted array reference of the files that declare it. Accepts the same options as "shadowed_ok".
DIAGNOSTICS
No shadowed Perl packages-
All packages were found in exactly one file.
Foo::Bar is provided by 2 files:-
Each provider is listed on its own line, indented, as
provider: file. Foo::Bar was loaded from /path/to/Foo/Bar.pm, not from ... - is a shadowing copy installed?-
The module was loaded from outside this distribution's
liborblib/lib. An older copy installed in@INC, or a vendored copy int/, will be loaded by the test suite instead of the code being tested. Foo::Bar has not been loaded; use it before calling lib_ok-
lib_okcan only check modules that are already in%INC. Test::NoShadow: $root is not a directory-
The directory passed to "shadowed_ok" or "find_shadows" does not exist.
LIMITATIONS
Package detection is a line-based heuristic:
package Foo::Bar;,package Foo::Bar 1.23;andpackage Foo::Bar {are recognised, but a declaration split across lines, or the stringpackage Foo::Bar;inside a here-document or data section, will confuse it.Packages provided only by an XS boot file, without a
.pm, are not seen.Files under
t/that intentionally duplicate alib/package (test fixtures) are reported as shadows. Use theignoreoption to exclude them.Scanning a large
@INCreads every.pmfile in it and can take a few seconds.
SEE ALSO
Test::Which - skip tests if external programs are missing from PATH
Test::CleanNamespaces - check for leftover subroutines in installed distributions
Test::Builder - the test framework this module is built on
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.