diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 295e03f..54d1307 100644 --- a/lib/Overload/FileCheck.pm +++ b/lib/Overload/FileCheck.pm @@ -63,7 +63,7 @@ my @STAT_HELPERS = qw{ stat_as_directory stat_as_file stat_as_symlink our @EXPORT_OK = ( qw{ mock_all_from_stat - mock_all_file_checks mock_file_check mock_stat + mock_all_file_checks mock_file_check mock_file_check_guard mock_stat unmock_file_check unmock_all_file_checks unmock_stat }, @CHECK_STATUS, @@ -240,6 +240,16 @@ sub mock_file_check { return 1; } +sub mock_file_check_guard { + my ( $check, $sub ) = @_; + + mock_file_check( $check, $sub ); + + ( my $normalized = $check ) =~ s{^-+}{}; + + return Overload::FileCheck::Guard->new($normalized); +} + sub unmock_file_check { my (@checks) = @_; @@ -683,6 +693,36 @@ sub _stat_for { return \@stat; } +###################################################### +### Scope guard for automatic mock cleanup +###################################################### + +package Overload::FileCheck::Guard; + +sub new { + my ( $class, @checks ) = @_; + + return bless { checks => \@checks, active => 1 }, $class; +} + +sub cancel { + my ($self) = @_; + + $self->{active} = 0; + return; +} + +sub DESTROY { + my ($self) = @_; + + return unless $self->{active}; + $self->{active} = 0; + + local $@; + eval { Overload::FileCheck::unmock_file_check( @{ $self->{checks} } ) }; + return; +} + 1; =pod @@ -974,6 +1014,21 @@ Otherwise returns 1 on success. # in that sample all '-e' checks will always return true... mock_file_check( '-e' => sub { 1 } ) +=head2 mock_file_check_guard( $check, CODE ) + +Like C, but returns a guard object instead of C<1>. +When the guard goes out of scope (or is otherwise destroyed), the mock is +automatically removed via C. This improves test isolation +by guaranteeing cleanup even if the test dies. + + { + my $guard = mock_file_check_guard( '-e' => sub { CHECK_IS_TRUE } ); + ok( -e "/fake/file", "mocked" ); + } + # -e is automatically unmocked here + +Call C<< $guard->cancel >> to prevent the automatic unmock. + =head2 unmock_file_check( $check, [@extra_checks] ) Disable the effect of one or more specific mock. diff --git a/t/guard.t b/t/guard.t new file mode 100644 index 0000000..e3275eb --- /dev/null +++ b/t/guard.t @@ -0,0 +1,65 @@ +use strict; +use warnings; + +use Test2::Bundle::Extended; +use Test2::Tools::Explain; + +use Overload::FileCheck qw( + mock_file_check_guard mock_file_check unmock_file_check + CHECK_IS_TRUE CHECK_IS_FALSE FALLBACK_TO_REAL_OP +); + +my $fake = "/guard/test/file"; + +# --- basic guard: mock is active inside scope, removed after --- +{ + my $guard = mock_file_check_guard( '-e' => sub { CHECK_IS_TRUE } ); + isa_ok( $guard, 'Overload::FileCheck::Guard' ); + ok( -e $fake, "mocked -e returns true inside guard scope" ); +} +ok( !-e $fake, "-e falls back to real op after guard is destroyed" ); + +# --- guard with cancel: mock persists after scope --- +{ + my $guard = mock_file_check_guard( '-f' => sub { CHECK_IS_TRUE } ); + ok( -f $fake, "mocked -f returns true" ); + $guard->cancel; +} +# mock still active because we cancelled the guard +ok( -f $fake, "-f still mocked after cancelled guard" ); +unmock_file_check('-f'); # manual cleanup +ok( !-f $fake, "-f unmocked manually" ); + +# --- guard handles double-destroy gracefully --- +{ + my $guard = mock_file_check_guard( '-d' => sub { CHECK_IS_TRUE } ); + ok( -d $fake, "mocked -d" ); + # explicitly destroy, then let scope destroy again + $guard->DESTROY; + ok( !-d $fake, "-d unmocked after explicit DESTROY" ); +} +# second DESTROY from scope exit should not die +pass("double DESTROY did not die"); + +# --- guard unmocks even if test dies (eval) --- +eval { + my $guard = mock_file_check_guard( '-e' => sub { CHECK_IS_TRUE } ); + ok( -e $fake, "mocked -e inside eval" ); + die "simulated test failure"; +}; +ok( !-e $fake, "-e unmocked after die inside eval" ); + +# --- guard works with dash-less check names --- +{ + my $guard = mock_file_check_guard( 'e' => sub { CHECK_IS_TRUE } ); + ok( -e $fake, "mocked with dash-less 'e'" ); +} +ok( !-e $fake, "unmocked after dash-less guard" ); + +# --- guard with FALLBACK_TO_REAL_OP --- +{ + my $guard = mock_file_check_guard( '-e' => sub { FALLBACK_TO_REAL_OP } ); + ok( !-e $fake, "FALLBACK_TO_REAL_OP falls through to real check" ); +} + +done_testing;