DateTime-Format-Genealogy

 view release on metacpan or  search on metacpan

t/edge_cases.t  view on Meta::CPAN

	my $long_result;
	lives_ok(
		sub { $long_result = $obj->parse_datetime("\@#D${long_type}\@ 25 Dec 2022") },
		'Extremely long calendar type does not crash',
	);
	diag("Long calendar type result: " . (defined $long_result ? $long_result->dmy : 'undef'))
		if $ENV{TEST_VERBOSE};
};

subtest 'parse_datetime - lowercase GEDCOM escape is not recognised' => sub {
	# The regex @#D([A-Z ]+?)@ requires uppercase letters; lowercase should
	# not be treated as a calendar escape and falls through to normal parsing.
	my $obj = $PKG->new(quiet => 1);

	my $result = $obj->parse_datetime('@#djulian@ 25 Dec 2022');
	ok(!defined $result,
		'Lowercase GEDCOM escape not recognised: date cannot be parsed normally');
};

subtest 'parse_datetime - GEDCOM Julian offset for each century tier' => sub {
	# Verify all four offset tiers (<1700=>10, <1800=>11, <1900=>12, >=1900=>13)
	# are applied correctly end-to-end.
	my $obj = $PKG->new();

	Readonly my @TIER_CASES => (
		# [ julian_date,      expected_day, expected_offset ]
		['@#DJULIAN@ 1 Mar 1620',  11, 10],
		['@#DJULIAN@ 1 Mar 1750',  12, 11],
		['@#DJULIAN@ 1 Mar 1850',  13, 12],
		['@#DJULIAN@ 1 Mar 1920',  14, 13],
	);

	for my $case (@TIER_CASES) {
		my ($input, $expected_day, $offset) = @{$case};
		my $dt = $obj->parse_datetime($input);
		isa_ok($dt, 'DateTime', "$input parses to DateTime");
		is($dt->day, $expected_day, "$input: day advanced by $offset") if defined $dt;
	}
};

# ===========================================================================
# SECTION 7: Date range edge cases and list/scalar context abuse
# ===========================================================================

subtest 'parse_datetime - range in scalar context always returns undef' => sub {
	my $obj = $PKG->new();

	# These must NOT return a DateTime or throw; scalar context must give undef.
	Readonly my @RANGE_STRINGS => (
		'bet 1 Jan 2000 and 31 Dec 2000',
		'from 1 Jan 2000 to 31 Dec 2000',
		'1 Jan 2000 - 31 Dec 2000',
	);

	for my $range (@RANGE_STRINGS) {
		my $scalar = $obj->parse_datetime($range);
		ok(!defined $scalar, "'$range' in scalar context returns undef");
	}
};

subtest 'parse_datetime - range with invalid first endpoint' => sub {
	# When one endpoint of a bet...and... range is itself invalid (31 Nov),
	# the recursive parse_datetime call returns the empty list.  In list
	# context that causes the outer return to have fewer than 2 elements.
	# This is a known limitation: callers must check scalar(@result) == 2.
	my $obj = $PKG->new(quiet => 1);

	my @range;
	lives_ok(
		sub { @range = $obj->parse_datetime('bet 31 Nov 2000 and 31 Dec 2000') },
		'bet range with invalid first endpoint does not crash',
	);
	# Verify the caller gets back something that is at least not a full 2-DT list.
	ok(scalar(@range) != 2 || !defined $range[0],
		'invalid first endpoint: result is not a clean 2-element range');

	diag("Range with bad first endpoint: " . scalar(@range) . " elements")
		if $ENV{TEST_VERBOSE};
};

subtest 'parse_datetime - deeply nested bet...and... does not crash' => sub {
	# "bet bet A and B and C" is nonsensical but must not crash or loop forever.
	my $obj = $PKG->new(quiet => 1);

	my @result;
	lives_ok(
		sub { @result = $obj->parse_datetime('bet bet 1 Jan 2000 and 31 Dec 2000 and 1 Jan 2001') },
		'nested bet...and... does not crash',
	);
	diag("nested bet result: " . scalar(@result) . " elements") if $ENV{TEST_VERBOSE};
};

subtest 'parse_datetime - from...to... rejected by strict flag' => sub {
	my $obj = $PKG->new(strict => 1, quiet => 1);

	my @result = $obj->parse_datetime('from 1 Jan 2000 to 31 Dec 2000');
	is(scalar @result, 0, 'from...to... returns empty list when strict is set');
};

# ===========================================================================
# SECTION 8: Extremely long and malformed strings
# ===========================================================================

subtest 'parse_datetime - extremely long date strings do not crash or hang' => sub {
	my $obj = $PKG->new(quiet => 1);

	Readonly my $LONG_GARBAGE => 'X' x 10_000;
	Readonly my $LONG_DASH    => ('A' x 100) . ' - ' . ('B' x 100);
	Readonly my $LONG_BET     => 'bet ' . ('A ' x 500) . 'and ' . ('B ' x 500);

	my $t0 = [gettimeofday];
	lives_ok(sub { $obj->parse_datetime($LONG_GARBAGE) }, '10k garbage string: no crash');
	lives_ok(sub { $obj->parse_datetime($LONG_DASH)    }, 'long dash-range string: no crash');
	lives_ok(sub { $obj->parse_datetime($LONG_BET)     }, 'long bet...and... string: no crash');
	my $elapsed = tv_interval($t0);

	# Pathological regex backtracking would make this take many seconds.
	# Allow 30 s to accommodate slow CI / Windows runners while still catching
	# catastrophic backtracking (which would take minutes, not seconds).
	cmp_ok($elapsed, '<', 30, 'all long-string tests complete within 30 seconds');

	diag(sprintf('Long-string subtests took %.3fs', $elapsed)) if $ENV{TEST_VERBOSE};
};

subtest 'parse_datetime - shell-injection strings are inert' => sub {
	# The module must not execute shell commands.  These strings must return
	# undef without doing anything dangerous.
	my $obj = $PKG->new(quiet => 1);

	Readonly my @SHELL_STRINGS => (
		'25 Dec 2022; rm -rf /',
		'25 Dec 202`date`',
		'25 Dec 202$(cat /etc/passwd)',
		"25 Dec 2022\nrm -rf /",
		'25 Dec 202|cat /etc/passwd',
		'25 Dec 202 && id',
	);



( run in 3.687 seconds using v1.01-cache-2.11-cpan-9789f410c06 )