App-makefilepl2cpanfile
view release on metacpan or search on metacpan
t/edge_cases.t view on Meta::CPAN
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
my $result;
lives_ok { $result = App::makefilepl2cpanfile::parse_prereqs($circular) }
'circular reference does not cause infinite recursion or crash';
isa_ok $result, 'HASH', 'circular ref input returns a hashref';
is scalar keys %{$result}, 0, 'result is empty for circular reference input';
is scalar @warnings, 0, 'no warnings for circular reference input';
};
subtest 'parse_prereqs: invalid UTF-8 bytes in content string â no crash, no warnings' => sub {
# When the caller passes a byte string containing invalid UTF-8 sequences,
# the regex engine must operate on the raw bytes without crashing or warning.
# Byte strings (no UTF-8 flag) are valid Perl scalars; [^{}] matches any byte.
my $content = "PREREQ_PM => { \x27Carp\x27 => 0 };\xFF\xFEjunk";
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
my $result;
lives_ok { $result = App::makefilepl2cpanfile::parse_prereqs($content) }
'invalid UTF-8 bytes in string content do not crash parse_prereqs';
isa_ok $result, 'HASH', 'returns a hashref for content with invalid UTF-8 bytes';
is scalar @warnings, 0, 'no warnings emitted for byte-string content';
diag "Found phases: " . join(', ', keys %{$result}) if $ENV{TEST_VERBOSE};
};
subtest 'generate: Makefile.PL with trailing invalid UTF-8 bytes â warns, finds valid deps' => sub {
# Path::Tiny slurp_utf8 emits Carp warnings for each ill-formed byte but
# does NOT die â it returns the (possibly substituted) content and continues.
# Strategy: write a file whose valid prefix contains a PREREQ_PM block and
# whose suffix contains \xFF\xFE; verify generate() survives and finds Carp.
my $g = empty_home();
my $dir = tempdir(CLEANUP => 1);
my $mf = path($dir)->child('Makefile.PL');
$mf->spew_raw("WriteMakefile(PREREQ_PM => { \x27Carp\x27 => 0 });\xFF\xFE\n");
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
my $out;
lives_ok {
$out = App::makefilepl2cpanfile::generate(
makefile => "$mf",
with_develop => 0,
)
} 'invalid UTF-8 bytes in Makefile.PL do not crash generate()';
like $out, qr/Carp/, 'valid dep before invalid bytes is captured correctly';
ok scalar @warnings > 0,
'invalid UTF-8 bytes trigger warnings from slurp_utf8 (not silent corruption)';
diag "Warning count: " . scalar(@warnings) if $ENV{TEST_VERBOSE};
};
subtest 'parse_prereqs: extreme numerical version strings â _has_version contract' => sub {
# Verify that unusual-but-valid number strings (Inf, NaN, very large ints)
# are handled consistently by the looks_like_number + != 0 logic.
# These are "real constraints" (non-zero) even if nonsensical in practice.
Readonly my @EXTREME_NONZERO_VERS => (
[ 'Inf', 1, 'Inf is looks_like_number and != 0' ],
[ '-Inf', 1, '-Inf is looks_like_number and != 0' ],
[ 'NaN', 1, 'NaN is looks_like_number and != 0 (IEEE 754)'],
[ '99999999999999999999', 1, '20-digit int overflows to ~1e20, != 0' ],
[ '0.000000001', 1, 'tiny positive fraction is non-zero' ],
[ '1e300', 1, 'very large float is non-zero' ],
);
Readonly my @EXTREME_ZERO_VERS => (
[ '0.000', 0, 'leading zeros â numerically zero' ],
[ '+0', 0, 'explicit positive zero is still zero' ],
);
for my $case (@EXTREME_NONZERO_VERS) {
my ($ver, $expected, $label) = @{$case};
is !!App::makefilepl2cpanfile::_has_version($ver), !!$expected, $label;
}
for my $case (@EXTREME_ZERO_VERS) {
my ($ver, $expected, $label) = @{$case};
is !!App::makefilepl2cpanfile::_has_version($ver), !!$expected, $label;
}
};
subtest 'parse_prereqs: 1 MB of noise with embedded PREREQ_PM â completes without crashing' => sub {
# Defensive performance test: a megabyte of random-ish content must not
# cause catastrophic regex backtracking. [^{}] and \{...\} are mutually
# exclusive at each position, so no backtracking occurs.
# Strategy: embed a valid PREREQ_PM block inside 500 KB of ASCII noise on
# either side and verify parse_prereqs finds the module.
Readonly my $HALF_MB => 'abcdefg ' x 70_000; # ~560 KB, no braces
my $content = $HALF_MB
. "PREREQ_PM => { \x27Carp\x27 => 0 },"
. $HALF_MB;
my $result;
lives_ok { $result = App::makefilepl2cpanfile::parse_prereqs($content) }
'1 MB of content does not crash parse_prereqs';
ok exists $result->{runtime}{requires}{'Carp'},
'Carp found inside 1 MB content â regex scans without hanging';
};
subtest 'parse_prereqs: 100-level brace nesting completes within time limit' => sub {
# Verify no catastrophic backtracking when brace nesting far exceeds the
# 4-level regex limit. Uses alarm() to enforce a hard wall-clock ceiling.
SKIP: {
skip 'alarm() is unreliable on this platform', 3 if $^O eq 'MSWin32';
Readonly my $ALARM_SECS => 5;
my $timed_out = 0;
local $SIG{ALRM} = sub { $timed_out = 1; die "TIMEOUT\n" };
alarm($ALARM_SECS);
my $content = 'PREREQ_PM => '
. ('{' x 100)
. "\x27Module\x27 => 0" # 'Module' => 0 as raw bytes
. ('}' x 100);
my $result;
lives_ok { $result = App::makefilepl2cpanfile::parse_prereqs($content) }
'100-level brace nesting does not crash or time out';
alarm(0);
ok !$timed_out, "completed within ${ALARM_SECS}s (no catastrophic backtracking)";
isa_ok $result, 'HASH', 'returns a hashref even for extreme brace nesting';
}
};
subtest 'parse_prereqs: version as unevaluated Perl expression â security invariant' => sub {
# A module version written as a Perl expression (e.g. 9**9**9) in the
# source of Makefile.PL must NEVER be evaluated. The regex captures only
# the leading digit sequence; 9**9**9 becomes version '9', not Inf or error.
my $content = "PREREQ_PM => { \x27Module\x27 => 9**9**9 },";
my $result;
lives_ok { $result = App::makefilepl2cpanfile::parse_prereqs($content) }
'Perl expression as version does not crash or eval';
my $ver = $result->{runtime}{requires}{'Module'}{version} // 'undef';
ok defined $ver, 'version field is defined (regex captured leading digits)';
unlike "$ver", qr/Inf|NaN/i, 'captured version is not Inf/NaN (no eval occurred)';
ok $ver =~ /^\d/, 'captured version starts with a digit â just the literal text';
diag "Captured version for 9**9**9: $ver" if $ENV{TEST_VERBOSE};
};
subtest 'generate: reference as existing arg â no crash, no spurious develop merge' => sub {
# A reference passed as existing stringifies silently to e.g. "ARRAY(0x...)"
# which the develop-block regex cannot match. No warnings are emitted and
# no develop block should appear in the output.
my $g = empty_home();
my $mf = make_mf($MF_SIMPLE);
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
my $out;
lives_ok {
$out = App::makefilepl2cpanfile::generate(
makefile => "$mf",
existing => [], # array ref, not a string
with_develop => 0,
)
} 'array ref as existing does not crash';
is scalar @warnings, 0, 'no warnings for reference existing arg';
unlike $out, qr/on 'develop' => sub/,
'no spurious develop block from reference existing';
like $out, qr/Carp/, 'normal dep output produced';
};
# -----------------------------------------------------------------------
# SECTION 6: Filesystem Hostility
# -----------------------------------------------------------------------
subtest 'generate: unreadable Makefile.PL (mode 000) must croak' => sub {
# Strategy: create a file, remove all permissions, then verify the
# -r guard fires. Root bypasses permissions, so skip under euid 0.
SKIP: {
skip 'running as root â filesystem permissions are not enforced', 1
if $> == 0;
my $g = empty_home();
my $dir = tempdir(CLEANUP => 1);
my $mf = path($dir)->child('Makefile.PL');
$mf->spew_utf8($MF_SIMPLE);
chmod 0000, "$mf";
throws_ok {
App::makefilepl2cpanfile::generate(makefile => "$mf")
} qr/Cannot read/, 'mode-000 Makefile.PL causes croak';
chmod 0644, "$mf"; # restore so temp-cleanup can remove it
}
};
subtest 'generate: dangling symlink must croak' => sub {
# A symlink whose target does not exist: -f returns false (target absent),
# so the Cannot-read guard must fire.
my $g = empty_home();
my $dir = tempdir(CLEANUP => 1);
( run in 2.375 seconds using v1.01-cache-2.11-cpan-e86d8f7595a )