DateTime-Format-Genealogy
view release on metacpan or search on metacpan
t/extended_tests.t view on Meta::CPAN
#!perl
# Extended coverage tests for DateTime::Format::Genealogy.
#
# Purpose: hit every remaining branch and condition gap identified by
# Devel::Cover after the core test suite (date.t, error.t, function.t,
# unit.t, integration.t, edge_cases.t) was run.
#
# Coverage gaps targeted (all in lib/DateTime/Format/Genealogy.pm):
#
# Line 486 TRUE - French August "Ao\x{FB}t" branch (0/242 hits)
# Cond 421 !l - date IS a ref at the condition (dead; validate_strict
# catches refs before reaching this line)
# Cond 531 !l - $rc is falsy after success check (dead; success guard
# at line 526 returns early, so $rc is always truthy here)
# Cond 542 !l - date matches /^(Abt|ca?)/ - circa/approximate prefix
# without the whitespace that the earlier /^abt\s/ catches
# Cond 508 !l&&!r - $self->{'dfn'} undef AND DFN::new returns false (dead;
# DFN::new always returns a truthy object)
#
# Dead-code note: the else branch at the original line 548-549 that carped
# "Can't parse date" has been REMOVED from the module because
# DateTime::Format::Natural->parse_datetime never returns a falsy value.
# It now lives only as a comment in the source for future maintainers.
#
# All dead-code conditions are documented below with explicit reasoning.
use strict;
use warnings;
use Test::Most;
use Test::Mockingbird;
use Test::Returns;
use Readonly;
use Scalar::Util qw(blessed);
# Allow direct access to private/protected helpers for white-box assertions.
$Sub::Private::BYPASS = 1;
BEGIN { use_ok('DateTime::Format::Genealogy') || BAIL_OUT('Cannot load module') }
Readonly my $PKG => 'DateTime::Format::Genealogy';
# ---------------------------------------------------------------------------
# Minimal calendar back-end stubs so require() short-circuits.
# ---------------------------------------------------------------------------
{
package DateTime::Calendar::Hebrew;
sub new { my ($c, %a) = @_; bless { %a }, $c }
}
$INC{'DateTime/Calendar/Hebrew.pm'} = 1;
{
package DateTime::Calendar::FrenchRevolutionary;
sub new { my ($c, %a) = @_; bless { %a }, $c }
}
$INC{'DateTime/Calendar/FrenchRevolutionary.pm'} = 1;
# ===========================================================================
# SECTION 1: French August â Ao\x{FB}t (line 486 TRUE branch)
#
# This is the only functional execution path untouched by the entire existing
# test suite. The regex:
# /^(\d{1,2})\s+Ao\x{FB}t\s+(\d{3,4})$/i
# is designed for the French month name "Août" (August), where '\x{FB}' is
# LATIN SMALL LETTER U WITH CIRCUMFLEX (û, U+00FB).
#
# The branch must rewrite "NN Août YYYY" to "NN Aug YYYY" so that
# Genealogy::Gedcom::Date can then parse it. Strict mode never reaches this
# branch because the strict path returns at the 3-letter check above it.
# ===========================================================================
subtest 'parse_datetime - French August Ao\x{FB}t (line 486 true branch)' => sub {
my $obj = $PKG->new();
# Core case: exact-case as the regex expects.
my $dt = $obj->parse_datetime("15 Ao\x{fb}t 2022");
isa_ok($dt, 'DateTime', '"Août" parses to a DateTime');
is($dt->dmy, '15-08-2022', '"Août" correctly mapped to August');
returns_ok($dt, { type => 'object', isa => 'DateTime' }, 'return type validates');
# Case-insensitive match: the /i flag must handle "ao\x{fb}T" etc.
# Perl's /i on \x{FB} matches the uppercase equivalent U+00DB (Ã).
my $dt_uc = $obj->parse_datetime("15 Ao\x{fb}T 2022");
isa_ok($dt_uc, 'DateTime', '"AoûT" (mixed case) parses');
is($dt_uc->dmy, '15-08-2022', 'case-insensitive Août produces correct date') if defined $dt_uc;
# Different valid days in August.
Readonly my %AOUT_CASES => (
"1 Ao\x{fb}t 2020" => '01-08-2020',
"31 Ao\x{fb}t 1789" => '31-08-1789',
"15 Ao\x{fb}t 1800" => '15-08-1800',
);
while (my ($input, $expected) = each %AOUT_CASES) {
my $r = $obj->parse_datetime($input);
ok(defined $r, "Août: '$input' is accepted");
is($r->dmy, $expected, "Août: '$input' => $expected") if defined $r;
}
# Strict mode must reject the non-ASCII month name at the 3-letter check
# (the Août regex is only reached in non-strict mode).
my $strict_result;
warnings_are(
sub { $strict_result = $obj->parse_datetime(date => "15 Ao\x{fb}t 2022", strict => 1, quiet => 1) },
[],
t/extended_tests.t view on Meta::CPAN
is($first_ref, $second_ref,
'DFN instance is reused on subsequent parses (slot initialised once)');
# The !l&&!r dead-code case would require DFN::new to return false.
# Confirm DFN::new always returns a blessed object.
my $dfn = DateTime::Format::Natural->new();
ok(defined $dfn && ref $dfn, 'DFN::new always returns a truthy ref (dead code confirmed)');
diag("DFN ref: " . ref($obj->{'dfn'})) if $ENV{TEST_VERBOSE};
};
# ===========================================================================
# SECTION 4: Dead-code documentation â condition 421 !l
#
# The condition at line 421 is:
# if ((!ref($params->{'date'})) && (my $date = $params->{'date'}))
#
# The !l case (date IS a reference) would short-circuit this if-block and
# fall through to the final Carp::croak. However, Params::Validate::Strict
# (called at line 418) already rejects every reference type with:
# "Invalid parse_datetime parameters: ... must be a scalar"
# before execution reaches line 421.
#
# Therefore the !l path is dead code: a ref date never survives to line 421.
# The test below confirms that all ref types are caught before line 421.
# ===========================================================================
subtest 'Dead code â condition 421 !l: ref date caught before reaching condition' => sub {
my $obj = $PKG->new();
# All of these die with "Invalid parse_datetime parameters" (from
# validate_strict at line 418), NOT with the Usage: croak at line 542
# (which is what would happen if the !l branch at line 421 were reached).
my %REF_CASES = (
arrayref => [],
hashref => {},
scalarref => \"str",
coderef => sub { },
);
while (my ($label, $ref) = each %REF_CASES) {
throws_ok(
sub { $obj->parse_datetime(date => $ref) },
qr/Invalid parse_datetime parameters/,
"$label: caught by validate_strict (line 418), not line 421",
);
}
# If condition 421 !l were ever reachable, the error would be "Usage:"
# not "Invalid parse_datetime parameters". Verify it is NOT "Usage:".
my $err = '';
eval { $obj->parse_datetime(date => []) };
$err = $@ // '';
unlike($err, qr/^Usage:/, 'ref date error is NOT the line-421 usage message');
pass('All ref types are intercepted by validate_strict before line 421');
};
# ===========================================================================
# SECTION 5: Dead-code documentation â condition 531 !l
#
# The condition at line 531 is:
# if ($rc && $calendar_type ne 'DGREGORIAN')
#
# The !l case ($rc is falsy) would fall through without calling
# _convert_calendar. However, the guard added at line 526:
# unless ($dfn->success) { carp...; return }
# ensures that when we reach line 531, $dfn->success() was true and
# $rc holds the DateTime object DFN returned â which is always truthy.
#
# Therefore the !l path (rc is false/undef at line 531) is dead code.
# ===========================================================================
subtest 'Dead code â condition 531 !l: $rc is always truthy when we reach it' => sub {
# Mock DFN to return a truthy object and report success.
# Verify that $rc at line 531 is always a DateTime.
my $sentinel = DateTime->new(year => 2022, month => 12, day => 25);
mock 'DateTime::Format::Natural::parse_datetime' => sub { return $sentinel };
mock 'DateTime::Format::Natural::success' => sub { return 1 };
my $obj = $PKG->new();
my $r = $obj->parse_datetime('25 Dec 2022');
isa_ok($r, 'DateTime', 'When DFN succeeds, $rc is a truthy DateTime at line 531');
is($r->year, 2022, 'correct year from mock'); # sentinel year
restore_all();
# The !l path would only be reached if success() returned true but
# parse_datetime returned a falsy value â a contradiction in DFN's contract.
# Mock that contradiction to prove parse_datetime catches it gracefully via
# the success check (returns before line 531).
mock 'DateTime::Format::Natural::parse_datetime' => sub { return undef };
mock 'DateTime::Format::Natural::success' => sub { return 0 };
mock 'DateTime::Format::Natural::error' => sub { return 'synthetic error' };
my $obj2 = $PKG->new();
my $r2 = $obj2->parse_datetime(date => '25 Dec 2022', quiet => 1);
ok(!defined $r2,
'When DFN returns undef with success=false, the success check returns undef before line 531');
restore_all();
pass('Condition 531 !l is unreachable in normal operation');
};
# ===========================================================================
# SECTION 6: Additional branch coverage for the DFN-fallback path
#
# The DFN-fallback (lines 542-551) is reached when:
# (a) the date does NOT start with a digit (GGD path skipped), AND
# (b) the date does NOT match /^(Abt|ca?)/i, AND
# (c) the date matches /^[\w\s,]+$/
#
# Condition 542 l&&!r case: date does NOT match /^(Abt|ca?)/ but also does
# NOT match /^[\w\s,]+$/ (contains non-word chars other than space/comma).
# ===========================================================================
subtest 'parse_datetime - DFN fallback skipped for non-word chars (cond 542 l&&!r)' => sub {
my $obj = $PKG->new(quiet => 1);
# These strings reach line 542 but fail the /^[\w\s,]+$/ test because
# they contain characters outside the word/space/comma set.
# Result: undef without entering DFN fallback.
Readonly my @NON_WORD_DATES => (
( run in 1.014 second using v1.01-cache-2.11-cpan-364913b4093 )