Skip to content
Draft
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
42 changes: 41 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_file_check_guard mock_stat
mock_all_file_checks mock_file_check mock_file_check_guard mock_file_checks_guard mock_stat
unmock_file_check unmock_all_file_checks unmock_stat
},
@CHECK_STATUS,
Expand Down Expand Up @@ -254,6 +254,24 @@ sub mock_file_check_guard {
return Overload::FileCheck::Guard->new($normalized);
}

sub mock_file_checks_guard {
my (@args) = @_;

Carp::croak(q[mock_file_checks_guard requires an even number of arguments (check => CODE pairs)])
if @args % 2;
Carp::croak(q[mock_file_checks_guard requires at least one check => CODE pair])
unless @args;

my @mocked_checks;
while ( my ( $check, $sub ) = splice( @args, 0, 2 ) ) {
mock_file_check( $check, $sub );
( my $normalized = $check ) =~ s{^-+}{};
push @mocked_checks, $normalized;
}

return Overload::FileCheck::Guard->new(@mocked_checks);
}

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

Expand Down Expand Up @@ -1036,6 +1054,28 @@ by guaranteeing cleanup even if the test dies.

Call C<< $guard->cancel >> to prevent the automatic unmock.

=head2 mock_file_checks_guard( check1 => CODE1, check2 => CODE2, ... )

Mock multiple file checks at once and return a single guard object that
unmocks all of them when it goes out of scope. This is a convenience
wrapper around C<mock_file_check> for the common case where you need to
set up several mocks and want a single cleanup point.

{
my $guard = mock_file_checks_guard(
'-e' => sub { CHECK_IS_TRUE },
'-f' => sub { CHECK_IS_TRUE },
'-d' => sub { CHECK_IS_FALSE },
);
ok( -e "/fake/file", "exists" );
ok( -f "/fake/file", "is a file" );
ok( !-d "/fake/file", "not a directory" );
}
# -e, -f, and -d are all automatically unmocked here

Requires at least one check/CODE pair. Croaks if the number of arguments
is odd or if any check is already mocked.

=head2 unmock_file_check( $check, [@extra_checks] )

Disable the effect of one or more specific mock.
Expand Down
110 changes: 110 additions & 0 deletions t/mock-file-checks-guard.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!perl

use strict;
use warnings;

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

use Overload::FileCheck '-e' => \&my_custom_check, qw(:check);

my $fake_file = '/this/file/does/not/exist.for" ~ testing';

sub my_custom_check {
my ($file) = @_;
return CHECK_IS_TRUE if $file eq $fake_file;
return FALLBACK_TO_REAL_OP;
}

# Sanity: single-check mock from import is active
ok( -e $fake_file, "-e mock from import is active" );

# --- Test: basic multi-check guard ---
{
my $guard = Overload::FileCheck::mock_file_checks_guard(
'-f' => sub {
my ($file) = @_;
return CHECK_IS_TRUE if $file eq $fake_file;
return FALLBACK_TO_REAL_OP;
},
'-d' => sub {
my ($file) = @_;
return CHECK_IS_FALSE if $file eq $fake_file;
return FALLBACK_TO_REAL_OP;
},
);

ok( -f $fake_file, "-f is mocked inside guard scope" );
ok( !-d $fake_file, "-d is mocked inside guard scope" );

# Real files still work via FALLBACK_TO_REAL_OP
ok( -f $0, "real file (-f \$0) still works" );
}

# After guard goes out of scope, mocks are removed
{
# -f and -d should now fall back to real ops (file doesn't exist)
ok( !-f $fake_file, "-f unmocked after guard scope" );
ok( !-d $fake_file, "-d unmocked after guard scope" );
}

# --- Test: guard cancel prevents unmocking ---
{
my $guard = Overload::FileCheck::mock_file_checks_guard(
'-z' => sub { CHECK_IS_TRUE },
);

ok( -z $fake_file, "-z mocked via guard" );
$guard->cancel;
}

# After cancel + scope exit, mock should still be active
ok( -z $fake_file, "-z still mocked after cancel" );

# Clean up manually
Overload::FileCheck::unmock_file_check('-z');

# --- Test: error on odd number of args ---
like(
dies { Overload::FileCheck::mock_file_checks_guard('-f') },
qr/even number of arguments/,
"croaks on odd argument count",
);

# --- Test: error on empty args ---
like(
dies { Overload::FileCheck::mock_file_checks_guard() },
qr/at least one/,
"croaks on zero arguments",
);

# --- Test: error on duplicate mock ---
like(
dies {
Overload::FileCheck::mock_file_checks_guard(
'-e' => sub { CHECK_IS_TRUE },
)
},
qr/already mocked/,
"croaks when check is already mocked",
);

# --- Test: three checks with mixed dash/no-dash ---
{
my $guard = Overload::FileCheck::mock_file_checks_guard(
'-f' => sub { CHECK_IS_TRUE },
'd' => sub { CHECK_IS_TRUE }, # no dash
'-S' => sub { CHECK_IS_FALSE },
);

ok( -f $fake_file, "-f mocked (dash)" );
ok( -d $fake_file, "-d mocked (no dash)" );
ok( !-S $fake_file, "-S mocked (false)" );
}
ok( !-f $fake_file, "-f unmocked after multi-check guard" );
ok( !-d $fake_file, "-d unmocked after multi-check guard" );

# Clean up the import mock
Overload::FileCheck::unmock_file_check('-e');

done_testing;
Loading