Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions FileCheck.xs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ OverloadFTOps *gl_overload_ft = 0;
*
* 1 check is true -> OP returns Yes
* 0 check is false -> OP returns No
* -2 check is null -> OP returns undef (CHECK_IS_NULL)
* -1 fallback to the original OP
*/
int _overload_ft_ops() {
Expand Down Expand Up @@ -93,7 +94,13 @@ int _overload_ft_ops() {
if (count != 1)
croak("No return value from Overload::FileCheck::_check for OP #%d\n", optype);

check_status = POPi;
{
SV *result_sv = POPs;
if (!SvOK(result_sv))
check_status = -2; /* undef => CHECK_IS_NULL */
else
check_status = SvIV(result_sv);
}

OFC_DEBUG("_overload_ft_ops: result=%d optype=%d\n", check_status, optype);

Expand Down Expand Up @@ -258,8 +265,9 @@ PP(pp_overload_ft_yes_no) {
{
FT_SETUP_dSP_IF_NEEDED;

if ( check_status == 1 ) FT_RETURNYES;
if ( check_status == 0 ) FT_RETURNUNDEF;
if ( check_status == 1 ) FT_RETURNYES;
if ( check_status == 0 ) FT_RETURNUNDEF;
if ( check_status == -2 ) FT_RETURNUNDEF; /* CHECK_IS_NULL */
}

/* fallback */
Expand All @@ -281,6 +289,11 @@ PP(pp_overload_ft_int) {
if ( check_status == -1 )
return CALL_REAL_OP();

if ( check_status == -2 ) { /* CHECK_IS_NULL */
FT_SETUP_dSP_IF_NEEDED;
FT_RETURNUNDEF;
}

/* Save errno — sv_setiv() and FT_RETURN_TARG can trigger allocations
* or other Perl internals that clobber errno. */
saved_errno = errno;
Expand All @@ -307,6 +320,12 @@ PP(pp_overload_ft_nv) {

status = _overload_ft_ops_sv();

if ( !SvOK(status) ) { /* CHECK_IS_NULL — undef */
SvREFCNT_dec(status);
FT_SETUP_dSP_IF_NEEDED;
FT_RETURNUNDEF;
}

/* Save errno — sv_setnv()/sv_setiv() and FT_RETURN_TARG can trigger
* allocations or other Perl internals that clobber errno. */
saved_errno = errno;
Expand Down
15 changes: 12 additions & 3 deletions lib/Overload/FileCheck.pm
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ my @STAT_T_IX = qw{
ST_BLOCKS
};

my @CHECK_STATUS = qw{CHECK_IS_FALSE CHECK_IS_TRUE FALLBACK_TO_REAL_OP};
my @CHECK_STATUS = qw{CHECK_IS_FALSE CHECK_IS_TRUE CHECK_IS_NULL FALLBACK_TO_REAL_OP};

my @STAT_HELPERS = qw{ stat_as_directory stat_as_file stat_as_symlink
stat_as_socket stat_as_chr stat_as_block};
Expand Down Expand Up @@ -425,7 +425,7 @@ sub unmock_stat {

sub unmock_all_file_checks {

my @mocks = map { $REVERSE_MAP{$_} } keys %$_current_mocks;
my @mocks = sort map { $REVERSE_MAP{$_} } keys %$_current_mocks;
return unless scalar @mocks;

return unmock_file_check(@mocks);
Expand Down Expand Up @@ -454,7 +454,16 @@ sub _check {
# causing resource leaks (e.g. sockets staying open). See GH #179.
$_last_call_for = ref($file) ? undef : $file;

if ( defined $out && $OP_CAN_RETURN_INT{$optype} ) {
if ( !defined $out ) {
# CHECK_IS_NULL: callback returned undef — propagate as undef
# so the OP returns undef to the caller (file not found / unknown)
if ( !int($!) ) {
$! = $DEFAULT_ERRNO{ $REVERSE_MAP{$optype} || 'default' } || $DEFAULT_ERRNO{'default'};
}
return CHECK_IS_NULL;
}

if ( $OP_CAN_RETURN_INT{$optype} ) {
return $out;
}

Expand Down
206 changes: 206 additions & 0 deletions t/check-null.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/perl -w

# Tests for CHECK_IS_NULL handling.
# When a mock callback returns undef (CHECK_IS_NULL), the file-test OP
# should return undef to the caller, distinct from CHECK_IS_FALSE (0).

use strict;
use warnings;

use Test2::Bundle::Extended;
use Test2::Tools::Explain;
use Test2::Plugin::NoWarnings;

use Overload::FileCheck q(:all);

# ---------------------------------------------------------------
# Boolean ops (-e, -f, -d, …) — CHECK_IS_NULL should yield undef
# ---------------------------------------------------------------

{
note "boolean op: -e with CHECK_IS_NULL";

my $mock_return;

mock_file_check(
'-e' => sub { return $mock_return },
);

$mock_return = CHECK_IS_TRUE;
is( -e "/null-test", 1, "-e CHECK_IS_TRUE returns 1" );

$mock_return = CHECK_IS_FALSE;
ok( !defined( -e "/null-test" ), "-e CHECK_IS_FALSE returns undef (existing behaviour)" );

$mock_return = CHECK_IS_NULL; # undef
ok( !defined( -e "/null-test" ), "-e CHECK_IS_NULL returns undef" );

# Ensure it is really undef, not 0 or ''
$mock_return = CHECK_IS_NULL;
my $result = -e "/null-test";
is( $result, undef, "-e CHECK_IS_NULL is literally undef" );

unmock_all_file_checks();
}

{
note "boolean op: -f with CHECK_IS_NULL";

mock_file_check(
'-f' => sub { return CHECK_IS_NULL },
);

my $result = -f "/null-test-f";
is( $result, undef, "-f CHECK_IS_NULL returns undef" );

unmock_all_file_checks();
}

{
note "boolean op: -d with CHECK_IS_NULL";

mock_file_check(
'-d' => sub { return CHECK_IS_NULL },
);

my $result = -d "/null-test-d";
is( $result, undef, "-d CHECK_IS_NULL returns undef" );

unmock_all_file_checks();
}

# ---------------------------------------------------------------
# Integer op (-s) — CHECK_IS_NULL should yield undef, not 0
# ---------------------------------------------------------------

{
note "integer op: -s with CHECK_IS_NULL";

my $mock_return;

mock_file_check(
'-s' => sub { return $mock_return },
);

$mock_return = 42;
{
my $result = -s "/null-test-s";
is( $result, 42, "-s returns the mocked size" );
}

$mock_return = 0;
{
my $result = -s "/null-test-s";
is( $result, 0, "-s returns 0 when size is 0" );
}

$mock_return = CHECK_IS_NULL; # undef
my $result = -s "/null-test-s";
is( $result, undef, "-s CHECK_IS_NULL returns undef (not 0)" );

unmock_all_file_checks();
}

# ---------------------------------------------------------------
# NV ops (-M, -A, -C) — CHECK_IS_NULL should yield undef
# ---------------------------------------------------------------

{
note "NV op: -M with CHECK_IS_NULL";

my $mock_return;

mock_file_check(
'-M' => sub { return $mock_return },
);

$mock_return = 1.5;
ok( defined( -M "/null-test-M" ), "-M returns a defined value for 1.5" );

$mock_return = CHECK_IS_NULL; # undef
my $result = -M "/null-test-M";
is( $result, undef, "-M CHECK_IS_NULL returns undef" );

unmock_all_file_checks();
}

{
note "NV op: -A with CHECK_IS_NULL";

mock_file_check(
'-A' => sub { return CHECK_IS_NULL },
);

my $result = -A "/null-test-A";
is( $result, undef, "-A CHECK_IS_NULL returns undef" );

unmock_all_file_checks();
}

{
note "NV op: -C with CHECK_IS_NULL";

mock_file_check(
'-C' => sub { return CHECK_IS_NULL },
);

my $result = -C "/null-test-C";
is( $result, undef, "-C CHECK_IS_NULL returns undef" );

unmock_all_file_checks();
}

# ---------------------------------------------------------------
# errno is set when CHECK_IS_NULL is returned
# ---------------------------------------------------------------

{
note "errno handling with CHECK_IS_NULL";
local $! = 0;

mock_file_check(
'-e' => sub { return CHECK_IS_NULL },
);

my $result = -e "/null-errno-test";
ok( !defined $result, "result is undef" );
ok( int($!) > 0, "errno is set when CHECK_IS_NULL is returned" );

unmock_all_file_checks();
}

# ---------------------------------------------------------------
# FALLBACK_TO_REAL_OP still works alongside CHECK_IS_NULL
# ---------------------------------------------------------------

{
note "FALLBACK_TO_REAL_OP alongside CHECK_IS_NULL";

mock_file_check(
'-e' => sub {
my $f = shift;
return CHECK_IS_NULL if $f eq '/check-null-missing';
return FALLBACK_TO_REAL_OP;
},
);

my $null_result = -e '/check-null-missing';
is( $null_result, undef, "CHECK_IS_NULL path returns undef" );

my $real_result = -e $0;
is( $real_result, 1, "FALLBACK_TO_REAL_OP path works for existing file" );

unmock_all_file_checks();
}

# ---------------------------------------------------------------
# CHECK_IS_NULL constant value
# ---------------------------------------------------------------

{
note "CHECK_IS_NULL constant";

ok( !defined CHECK_IS_NULL, "CHECK_IS_NULL is undef" );
}

done_testing;
Loading