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
57 changes: 56 additions & 1 deletion lib/Overload/FileCheck.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -240,6 +240,16 @@ sub mock_file_check {
return 1;
}

sub mock_file_check_guard {
Comment thread
atoomic marked this conversation as resolved.
my ( $check, $sub ) = @_;

mock_file_check( $check, $sub );

( my $normalized = $check ) =~ s{^-+}{};

return Overload::FileCheck::Guard->new($normalized);
}

sub unmock_file_check {
my (@checks) = @_;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<mock_file_check>, 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<unmock_file_check>. 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.
Expand Down
65 changes: 65 additions & 0 deletions t/guard.t
Original file line number Diff line number Diff line change
@@ -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;
Loading