diff --git a/FileCheck.xs b/FileCheck.xs index d9d38d3..e23c242 100644 --- a/FileCheck.xs +++ b/FileCheck.xs @@ -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() { @@ -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); @@ -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 */ @@ -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; @@ -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; diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 6238a51..219dcc3 100644 --- a/lib/Overload/FileCheck.pm +++ b/lib/Overload/FileCheck.pm @@ -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}; @@ -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); @@ -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; } diff --git a/t/check-null.t b/t/check-null.t new file mode 100644 index 0000000..b692655 --- /dev/null +++ b/t/check-null.t @@ -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;