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 )