diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 6238a51..0514f6f 100644 --- a/lib/Overload/FileCheck.pm +++ b/lib/Overload/FileCheck.pm @@ -448,11 +448,17 @@ sub _check { && !defined $_current_mocks->{ $MAP_FC_OP{'stat'} } ) { $file = $_last_call_for; } + + # Save $_last_call_for before callback dispatch so that re-entrant + # calls (e.g. mock_all_from_stat callbacks invoking mocked file tests) + # cannot corrupt the outer call's filename context. See GH #68. + my $saved_last_call_for = $_last_call_for; my ( $out, @extra ) = $_current_mocks->{$optype}->($file); - # Only cache string filenames, not filehandle references. - # Storing a ref here prevents the filehandle from being garbage collected, - # causing resource leaks (e.g. sockets staying open). See GH #179. - $_last_call_for = ref($file) ? undef : $file; + # Cache string filenames for stacked -X _ ops. When the file is a + # reference (filehandle), restore the pre-callback value instead of + # clobbering with undef — an inner re-entrant call may have set a + # valid filename that a subsequent stacked op needs. See GH #179. + $_last_call_for = ref($file) ? $saved_last_call_for : $file; if ( defined $out && $OP_CAN_RETURN_INT{$optype} ) { return $out; diff --git a/t/reentrant-check.t b/t/reentrant-check.t new file mode 100644 index 0000000..40982c6 --- /dev/null +++ b/t/reentrant-check.t @@ -0,0 +1,108 @@ +#!/usr/bin/perl + +# Test that _check() is re-entrant safe. +# When a mock callback triggers another mocked file test, the outer +# call's $_last_call_for must not be corrupted. See GH #68. + +use strict; +use warnings; + +use Test2::Bundle::Extended; +use Test2::Tools::Explain; +use Test2::Plugin::NoWarnings; + +use Overload::FileCheck q/:all/; + +# Track which filenames each mock callback receives. +my @ftis_files; # -e callback +my @ftfile_files; # -f callback +my @ftdir_files; # -d callback + +# --- Test 1: Inner call does not corrupt outer stacked-op filename --- + +mock_file_check( + '-e' => sub { + my ($file) = @_; + push @ftis_files, $file; + return CHECK_IS_TRUE; + } +); + +mock_file_check( + '-f' => sub { + my ($file) = @_; + push @ftfile_files, $file; + + # Re-entrant call: trigger another mocked file test inside + # the callback. This will call _check() recursively. + my $inner = -e "/inner/file"; + + return CHECK_IS_TRUE; + } +); + +mock_file_check( + '-d' => sub { + my ($file) = @_; + push @ftdir_files, $file; + return CHECK_IS_TRUE; + } +); + +# Run the sequence: -f on outer, which triggers -e on inner inside the +# callback, then -d _ (stacked) should still see the outer filename. +@ftis_files = (); +@ftfile_files = (); +@ftdir_files = (); + +ok( -f "/outer/file", "-f /outer/file" ); +ok( -d _, "-d _ (stacked after -f)" ); + +is \@ftfile_files, ["/outer/file"], "-f callback received /outer/file"; +is \@ftis_files, ["/inner/file"], "-e callback received /inner/file (re-entrant)"; +is \@ftdir_files, ["/outer/file"], "-d _ received /outer/file (not corrupted by re-entrant call)"; + +unmock_all_file_checks(); + +# --- Test 2: mock_all_from_stat with re-entrant file test --- + +my @stat_files; +my $reentrant_result; + +mock_all_from_stat( + sub { + my ( $stat_or_lstat, $file ) = @_; + push @stat_files, $file; + + # The mock_all_from_stat callback may itself trigger a + # separate file test in complex scenarios. Simulate by + # checking if the file is /trigger — if so, do a nested -e. + if ( defined $file && $file eq "/trigger" ) { + $reentrant_result = -e "/nested"; + } + + return stat_as_file( size => 42 ); + } +); + +@stat_files = (); +$reentrant_result = undef; + +# -e "/trigger" will call the mock, which will re-enter via -e "/nested" +ok( -e "/trigger", "-e /trigger (triggers re-entrant call)" ); + +# After -e "/trigger", a stacked -s _ should see "/trigger", not "/nested" +is( -s _, 42, "-s _ returns the stat size from /trigger context" ); + +# Verify the re-entrant call happened +ok( defined $reentrant_result, "re-entrant -e /nested was called" ); + +# Verify the order of stat calls: /trigger first, then /nested from +# re-entrancy. The stacked -s _ reuses the cached stat buffer from +# the /trigger call so it does not trigger a new callback. +is \@stat_files, ["/trigger", "/nested"], + "stat callback order: /trigger, /nested (re-entrant)"; + +unmock_all_file_checks(); + +done_testing;