TER1 (Statement): 100.00%
TER2 (Branch): 91.67%
TER3 (LCSAJ): 100.0% (11/11)
Approximate LCSAJ segments: 25
● 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::GHGen::PerlCustomizer; 2: 3: use v5.36; 4: use strict; 5: use warnings; 6: 7: use Path::Tiny; 8: 9: use Exporter 'import'; 10: our @EXPORT_OK = qw( 11: detect_perl_requirements 12: generate_custom_perl_workflow 13: ); 14: 15: our $VERSION = '0.10'; 16: 17: =encoding utf-8 18: 19: =head1 NAME 20: 21: App::GHGen::PerlCustomizer - Customize Perl workflows based on project requirements 22: 23: =head1 SYNOPSIS 24: 25: use App::GHGen::PerlCustomizer qw(detect_perl_requirements); 26: 27: my $requirements = detect_perl_requirements(); 28: # Returns: { min_version => '5.036', has_cpanfile => 1, ... } 29: 30: =head1 FUNCTIONS 31: 32: =head2 detect_perl_requirements() 33: 34: Detect Perl version requirements and dependency-file presence in the current directory. 35: 36: =head3 Purpose 37: 38: Inspect the current working directory for common Perl distribution files 39: (C<cpanfile>, C<Makefile.PL>, C<dist.ini>, C<Build.PL>) and extract the 40: minimum Perl version declared in any of them. 41: 42: =head3 Arguments 43: 44: None. 45: 46: =head3 Returns 47: 48: A hash reference with the following keys, all of which are always present: 49: 50: { 51: min_version => Str | undef, # e.g. '5.036'; undef when undetected 52: has_cpanfile => Bool, 53: has_makefile_pl => Bool, 54: has_dist_ini => Bool, 55: has_build_pl => Bool, 56: } 57: 58: =head3 Side Effects 59: 60: Reads files from the current working directory. 61: 62: =head3 Usage Example 63: 64: use App::GHGen::PerlCustomizer qw(detect_perl_requirements); 65: my $reqs = detect_perl_requirements(); 66: say $reqs->{min_version} // 'not specified'; 67: 68: =head3 API SPECIFICATION 69: 70: =head4 Input 71: 72: # No parameters. 73: 74: =head4 Output 75: 76: { 77: type => 'hashref', 78: keys => { 79: min_version => { type => 'scalar', optional => 1 }, 80: has_cpanfile => { type => 'scalar' }, 81: has_makefile_pl => { type => 'scalar' }, 82: has_dist_ini => { type => 'scalar' }, 83: has_build_pl => { type => 'scalar' }, 84: }, 85: } 86: 87: =head3 FORMAL SPECIFICATION 88: 89: detect_perl_requirements : â Requirements 90: 91: Requirements â { 92: min_version: â¤* ⪠{ ⥠}, 93: has_cpanfile: ð¹, 94: has_makefile_pl: ð¹, 95: has_dist_ini: ð¹, 96: has_build_pl: ð¹, 97: } 98: 99: min_version â 100: cpanfile exists â§ version parseable from cpanfile â version 101: Makefile.PL exists â§ MIN_PERL_VERSION parseable â version 102: otherwise â ⥠103: 104: =cut 105: 106: sub detect_perl_requirements() { ●107 → 122 → 129 107: my %reqs = ( 108: min_version => undef, 109: has_cpanfile => 0, 110: has_makefile_pl => 0, 111: has_dist_ini => 0, 112: has_build_pl => 0, 113: ); 114: 115: # Check for dependency files 116: $reqs{has_cpanfile} = path('cpanfile')->exists; 117: $reqs{has_makefile_pl} = path('Makefile.PL')->exists; 118: $reqs{has_dist_ini} = path('dist.ini')->exists; 119: $reqs{has_build_pl} = path('Build.PL')->exists; 120: 121: # Try to detect minimum Perl version 122: if ($reqs{has_cpanfile}) {Mutants (Total: 1, Killed: 1, Survived: 0)
123: my $content = path('cpanfile')->slurp_utf8; 124: if ($content =~ /requires\s+['"]perl['"],?\s+['"]([0-9.]+)['"]/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
125: $reqs{min_version} = $1; 126: } 127: } 128: ●129 → 129 → 136 129: if (!$reqs{min_version} && $reqs{has_makefile_pl}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
130: my $content = path('Makefile.PL')->slurp_utf8; 131: if ($content =~ /MIN_PERL_VERSION\s*=>\s*['"]([0-9.]+)['"]/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
132: $reqs{min_version} = $1; 133: } 134: } 135: 136: return \%reqs;
Mutants (Total: 2, Killed: 2, Survived: 0)
137: } 138: 139: =head2 generate_custom_perl_workflow($options) 140: 141: Generate a customized Perl CI workflow and return it as a YAML string. 142: 143: B<Options:> 144: 145: =over 4 146: 147: =item C<perl_versions> (array ref) 148: 149: Explicit list of Perl versions to include in the test matrix, e.g. 150: C<['5.40', '5.38', '5.36']>. Takes precedence over C<min_perl_version> 151: and C<max_perl_version> when supplied. 152: 153: =item C<min_perl_version> (string, default C<'5.36'>) 154: 155: Lowest Perl version to include in the matrix when C<perl_versions> is not 156: given. Accepts both C<'5.036'> and C<'5.36'> notation. 157: 158: =item C<max_perl_version> (string, default C<'5.42'>) 159: 160: Highest Perl version to include in the default matrix when C<perl_versions> 161: is not given. The built-in version table currently spans C<5.22> through 162: C<5.44>; the default produces the matrix C<5.36, 5.38, 5.40, 5.42>. 163: 164: To test against B<Perl 5.44> (opt-in, not tested by default), pass 165: C<< max_perl_version => '5.44' >> or set C<perl_versions> explicitly: 166: 167: generate_custom_perl_workflow({ max_perl_version => '5.44' }); 168: # matrix: 5.36, 5.38, 5.40, 5.42, 5.44 169: 170: generate_custom_perl_workflow({ perl_versions => ['5.42', '5.44'] }); 171: # matrix: 5.42, 5.44 172: 173: =item C<os> (array ref) 174: 175: Operating systems for the build matrix. 176: Default: C<['ubuntu-latest', 'macos-latest', 'windows-latest']>. 177: 178: =item C<timeout> (integer, default C<30>) 179: 180: Value written to C<timeout-minutes> on every matrix job. 181: 182: =item C<enable_linter> (boolean, default C<1>) 183: 184: When true, inserts a B<"Lint and syntax check"> step after all dependency 185: installation steps and before the test run. The step runs on B<every> 186: matrix cell (all OS and Perl version combinations) so that compile-time 187: errors are caught across the full version range being tested. 188: 189: The step uses C<shell: perl {0}>, which works identically on Linux, macOS, 190: and Windows without any OS-specific branching. It searches C<lib/> and 191: C<bin/> (falling back to C<.> when neither exists), loads each C<.pm> file 192: via C<do $file> with a stub C<@INC> handler that silently satisfies any 193: missing C<use> statements, and exits non-zero if any file fails. Using 194: C<do $file> avoids spawning C<perl -c> subprocesses (which have 195: Windows command-line quoting issues) and avoids false failures caused by 196: missing CPAN dependencies. B<No additional CPAN modules are required.> 197: 198: =item C<enable_linter_unused> (boolean, default C<1>) 199: 200: When true, appends an unused-variable check to the B<end of the lint step> 201: (i.e. it runs before the test run, not after). The check installs 202: L<warnings::unused> from CPAN, then runs 203: C<PERL5OPT=-Mwarnings::unused prove -lr t/> so that variable lifetimes are 204: exercised at runtime (C<perl -c> is compile-only and cannot detect unused 205: variables). It is gated on C<RUNNER_OS == Linux> and marked 206: C<continue-on-error: true> because unused-variable warnings can be 207: legitimately noisy on some codebases. 208: 209: =item C<enable_critic> (boolean, default C<1>) 210: 211: When true, adds a Perl::Critic step on the latest matrix Perl version and 212: C<ubuntu-latest>. 213: 214: =item C<enable_coverage> (boolean, default C<1>) 215: 216: When true, adds a Devel::Cover test-coverage step on the latest matrix Perl 217: version and C<ubuntu-latest>. 218: 219: =item C<enable_perlimports> (boolean, default C<1>) 220: 221: When true, adds a B<"Check imports with perlimports"> step on the latest matrix 222: Perl version and C<ubuntu-latest>. The step installs L<App::perlimports> from 223: CPAN and runs C<perlimports --lint> across all C<.pm> files under C<lib/>. It 224: is marked C<continue-on-error: true> because import hygiene warnings are 225: advisory; they should not block a CI run in established codebases. 226: 227: =back 228: 229: B<Generated step order:> 230: 231: =over 4 232: 233: =item 1. C<actions/checkout> 234: 235: =item 2. Setup Perl via C<shogo82148/actions-setup-perl> 236: 237: =item 3. Cache CPAN modules via C<actions/cache> 238: 239: =item 4. Install cpanm and C<local::lib> 240: 241: =item 5. Install project dependencies 242: 243: =item 6. B<Lint and syntax check> â all matrix cells (when C<enable_linter> is true); the unused-variable check via C<PERL5OPT=-Mwarnings::unused> is embedded at the end of this step when C<enable_linter_unused> is true 244: 245: =item 7. Run tests 246: 247: =item 8. Run Perl::Critic â latest Perl + Ubuntu only (when C<enable_critic> is true) 248: 249: =item 9. B<Check imports with perlimports> â latest Perl + Ubuntu only (when C<enable_perlimports> is true) 250: 251: =item 10. Test coverage â latest Perl + Ubuntu only (when C<enable_coverage> is true) 252: 253: =item 11. Show cpanm build log on failure 254: 255: =back 256: 257: =head3 API SPECIFICATION 258: 259: =head4 Input 260: 261: { 262: opts => { 263: type => 'hashref', 264: default => {}, 265: keys => { 266: perl_versions => { type => 'arrayref', optional => 1 }, 267: min_perl_version => { type => 'scalar', default => '5.36' }, 268: max_perl_version => { type => 'scalar', default => '5.42' }, 269: os => { type => 'arrayref', optional => 1 }, 270: timeout => { type => 'scalar', default => 30 }, 271: enable_linter => { type => 'scalar', default => 1 }, 272: enable_linter_unused => { type => 'scalar', default => 1 }, 273: enable_critic => { type => 'scalar', default => 1 }, 274: enable_coverage => { type => 'scalar', default => 1 }, 275: enable_perlimports => { type => 'scalar', default => 1 }, 276: }, 277: }, 278: } 279: 280: =head4 Output 281: 282: { type => 'scalar' } # multi-line YAML string starting with '---' 283: 284: =head3 FORMAL SPECIFICATION 285: 286: generate_custom_perl_workflow : Opts â YAML 287: 288: versions â opts.perl_versions ?? range(min, max) 289: latest â versions[|versions|â1] 290: 291: yaml contains "Lint and syntax check" step â opts.enable_linter = 1 292: yaml contains "PERL5OPT=-Mwarnings::unused" â opts.enable_linter_unused = 1 293: yaml contains "Run Perl::Critic" â opts.enable_critic = 1 294: yaml contains "Test coverage" â opts.enable_coverage = 1 295: yaml contains "Check imports with perlimports" â opts.enable_perlimports = 1 296: 297: step ordering invariant: 298: pos(lint) < pos(unused) < pos(tests) < pos(critic) < pos(perlimports) < pos(coverage) 299: 300: =cut 301: 302: sub generate_custom_perl_workflow($opts = {}) { ●303 → 315 → 321 303: my $min_version = $opts->{min_perl_version} // '5.36'; 304: my $max_version = $opts->{max_perl_version} // '5.42'; 305: my $timeout = $opts->{timeout} // 30; 306: my @os = @{$opts->{os} // ['ubuntu-latest', 'macos-latest', 'windows-latest']}; 307: my $enable_linter = $opts->{enable_linter} // 1; 308: my $enable_linter_unused = $opts->{enable_linter_unused} // 1; 309: my $enable_critic = $opts->{enable_critic} // 1; 310: my $enable_coverage = $opts->{enable_coverage} // 1; 311: my $enable_perlimports = $opts->{enable_perlimports} // 1; 312: 313: # Generate Perl version list - use explicit list if provided, otherwise min/max 314: my @perl_versions; 315: if ($opts->{perl_versions} && @{$opts->{perl_versions}}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
316: @perl_versions = @{$opts->{perl_versions}}; 317: } else { 318: @perl_versions = _get_perl_versions($min_version, $max_version); 319: } 320: ●321 → 350 → 353 321: my $yaml = "---\n"; 322: $yaml .= '# Created by ' . __PACKAGE__ . "\n"; 323: 324: $yaml .= "name: Perl CI\n\n"; 325: $yaml .= "'on':\n"; 326: $yaml .= " push:\n"; 327: $yaml .= " branches:\n"; 328: $yaml .= " - main\n"; 329: $yaml .= " - master\n"; 330: $yaml .= " pull_request:\n"; 331: $yaml .= " branches:\n"; 332: $yaml .= " - main\n"; 333: $yaml .= " - master\n\n"; 334: 335: $yaml .= "concurrency:\n"; 336: $yaml .= " group: \${{ github.workflow }}-\${{ github.ref }}\n"; 337: $yaml .= " cancel-in-progress: true\n\n"; 338: 339: $yaml .= "permissions:\n"; 340: $yaml .= " contents: read\n\n"; 341: 342: $yaml .= "jobs:\n"; 343: $yaml .= " test:\n"; 344: $yaml .= " runs-on: \${{ matrix.os }}\n"; 345: $yaml .= " timeout-minutes: $timeout\n"; 346: $yaml .= " strategy:\n"; 347: $yaml .= " fail-fast: false\n"; 348: $yaml .= " matrix:\n"; 349: $yaml .= " os:\n"; 350: for my $os (@os) { 351: $yaml .= " - $os\n"; 352: } ●353 → 354 → 363 353: $yaml .= " perl:\n"; 354: for my $version (@perl_versions) { 355: $yaml .= " - '$version'\n"; 356: } 357: # shogo82148/build-perl perl-5.40.4-thr-win32-x64.zip ships a perl.exe and a 358: # libperl540.a from two different builds. Any XS module compiled against that 359: # libperl540.a (key 0x12c00080) mismatches the running perl.exe (needs 360: # 0x12d00080). Even Win32::Process (a core Windows XS module bundled in the 361: # zip) fails to load, so no XS-using test can pass on this combination. 362: # Exclude until the upstream distribution is fixed. ●363 → 363 → 368 363: if ((grep { $_ eq '5.40' } @perl_versions) && (grep { $_ eq 'windows-latest' } @os)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
364: $yaml .= " exclude:\n"; 365: $yaml .= " - os: windows-latest\n"; 366: $yaml .= " perl: '5.40'\n"; 367: } ●368 → 465 → 503 368: $yaml .= " name: Perl \${{ matrix.perl }} on \${{ matrix.os }}\n"; 369: $yaml .= " env:\n"; 370: $yaml .= " AUTOMATED_TESTING: 1\n"; 371: $yaml .= " NO_NETWORK_TESTING: 1\n"; 372: $yaml .= " NONINTERACTIVE_TESTING: 1\n"; 373: $yaml .= " steps:\n"; 374: $yaml .= " - uses: actions/checkout\@v7\n\n"; 375: 376: $yaml .= " - name: Setup Perl\n"; 377: $yaml .= " id: setup-perl\n"; 378: $yaml .= " uses: shogo82148/actions-setup-perl\@v1.43.1\n"; 379: $yaml .= " with:\n"; 380: $yaml .= " perl-version: \${{ matrix.perl }}\n\n"; 381: 382: # Hash the actual Perl binary files so the cache key changes whenever the 383: # binary changes, even across silent Strawberry Perl re-releases that keep the 384: # same $Config{version} string but change the DLL ABI and handshake key. 385: # On Windows we hash both perl.exe and the Perl runtime DLL (e.g. perl540.dll) 386: # because perl.exe is a thin stub â only the DLL contains the real runtime. 387: # shell: perl {0} runs identically on Linux, macOS, and Windows. 388: $yaml .= " - name: Get exact Perl binary version for cache key\n"; 389: $yaml .= " id: perl-version\n"; 390: $yaml .= " shell: perl {0}\n"; 391: $yaml .= " run: |\n"; 392: $yaml .= <<'VERSION_STEP'; 393: use Config; 394: my @paths = ($^X); 395: if ($^O eq 'MSWin32' && $Config{libperl}) { 396: (my $dir = $^X) =~ s{[\\/][^\\/]+$}{}; 397: my $dll = "$dir/$Config{libperl}"; 398: push @paths, $dll if -f $dll; 399: } 400: my $sum = 0; 401: for my $path (@paths) { 402: open(my $fh, '<:raw', $path) or next; 403: $sum += unpack('%32C*', $_) while read $fh, $_, 65536; 404: } 405: open(my $out, '>>', $ENV{GITHUB_OUTPUT}) or die $!; 406: print $out "version=$Config{version}-$Config{archname}-$sum\n"; 407: 408: VERSION_STEP 409: 410: $yaml .= " - name: Cache CPAN modules\n"; 411: $yaml .= " uses: actions/cache\@v6\n"; 412: $yaml .= " with:\n"; 413: $yaml .= " path: ~/perl5\n"; 414: $yaml .= " key: \${{ runner.os }}-perl-\${{ steps.perl-version.outputs.version }}-\${{ hashFiles('cpanfile') }}\n"; 415: $yaml .= " restore-keys: |\n"; 416: $yaml .= " \${{ runner.os }}-perl-\${{ steps.perl-version.outputs.version }}-\n\n"; 417: 418: $yaml .= " - name: Install cpanm and local::lib\n"; 419: $yaml .= " if: runner.os != 'Windows'\n"; 420: $yaml .= " run: cpanm --notest --local-lib=~/perl5 local::lib\n\n"; 421: 422: $yaml .= " - name: Install cpanm and local::lib (Windows)\n"; 423: $yaml .= " if: runner.os == 'Windows'\n"; 424: $yaml .= " run: cpanm --notest App::cpanminus local::lib\n\n"; 425: 426: # TODO: should be configurable 427: $yaml .= " - name: Install DB_File, needed for a lot of CPAN modules\n"; 428: $yaml .= " if: runner.os == 'Linux'\n"; 429: $yaml .= " run: |\n"; 430: $yaml .= " sudo apt update\n"; 431: $yaml .= " sudo apt install libdb-dev libperl-dev\n"; 432: 433: $yaml .= " - name: Install dependencies\n"; 434: $yaml .= " if: runner.os != 'Windows'\n"; 435: $yaml .= " shell: bash\n"; 436: $yaml .= " run: |\n"; 437: $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; 438: # Add the --verbose to stop the no activity timeout in GitHub actions whene there are a lot of prequisites 439: # TODO: should be configurable 440: $yaml .= " cpanm --notest --installdeps --verbose .\n\n"; 441: 442: $yaml .= " - name: Install dependencies (Windows)\n"; 443: $yaml .= " if: runner.os == 'Windows'\n"; 444: $yaml .= " shell: cmd\n"; 445: $yaml .= " run: |\n"; 446: $yaml .= " \@echo off\n"; 447: $yaml .= " set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n"; 448: $yaml .= " set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n"; 449: $yaml .= " cpanm --notest --installdeps --verbose .\n\n"; 450: 451: # The shogo82148 Perl distributions bundle pre-compiled XS modules (e.g. 452: # YAML::XS) in the Perl zip. cpanm sees them as "already installed" and 453: # skips recompilation, leaving a DLL that may have the wrong handshake key. 454: # Force-reinstall YAML::XS into site/lib (which precedes the bundled lib in 455: # @INC) to ensure a freshly compiled copy is always used. 456: # NOTE: this step cannot rescue a fundamentally broken distribution where 457: # libperl540.a and perl.exe themselves are from different builds (as in the 458: # shogo82148 perl-5.40.4-thr-win32-x64.zip build) â that case is handled 459: # by the matrix exclusion above. 460: $yaml .= " - name: Reinstall YAML::XS against current Perl (Windows)\n"; 461: $yaml .= " if: runner.os == 'Windows'\n"; 462: $yaml .= " shell: cmd\n"; 463: $yaml .= " run: cpanm --notest --reinstall YAML::XS\n\n";
Mutants (Total: 1, Killed: 1, Survived: 0)
464: 465: if ($enable_linter) { 466: $yaml .= " - name: Lint and syntax check\n"; 467: $yaml .= " shell: perl {0}\n"; 468: $yaml .= " run: |\n"; 469: $yaml .= <<'LINT_STEP'; 470: use strict; 471: use warnings; 472: use File::Find; 473: use lib 'lib'; 474: my @failed; 475: push @INC, sub { open my $h, '<', \qq{1;\n}; $h }; 476: my @dirs = grep { -d } qw(lib bin); 477: @dirs = ('.') unless @dirs; 478: find({ wanted => sub { 479: return unless -f && /\.pm$/; 480: my $file = $File::Find::name; 481: do $file; 482: if ($@) { 483: warn "Syntax check failed: $file\n"; 484: push @failed, $file; 485: } 486: }, no_chdir => 1 }, @dirs); 487: 488: LINT_STEP
Mutants (Total: 1, Killed: 1, Survived: 0)
489: 490: if ($enable_linter_unused) { 491: $yaml .= <<'UNUSED_CODE'; 492: if (($ENV{RUNNER_OS} // '') eq 'Linux') { 493: system('cpanm --notest --quiet warnings::unused 2>/dev/null'); 494: system('PERL5OPT=-Mwarnings::unused prove -lr t/ 2>&1 | grep -i unused && true || echo "warnings::unused: no unused variables detected"'); 495: } 496: 497: UNUSED_CODE 498: } 499: 500: $yaml .= " exit(\@failed ? 1 : 0);\n\n"; 501: } 502: ●503 → 519 → 531 503: $yaml .= " - name: Run tests\n"; 504: $yaml .= " if: runner.os != 'Windows'\n"; 505: $yaml .= " shell: bash\n"; 506: $yaml .= " run: |\n"; 507: $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; 508: $yaml .= " prove -lr t/\n\n"; 509: 510: $yaml .= " - name: Run tests (Windows)\n"; 511: $yaml .= " if: runner.os == 'Windows'\n"; 512: $yaml .= " shell: cmd\n"; 513: $yaml .= " run: |\n"; 514: $yaml .= " \@echo off\n"; 515: $yaml .= " set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n"; 516: $yaml .= " set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n"; 517: $yaml .= " prove -lr t/\n\n";
Mutants (Total: 1, Killed: 1, Survived: 0)
518: 519: if ($enable_critic) { 520: my $latest = $perl_versions[-1]; 521: $yaml .= " - name: Run Perl::Critic\n"; 522: $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n"; 523: $yaml .= " continue-on-error: true\n"; 524: $yaml .= " run: |\n"; 525: $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; 526: $yaml .= " cpanm --notest Perl::Critic\n"; 527: $yaml .= " perlcritic --severity 3 lib/ || true\n"; 528: $yaml .= " shell: bash\n\n"; 529: }
Mutants (Total: 1, Killed: 1, Survived: 0)
530: ●531 → 531 → 543 531: if ($enable_perlimports) { 532: my $latest = $perl_versions[-1]; 533: $yaml .= " - name: Check imports with perlimports\n"; 534: $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n"; 535: $yaml .= " continue-on-error: true\n"; 536: $yaml .= " run: |\n"; 537: $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; 538: $yaml .= " cpanm --notest App::perlimports\n"; 539: $yaml .= " find lib -name '*.pm' | xargs perlimports --lint\n"; 540: $yaml .= " shell: bash\n\n"; 541: }
Mutants (Total: 1, Killed: 1, Survived: 0)
542: ●543 → 543 → 556 543: if ($enable_coverage) { 544: my $latest = $perl_versions[-1]; 545: $yaml .= " - name: Test coverage\n"; 546: $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n"; 547: $yaml .= " run: |\n"; 548: $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n"; 549: $yaml .= " cpanm --notest Devel::Cover\n"; 550: $yaml .= " cover -delete\n"; 551: $yaml .= " HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t/\n"; 552: $yaml .= " cover\n"; 553: $yaml .= " shell: bash\n"; 554: } 555: 556: $yaml .= <<'YAML'; 557: 558: - name: Show cpanm build log on failure (Windows) 559: if: runner.os == 'Windows' && failure() 560: shell: pwsh 561: run: Get-Content "$env:USERPROFILE\.cpanm\work\*\build.log" -Tail 100 562: 563: - name: Show cpanm build log on failure (non-Windows) 564: if: runner.os != 'Windows' && failure() 565: run: tail -100 "$HOME/.cpanm/work/*/build.log" 566: YAML
Mutants (Total: 2, Killed: 2, Survived: 0)
567: 568: return $yaml; 569: } 570: 571: sub _get_perl_versions($min, $max) { 572: # All available Perl versions in descending order. 573: # 5.44 is listed so projects can opt in via max_perl_version => '5.44', 574: # but it is not included in the default matrix (max defaults to 5.42). ●575 → 582 → 589 575: my @all_versions = qw(5.44 5.42 5.40 5.38 5.36 5.34 5.32 5.30 5.28 5.26 5.24 5.22); 576: 577: # Normalize version strings for comparison 578: my $min_normalized = _normalize_version($min); 579: my $max_normalized = _normalize_version($max); 580: 581: my @selected; 582: for my $version (@all_versions) {
Mutants (Total: 7, Killed: 7, Survived: 0)
583: my $v_normalized = _normalize_version($version); 584: if ($v_normalized >= $min_normalized && $v_normalized <= $max_normalized) { 585: push @selected, $version; 586: } 587: }
Mutants (Total: 2, Killed: 2, Survived: 0)
588: 589: return reverse @selected; # Return in ascending order 590: } 591: 592: sub _normalize_version($version) { 593: # Convert "5.036" or "5.36" to comparable number 594: $version =~ s/^v?//;
Mutants (Total: 2, Killed: 2, Survived: 0)
595: my @parts = split /\./, $version; 596: return sprintf('%d.%03d', $parts[0] // 5, $parts[1] // 0); 597: } 598: 599: =head1 LIMITATIONS 600: 601: =head2 Windows + Perl 5.40 excluded from the generated matrix 602: 603: The shogo82148 C<perl-5.40.4-thr-win32-x64.zip> distribution ships a 604: C<perl.exe> and a C<libperl540.a> from two different builds. Any XS module 605: compiled against that C<libperl540.a> receives handshake key 606: C<0x12c00080>, but C<perl.exe> needs C<0x12d00080>. Even 607: C<Win32::Process> â a core Windows XS module bundled in the zip and loaded 608: transitively by C<IPC::System::Simple> â fails to load with this mismatch, 609: so no XS-using test can pass on this combination. 610: 611: Recompiling XS modules (e.g. via C<cpanm --reinstall YAML::XS>) does not 612: help: the compilation itself links against the broken C<libperl540.a> and 613: inherits the wrong key. 614: 615: The generated workflow therefore includes a C<matrix.exclude> entry for 616: C<{os: windows-latest, perl: '5.40'}>. This entry is only emitted when 617: both C<windows-latest> and C<5.40> are present in the matrix, so workflows 618: that restrict their OS or Perl lists are unaffected. 619: 620: Remove the exclusion once the upstream shogo82148/build-perl distribution 621: is fixed and the C<perl.exe> and C<libperl540.a> in the 5.40.x zip are 622: built from the same source tree. 623: 624: See L<https://github.com/shogo82148/actions-setup-perl/issues/2310> for further information. 625: 626: =head1 AUTHOR 627: 628: Nigel Horne E<lt>njh@nigelhorne.comE<gt> 629: 630: L<https://github.com/nigelhorne> 631: 632: =head1 LICENSE 633: 634: Usage is subject to the GPL2 licence terms. 635: If you use it, 636: please let me know. 637: 638: =cut 639: 640: 1;