TER1 (Statement): 100.00%
TER2 (Branch): 95.00%
TER3 (LCSAJ): 100.0% (7/7)
Approximate LCSAJ segments: 21
● 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.
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