lib/App/Project/Doctor/Check/CpanReadiness.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 95.00%
TER3 (LCSAJ): 100.0% (7/7)
Approximate LCSAJ segments: 21

LCSAJ Legend

Covered — this LCSAJ path was executed during testing.

Not covered — this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start → end → jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package App::Project::Doctor::Check::CpanReadiness;
    2: 
    3: use strict;
    4: use warnings;
    5: use autodie qw(:all);
    6: 
    7: use parent -norequire, 'App::Project::Doctor::Check::Base';
    8: 
    9: use Carp qw(croak carp);
   10: use Readonly;
   11: 
   12: our $VERSION = '0.02';
   13: 
   14: Readonly::Scalar my $VERSION_RE    => qr/^\d+\.\d+(?:\.\d+)?(?:_\d+)?$/;
   15: # Changes and MANIFEST must use these exact names; README accepts variants below.
   16: Readonly::Array  my @REQUIRED_FILES  => qw(Changes MANIFEST);
   17: # CPAN and GitHub both accept any of these forms as the distribution README.
   18: Readonly::Array  my @README_VARIANTS => qw(README README.md README.pod README.rst README.txt);
   19: 
   20: sub name        { 'CPAN Readiness' }
   21: sub description { 'Version format, Changes, MANIFEST, and a README variant are present.' }
   22: sub can_fix     { 0 }
   23: sub order       { 90 }
   24: 
   25: sub check {
26 → 33 → 48   26: 	my ($self, $ctx) = @_;
   27: 	croak 'check requires an App::Project::Doctor::Context' unless ref $ctx;
   28: 
   29: 	my @findings;
   30: 
   31: 	# Version format check.
   32: 	my $version = _read_version($ctx);
   33: 	if (defined $version) {

Mutants (Total: 1, Killed: 1, Survived: 0)

34: if ($version !~ $VERSION_RE) {

Mutants (Total: 1, Killed: 1, Survived: 0)

35: push @findings, _f( 36: severity => 'error', 37: message => "Version '$version' does not match CPAN format (X.YY or X.YY.ZZ).", 38: ); 39: } 40: } else { 41: push @findings, _f( 42: severity => 'warning', 43: message => 'Could not determine distribution version from any module.', 44: ); 45: } 46: 47: # Required release files (exact names required by CPAN toolchain). 48 → 48 → 59 48: for my $file (@REQUIRED_FILES) { 49: unless ($ctx->has_file($file)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

50: push @findings, _f( 51: severity => 'error', 52: message => "'$file' is missing from the distribution root.", 53: ); 54: } 55: } 56: 57: # README is required but any common variant is acceptable. README.md is the 58: # norm on GitHub; CPAN itself accepts all of these without complaint. 59 → 59 → 67 59: unless (grep { $ctx->has_file($_) } @README_VARIANTS) {

Mutants (Total: 1, Killed: 1, Survived: 0)

60: push @findings, _f( 61: severity => 'error', 62: message => 'README is missing -- none of ' . join(', ', @README_VARIANTS) . ' found.', 63: ); 64: } 65: 66: # Changes file must have at least one version entry. 67 → 67 → 79 67: if ($ctx->has_file('Changes')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

68: my $content = $ctx->slurp('Changes'); 69: unless ($content =~ /^\d+\.\d+/m || $content =~ /^v\d+/m) {

Mutants (Total: 1, Killed: 1, Survived: 0)

70: push @findings, _f( 71: severity => 'warning', 72: message => 'Changes file has no version entries.', 73: file => 'Changes', 74: ); 75: } 76: } 77: 78: # MANIFEST stale-check requires 'make manifest' -- too invasive; just advise. 79 → 79 → 87 79: if ($ctx->has_file('MANIFEST')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

80: push @findings, _f( 81: severity => 'info', 82: message => "MANIFEST present -- run 'make manifest' to verify it is not stale.", 83: ); 84: } 85: 86: # Emit a pass only when there are no errors or warnings. 87 → 88 → 95 87: my $has_problem = grep { $_->severity =~ /^(?:error|warning)$/ } @findings; 88: unless ($has_problem) {

Mutants (Total: 1, Killed: 1, Survived: 0)

89: push @findings, _f( 90: severity => 'pass', 91: message => 'Distribution meets basic CPAN readiness requirements.', 92: ); 93: } 94: 95: return @findings;

Mutants (Total: 2, Killed: 2, Survived: 0)

96: } 97: 98: # --------------------------------------------------------------------------- 99: # Private helpers 100: # --------------------------------------------------------------------------- 101: 102: sub _f { 103: require App::Project::Doctor::Finding; 104: return App::Project::Doctor::Finding->new(check_name => 'CPAN Readiness', @_);

Mutants (Total: 2, Killed: 2, Survived: 0)

105: } 106: 107: sub _read_version { 108 → 109 → 115 108: my $ctx = shift; 109: for my $mod (@{ $ctx->lib_modules }) { 110: my $content = eval { $ctx->slurp($mod) } // next; 111: if (my ($v) = $content =~ /^\s*our\s+\$VERSION\s*=\s*['"]?([^'";\s]+)['"]?/m) {

Mutants (Total: 1, Killed: 1, Survived: 0)

112: return $v;

Mutants (Total: 2, Killed: 2, Survived: 0)

113: } 114: } 115: return undef;

Mutants (Total: 2, Killed: 2, Survived: 0)

116: } 117: 118: 1; 119: 120: __END__ 121: 122: =head1 NAME 123: 124: App::Project::Doctor::Check::CpanReadiness - Pre-upload CPAN readiness check 125: 126: =head1 DESCRIPTION 127: 128: Performs a final pre-flight sweep: version format, C<Changes>, C<MANIFEST>, 129: README presence, and basic C<Changes> content. 130: 131: For the README requirement any of the following file names is accepted: 132: C<README>, C<README.md>, C<README.pod>, C<README.rst>, C<README.txt>. 133: An error is only raised when B<none> of these exist. 134: 135: =head1 METHODS 136: 137: =head2 check( $context ) 138: 139: =head3 API SPECIFICATION 140: 141: =head4 Input 142: 143: $context : App::Project::Doctor::Context 144: 145: =head4 Output 146: 147: List of App::Project::Doctor::Finding with severities: 148: error -- version format wrong, required file absent, no README variant found 149: warning -- version undetermined, Changes has no version entries 150: info -- MANIFEST present (stale-check advisory) 151: pass -- all criteria met (only when no errors or warnings) 152: 153: =head3 MESSAGES 154: 155: Code | Trigger | Resolution 156: -----|-------------------------------------------|------------------------------------------- 157: R001 | Version format invalid | Use X.YY or X.YY.ZZ 158: R002 | Changes or MANIFEST missing | Create the file 159: R003 | No README variant found | Add README, README.md, README.pod, etc. 160: R004 | Changes has no version entries | Add a changelog entry 161: 162: =head3 FORMAL SPECIFICATION 163: 164: README_VARIANTS = {README, README.md, README.pod, README.rst, README.txt} 165: 166: check : Context -> [Finding] 167: check ctx == 168: version_check ctx 169: ++ [file_check f | f <- REQUIRED_FILES] 170: ++ (if (exists v in README_VARIANTS: ctx has_file v) then [] else [error]) 171: ++ changes_check ctx 172: ++ (if no problems then [pass] else []) 173: 174: =head1 AUTHOR 175: 176: Nigel Horne C<< <njh@nigelhorne.com> >> 177: 178: =head1 LICENSE 179: 180: Copyright (C) 2026 Nigel Horne. 181: This library is free software; you can redistribute it and/or modify 182: it under the same terms as Perl itself. 183: 184: =cut