diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 5034976..9837af6 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_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, @@ -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) = @_; @@ -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 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. diff --git a/t/mock-file-checks-guard.t b/t/mock-file-checks-guard.t new file mode 100644 index 0000000..3565598 --- /dev/null +++ b/t/mock-file-checks-guard.t @@ -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;