From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use strict;
# ABSTRACT: Run Kwalitee tests including optional indicators, especially, prereq_matches_use
our $VERSION = 'v0.4.0'; # VERSION
use version 0.77;
use Cwd;
use Carp;
sub _exclude_proper_libs
{
my $target_ver = version->parse($Module::CPANTS::Analyse::VERSION);
return $target_ver == version->parse('0.88') || $target_ver > version->parse('0.89');
}
sub _init
{
return {
builder => Test::Builder->new,
exclude => {
# can not apply already unpacked dist
extractable => 1,
extracts_nicely => 1,
has_version => 1,
has_proper_version => 1,
_exclude_proper_libs() ? (proper_libs => 1) : (),
# already dirty in test phase
no_generated_files => 1,
manifest_matches_dist => 1,
},
include => {},
core => 1,
optional => 1,
experimental => 0,
analyser => Module::CPANTS::Analyse->new({
distdir => cwd(),
dist => cwd(),
}),
retry => 5,
};
}
sub _pmu_error_desc
{
my ($error, $remedy, $berror, $bremedy);
my $ref = Module::CPANTS::Kwalitee::Prereq->kwalitee_indicators;
foreach my $val (@$ref) {
($error, $remedy) = @{$val}{qw(error remedy)} if $val->{name} eq 'prereq_matches_use';
($berror, $bremedy) = @{$val}{qw(error remedy)} if $val->{name} eq 'build_prereq_matches_use';
}
$error ||= q{This distribution uses a module or a dist that's not listed as a prerequisite.};
$remedy ||= q{List all used modules in META.yml requires};
$berror ||= q{This distribution uses a module or a dist in its test suite that's not listed as a build prerequisite.};
$bremedy ||= q{List all modules used in the test suite in META.yml build_requires};
return ($error, $remedy, $berror, $bremedy);
}
sub _check_ind
{
my ($env, $ind) = @_;
return 1 if $env->{include}{$ind->{name}};
return 0 if $env->{exclude}{$ind->{name}};
if($ind->{is_experimental}) { # experimental
return $env->{experimental};
} elsif($ind->{is_extra}) { # optional
return $env->{optional};
} else { # core
return $env->{core};
}
}
sub _is_core
{
my ($module, $minperlver) = @_;
return 0 if defined Module::CoreList->removed_from($module);
my $fr = Module::CoreList->first_release($module);
return 0 if ! defined $fr;
return 1 if version->parse($minperlver) >= version->parse($fr);
return 0;
}
sub _do_test_one
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($test, $ok, $name, $error, $remedy, $more) = @_;
$test->ok($ok, $name);
if(!$ok) {
$test->diag(' Detail: ', $error);
$test->diag(' Detail: ', ref($more) ? join(', ', @$more) : $more) if defined $more;
$test->diag(' Remedy: ', $remedy);
}
}
sub _is_missing_check_for_old
{
my ($uses, $prereq, $build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, $missing, $bmissing, $qerror) = @_;
while(my ($key, $val) = each %$uses) {
next if version::is_lax($key); # perl version
# Skip packages provided by the distribution but not indexed by CPAN.
next if scalar( grep {$key eq $_} @$packages_not_indexed ) != 0;
next if _is_core($key, $minperlver);
next if $key =~ m'[$@%*&]'; # ignore entry including sigil
my $result = eval { $mcpan->module($key) };
if($@ || ! $result->distribution) {
$qerror->{$key} = 1;
next;
}
my $dist = $result->distribution;
push @$missing, $key.' in '.$dist if $val->{in_code} && $val->{in_code} != ($val->{evals_in_code} || 0) && ! exists $prereq->{$dist};
push @$bmissing, $key.' in '.$dist if $val->{in_tests} && $val->{in_tests} != ($val->{evals_in_tests} || 0) && ! exists $build_prereq->{$dist};
}
}
my %uses_keys = (
used_in_code => '',
required_in_code => '',
used_in_tests => 'build',
required_in_tests => 'build'
);
sub _is_missing_check_for_new
{
my ($uses, $prereq, $build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, $missing, $bmissing, $qerror) = @_;
foreach my $uses_keys (keys %uses_keys) {
while(my ($key, $val) = each %{$uses->{$uses_keys}}) {
next if version::is_lax($key); # perl version
# Skip packages provided by the distribution but not indexed by CPAN.
next if scalar( grep {$key eq $_} @$packages_not_indexed ) != 0;
next if _is_core($key, $minperlver);
next if $key =~ m'[$@%*&]'; # ignore entry including sigil
my $result = eval { $mcpan->module($key) };
if($@ || ! $result->distribution) {
$qerror->{$key} = 1;
next;
}
my $dist = $result->distribution;
if($uses_keys{$uses_keys} ne 'build') {
push @$missing, $key.' in '.$dist if ! exists $prereq->{$dist};
} else { # build
push @$bmissing, $key.' in '.$dist if ! exists $build_prereq->{$dist};
}
}
}
}
sub _do_test_pmu
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($env) = @_;
my ($error, $remedy, $berror, $bremedy) = _pmu_error_desc();
my ($test, $analyser) = @{$env}{qw(builder analyser)};
return if ! _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 }) &&
! _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
my $minperlver;
if(exists $env->{minperlver}) {
$minperlver = $env->{minperlver};
} else {
$minperlver = $];
for my $val (@{$analyser->d->{prereq}}) {
if($val->{requires} eq 'perl') {
$minperlver = $val->{version};
last;
}
}
}
my $mcpan = MetaCPAN::Client->new;
my %qerror;
my (%build_prereq, %prereq);
# NOTE: prereq part is kept in new stash layout of Module::CPANTS::Analyse since 0.93_01
foreach my $val (@{$analyser->d->{prereq}}) {
next if _is_core($val->{requires}, $minperlver);
my $retry = 0;
my $result;
while($retry < $env->{retry}) {
$result = eval { $mcpan->module($val->{requires}) };
if($@ || ! $result->distribution) {
++$retry;
} else {
last;
}
}
if($retry == $env->{retry}) {
$qerror{$val->{requires}} = 1;
next;
}
$prereq{$result->distribution} = 1 if $val->{is_prereq} || $val->{is_optional_prereq};
$build_prereq{$result->{distribution}} = 1 if $val->{is_prereq} || $val->{is_build_prereq} || $val->{is_optional_prereq};
}
# NOTE: uses part is changed in new stash layout of Module::CPANTS::Analyse since 0.93_01
my $is_old = grep { exists $analyser->d->{uses}{$_}{module} } keys %{$analyser->d->{uses}};
# Look at META.yml to determine if the author specified modules provided
# by the distribution that should not be indexed by CPAN.
my $packages_not_indexed = _get_packages_not_indexed(
d => $analyser->d,
distdir => $analyser->distdir,
is_old => $is_old,
);
my (@missing, @bmissing);
if($is_old) {
_is_missing_check_for_old($analyser->d->{uses}, \%prereq, \%build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, \@missing, \@bmissing, \%qerror);
} else {
_is_missing_check_for_new($analyser->d->{uses}, \%prereq, \%build_prereq, $minperlver, $mcpan, $is_old, $packages_not_indexed, \@missing, \@bmissing, \%qerror);
}
if(%qerror) {
$remedy = $bremedy = 'Fix query error(s) to MetaCPAN.';
}
_do_test_one($test, ! %qerror && @missing == 0, 'prereq_matches_use by '.__PACKAGE__, $error, $remedy,
! %qerror ? 'Missing: '.join(', ', sort @missing) : 'Query error: '.join(' ', sort keys %qerror))
if _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 });
_do_test_one($test, ! %qerror && @bmissing == 0, 'build_prereq_matches_use by '.__PACKAGE__, $berror, $bremedy,
! %qerror ? 'Missing: '.join(', ', sort @bmissing) : 'Query error: '.join(' ', sort keys %qerror))
if _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
}
# Look at META.yml to determine if the author specified modules provided
# by the distribution that should not be indexed by CPAN.
sub _get_packages_not_indexed
{
my (%args) = @_;
my $d = delete $args{'d'};
my $distdir = delete $args{'distdir'};
my $is_old = delete $args{'is_old'};
# Check if no_index exists in META.yml
my $meta_yml = $d->{'meta_yml'};
return [] if !defined $meta_yml;
my $no_index = $meta_yml->{'no_index'};
return [] if !defined $no_index;
# Get the uses, to determine which ones are no-index internals.
my $uses = $d->{'uses'};
return [] if !defined $uses;
# NOTE: uses part is changed in new stash layout of Module::CPANTS::Analyse since 0.93_01
if(!$is_old) {
my @uses;
push @uses, keys %{$uses->{$_}} for qw[used_in_code required_in_code used_in_tests required_in_tests];
$uses = { map { ($_ => undef) } @uses };
}
my $packages_not_indexed = {};
# Find all the files corresponding to the 'file' and 'directory'
# sections of 'no_index'.
my @files = ();
if (defined $no_index->{'file'}) {
push @files, map { File::Spec->catdir($distdir, $_) } @{$no_index->{'file'}};
}
if (defined $no_index->{'directory'}) {
my $filter_pm_files = sub {
return if $File::Find::name !~ /\.pm$/;
push(@files, $File::Find::name);
};
foreach my $directory (@{$no_index->{'directory'}}) {
my $no_meta_directory = File::Spec->catdir($distdir, $directory);
if(-d $no_meta_directory) {
File::Find::find(
$filter_pm_files,
$no_meta_directory,
);
}
}
}
# Extract the namespaces from those files.
foreach my $file (@files) {
my @namespaces = Module::Extract::Namespaces->from_file($file);
foreach my $namespace (@namespaces) {
next if !exists $uses->{$namespace};
$packages_not_indexed->{$namespace} = undef;
}
}
# 'package' section of no_index.
if (defined $no_index->{'package'}) {
foreach my $package (@{$no_index->{'package'}}) {
next if !exists $uses->{$package};
$packages_not_indexed->{$package} = undef;
}
}
# 'namespace' section of no_index.
if (defined $no_index->{'namespace'}) {
foreach my $use (keys %$uses) {
foreach my $namespace (@{$no_index->{'namespace'}}) {
next if $use !~ /^\Q$namespace\E(?:::|$)/;
$packages_not_indexed->{$use} = undef;
}
}
}
return [sort keys %$packages_not_indexed];
}
sub _count_tests
{
my ($env) = @_;
my ($test, $analyser) = @{$env}{qw(builder analyser)};
my $count = 0;
foreach my $mod (@{$analyser->mck->generators}) {
foreach my $ind (@{$mod->kwalitee_indicators}) {
next if $ind->{needs_db};
next if ! _check_ind($env, $ind);
++$count;
}
}
# overrides needs_db
++$count if _check_ind($env, { name => 'prereq_matches_use', is_extra => 1 });
++$count if _check_ind($env, { name => 'build_prereq_matches_use', is_experimental => 1 });
return $count;
}
sub _do_test
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($env) = @_;
my ($test, $analyser) = @{$env}{qw(builder analyser)};
if(! $env->{no_plan}) {
$test->plan(tests => _count_tests(@_));
}
foreach my $mod (@{$analyser->mck->generators}) {
$mod->analyse($analyser);
foreach my $ind (@{$mod->kwalitee_indicators}) {
next if $ind->{needs_db};
next if ! _check_ind($env, $ind);
_do_test_one(
$test,
$ind->{code}($analyser->d, $ind),
$ind->{name}.' by '.$mod,
$ind->{error},
$ind->{remedy},
$analyser->d->{error}{$ind->{name}}
);
}
}
_do_test_pmu($env);
}
my %class = ( core => 1, optional => 1, experimental => 1 );
sub import
{
my ($pkg, @arg) = @_;
my $env = _init();
my $ind_seen = 0;
while(my $arg = shift @arg) {
if($arg eq ':no_plan') {
$env->{no_plan} = 1;
} elsif($arg eq ':minperlver') {
$env->{minperlver} = shift @arg;
} elsif($arg eq ':retry') {
$env->{retry} = shift @arg;
} elsif($arg =~ /^!:/) {
warn "Tag $arg appears after indicator" if $ind_seen;
$arg =~ s/^!://;
if($arg eq 'all') {
$env->{core} = $env->{optional} = $env->{experimental} = 1;
} elsif($arg eq 'none') {
$env->{core} = $env->{optional} = $env->{experimental} = 0;
} elsif($class{$arg}) {
$env->{$arg} = 0;
} else {
warn "Unknown tag :$arg is used";
}
} elsif($arg =~ /^:/) {
warn "Tag $arg appears after indicator" if $ind_seen;
$arg =~ s/^://;
if($arg eq 'all') {
$env->{core} = $env->{optional} = $env->{experimental} = 0;
} elsif($arg eq 'none') {
$env->{core} = $env->{optional} = $env->{experimental} = 1;
} elsif($class{$arg}) {
$env->{$arg} = 1;
} else {
warn "Unknown tag :$arg is used";
}
} elsif($arg =~ /^!/) {
$ind_seen = 1;
$arg =~ s/^!//;
$env->{exclude}{$arg} = 1;
delete $env->{include}{$arg};
} else {
$ind_seen = 1;
$env->{include}{$arg} = 1;
delete $env->{exclude}{$arg};
}
}
_do_test($env);
}
1;
__END__
=pod
=head1 NAME
Test::Kwalitee::Extra - Run Kwalitee tests including optional indicators, especially, prereq_matches_use
=head1 VERSION
version v0.4.0
=head1 SYNOPSIS
# Simply use, with excluding indicators
use Test::Kwalitee::Extra qw(!has_example !metayml_declares_perl_version);
# Use with eval guard, with excluding class
use Test::More;
eval { require Test::Kwalitee::Extra; Test::Kwalitee::Extra->import(qw(!:optional)); };
plan( skip_all => "Test::Kwalitee::Extra not installed: $@; skipping") if $@;
# Typically, this test is limited to author test or release test
BEGIN { # limited to release test
unless ($ENV{RELEASE_TESTING}) { # or $ENV{AUTHOR_TESTING} for author test
require Test::More;
Test::More::plan(skip_all => 'these tests are for release candidate testing');
}
}
use Test::More;
eval { require Test::Kwalitee::Extra; Test::Kwalitee::Extra->import(qw(!:optional)); };
plan( skip_all => "Test::Kwalitee::Extra not installed: $@; skipping") if $@;
# Avoid network access
use Test::Kwalitee::Extra qw(!prereq_matches_use);
# or, when experimental enabled
use Test::Kwalitee::Extra qw(:experimental !prereq_matches_use !build_prereq_matches_use);
=head1 DESCRIPTION
L<CPANTS|http://cpants.cpanauthors.org/> checks Kwalitee indicators, which is not quality but automatically-measurable indicators how good your distribution is. L<Module::CPANTS::Analyse> calcluates Kwalitee but it is not directly applicable to your module test. CPAN has already had L<Test::Kwalitee> for the test module of Kwalitee. It is, however, impossible to calculate C<prereq_matches_use> indicator, because dependent module L<Module::CPANTS::Analyse> itself cannot calculate C<prereq_matches_use> indicator. It is marked as C<needs_db> which means pre-calculated module database is necessary, but only limited information is needed to calculate the indicator. This module calculate C<prereq_matches_use> to query needed information to L<MetaCPAN site|https://metacpan.org/> online.
For available indicators, see L</INDICATORS> section.
=head1 OPTIONS
You can specify including or excluding an indicator or a tag like L<Exporter>. Valid tags are C<core>, C<optional> and C<experimental>. For indicators, see L<Module::CPANTS::Analyse>.
Please NOTE that to specify tags are handled a bit differently from L<Exporter>. First, specifying an indicator is always superior to specifying tags, even though specifying an indicator is prior to specifying tags.
For example,
use Test::Kwalitee::Extra qw(!has_example :optional);
C<!has_example> is in effect, that is C<has_example> is excluded, even though C<has_example> is an C<optional> indicator.
Second, default excluded indicators mentioned in L</INDICATORS> section are not included by specifying tags. For example, in the above example, C<:optional> does not enable C<is_prereq>. You can override it by explicitly specifying the indicator:
use Test::Kwalitee::Extra qw(manifest_matches_dist);
=head2 SPECIAL TAGS
Some tags have special meanings.
=head2 C<:no_plan>
If specified, do not call C<Test::Builder::plan>. You may need to specify it, if this test is embedded into other tests.
=head2 C<:minperlver> <C<version>>
C<prereq_matches_use> indicator ignores core modules. What modules are in core, however, is different among perl versions. If minimum perl version is specified in META.yml or such a meta information, it is used as minimum perl version. Otherewise, C<$]>, the version of the current perl interpreter, is used.
If specified, this option overrides them.
=head2 C<:retry> <C<count>>
The number of retry to query to MetaCPAN. This is related with C<prereq_matches_use> and C<build_prereq_matches_use> indicators only.
Defaults to 5.
=head1 CAVEATS
An optional indicator C<prereq_matches_use> and an experimental indicator C<build_prereq_matches_use> require HTTP access to L<MetaCPAN site|https://metacpan.org/>. If you want to avoid it, you can specify excluded indicators like
# Avoid network access
use Test::Kwalitee::Extra qw(!prereq_matches_use);
# or, when experimental enabled
use Test::Kwalitee::Extra qw(:experimental !prereq_matches_use !build_prereq_matches_use);
Or mitigate wait by tentative failures to reduce retry counts like
# Try just one time for each query
use Test::Kwalitee::Extra qw(:retry 1);
=head1 INDICATORS
In L<Module::CPANTS::Analyse>, C<prereq_matches_use> requires CPANTS DB setup by L<Module::CPANTS::ProcessCPAN>. C<is_prereq> really requires information of prereq of other modules but C<prereq_matches_use> only needs mappings between modules and dists. So, this module query the mappings to MetaCPAN by using L<MetaCPAN::Client>.
Recently, L<Module::CPANTS::Analyse> has been changed much. For actual available indicators, please consult C<Module::CPANTS::Kwalitee::*> documentation. For default configuration, indicators are treated as follows:
=over 4
=item NOTES
=over 4
=item B<(+)>
No longer available for L<Module::CPANTS::Analyse> 0.88 or 0.90+.
=item B<(++)>
No longer available for L<Module::CPANTS::Analyse> 0.90+.
=item B<(+++)>
No longer available for L<Module::CPANTS::Analyse> 0.88 or 0.90+, moved to L<Module::CPANTS::SiteKwalitee|https://github.com/cpants/Module-CPANTS-SiteKwalitee>.
=item B<(++++)>
No longer available for L<Module::CPANTS::Analyse> 0.88 or 0.90+, moved to L<Module::CPANTS::SiteKwalitee|https://github.com/cpants/Module-CPANTS-SiteKwalitee> but supported by this module.
=back
=item Available indicators in core
=over 4
=item *
has_readme
=item *
has_manifest
=item *
has_meta_yml
=item *
has_buildtool
=item *
has_changelog
=item *
no_symlinks
=item *
has_tests
=item *
buildtool_not_executable B<(++)>
=item *
metayml_is_parsable
=item *
metayml_has_license B<(optional for 0.88 or 0.90+)>
=item *
metayml_conforms_to_known_spec
=item *
proper_libs B<(for 0.87 or 0.89)>
=item *
no_pod_errors B<(+)>
=item *
has_working_buildtool B<(+)>
=item *
has_better_auto_install B<(+)>
=item *
use_strict
=item *
valid_signature B<(+++)>
=item *
has_humanreadable_license B<(for 0.87 or 0.89)> | has_human_redable_license B<(for 0.88 or 0.90+)>
=item *
no_cpants_errors B<(+)>
=back
=item Available indicators in optional
=over 4
=item *
has_tests_in_t_dir
=item *
has_example B<(+)>
=item *
no_stdin_for_prompting
=item *
metayml_conforms_spec_current
=item *
metayml_declares_perl_version
=item *
prereq_matches_use B<(++++)>
=item *
use_warnings
=item *
has_test_pod B<(+)>
=item *
has_test_pod_coverage B<(+)>
=back
=item Excluded indicators in core
=over 4
=item Can not apply already unpacked dist
=over 4
=item *
extractable B<(+)>
=item *
extracts_nicely B<(+)>
=item *
has_version B<(+)>
=item *
has_proper_version B<(+)>
=back
=item Already dirty in test phase
=over 4
=item *
manifest_matches_dist
=item *
no_generated_files B<(++)>
=back
=back
=item Excluded indicators in optional
=over 4
=item Can not apply already unpacked dist
=over 4
=item *
proper_libs B<(for 0.88 or 0.90+)>
=back
=item Needs CPANTS DB
=over 4
=item *
is_prereq B<(+++)>
=back
=back
=item Indicators with special note in experimental
=over 4
=item *
build_prereq_matches_use B<(++++)>
=back
=back
=head1 SEE ALSO
=over 4
=item *
L<Module::CPANTS::Analyse> - Kwalitee indicators, except for prereq_matches_use, are calculated by this module.
=item *
L<Test::Kwalitee> - Another test module for Kwalitee indicators.
=item *
L<Dist::Zilla::Plugin::Test::Kwalitee::Extra> - Dist::Zilla plugin for this module.
=back
=head1 AUTHOR
Yasutaka ATARASHI <yakex@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2017 by Yasutaka ATARASHI.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut