diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 5034976..693a623 100644 --- a/lib/Overload/FileCheck.pm +++ b/lib/Overload/FileCheck.pm @@ -62,8 +62,10 @@ 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_from_stat mock_all_from_stat_guard + mock_all_file_checks mock_all_file_checks_guard + mock_file_check mock_file_check_guard + mock_stat mock_stat_guard unmock_file_check unmock_all_file_checks unmock_stat }, @CHECK_STATUS, @@ -254,6 +256,35 @@ sub mock_file_check_guard { return Overload::FileCheck::Guard->new($normalized); } +sub mock_stat_guard { + my ($sub) = @_; + + mock_stat($sub); + + return Overload::FileCheck::Guard->new( 'stat', 'lstat' ); +} + +sub mock_all_file_checks_guard { + my ($sub) = @_; + + mock_all_file_checks($sub); + + my @checks = sort grep { $_ ne 'stat' && $_ ne 'lstat' } + map { $REVERSE_MAP{$_} } keys %$_current_mocks; + + return Overload::FileCheck::Guard->new(@checks); +} + +sub mock_all_from_stat_guard { + my ($sub_for_stat) = @_; + + mock_all_from_stat($sub_for_stat); + + my @checks = sort map { $REVERSE_MAP{$_} } keys %$_current_mocks; + + return Overload::FileCheck::Guard->new(@checks); +} + sub unmock_file_check { my (@checks) = @_; @@ -1036,6 +1067,52 @@ by guaranteeing cleanup even if the test dies. Call C<< $guard->cancel >> to prevent the automatic unmock. +=head2 mock_stat_guard( CODE ) + +Like C, but returns a guard object. When the guard goes out of +scope, both C and C mocks are automatically removed. + + { + my $guard = mock_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file() if $file eq '/fake'; + return FALLBACK_TO_REAL_OP; + }); + my @st = stat('/fake'); # mocked + } + # stat and lstat are automatically unmocked here + +=head2 mock_all_file_checks_guard( CODE ) + +Like C, but returns a guard object. When the guard +goes out of scope, all file check mocks are automatically removed. + + { + my $guard = mock_all_file_checks_guard( sub { + my ( $check, $file ) = @_; + return CHECK_IS_TRUE if $file eq '/fake'; + return FALLBACK_TO_REAL_OP; + }); + ok( -e '/fake' ); # mocked + } + # all file checks are automatically unmocked here + +=head2 mock_all_from_stat_guard( CODE ) + +Like C, but returns a guard object. When the guard +goes out of scope, all mocks (file checks, stat, and lstat) are +automatically removed. + + { + my $guard = mock_all_from_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file( size => 42 ) if $file eq '/fake'; + return FALLBACK_TO_REAL_OP; + }); + is( -s '/fake', 42 ); # mocked + } + # everything is automatically unmocked here + =head2 unmock_file_check( $check, [@extra_checks] ) Disable the effect of one or more specific mock. diff --git a/t/guard-variants.t b/t/guard-variants.t new file mode 100644 index 0000000..148c778 --- /dev/null +++ b/t/guard-variants.t @@ -0,0 +1,184 @@ +use strict; +use warnings; + +use Test2::Bundle::Extended; +use Test2::Tools::Explain; + +use Overload::FileCheck qw( + mock_stat_guard mock_all_file_checks_guard mock_all_from_stat_guard + mock_file_check unmock_file_check + stat_as_file stat_as_directory + CHECK_IS_TRUE CHECK_IS_FALSE FALLBACK_TO_REAL_OP + ST_SIZE +); + +my $fake = "/guard-variants/test/file"; + +# =========================================================== +# mock_stat_guard +# =========================================================== + +subtest 'mock_stat_guard: active inside scope, removed after' => sub { + my @got; + { + my $guard = mock_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file( size => 99 ) if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + isa_ok( $guard, 'Overload::FileCheck::Guard' ); + @got = stat($fake); + is( $got[ST_SIZE], 99, "stat returns mocked size inside guard scope" ); + } + @got = stat($fake); + ok( !@got, "stat falls back to real op after guard destroyed" ); +}; + +subtest 'mock_stat_guard: cancel preserves mock' => sub { + { + my $guard = mock_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file() if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + $guard->cancel; + } + my @got = stat($fake); + ok( scalar @got, "stat still mocked after cancelled guard" ); + Overload::FileCheck::unmock_stat(); +}; + +subtest 'mock_stat_guard: cleanup on die' => sub { + eval { + my $guard = mock_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file() if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + die "simulated failure"; + }; + my @got = stat($fake); + ok( !@got, "stat unmocked after die inside eval" ); +}; + +# =========================================================== +# mock_all_file_checks_guard +# =========================================================== + +subtest 'mock_all_file_checks_guard: active inside scope' => sub { + { + my $guard = mock_all_file_checks_guard( sub { + my ( $check, $file ) = @_; + return CHECK_IS_TRUE if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + isa_ok( $guard, 'Overload::FileCheck::Guard' ); + ok( -e $fake, "-e mocked" ); + ok( -f $fake, "-f mocked" ); + ok( -d $fake, "-d mocked" ); + } + ok( !-e $fake, "-e unmocked after guard destroyed" ); + ok( !-f $fake, "-f unmocked after guard destroyed" ); + ok( !-d $fake, "-d unmocked after guard destroyed" ); +}; + +subtest 'mock_all_file_checks_guard: cleanup on die' => sub { + eval { + my $guard = mock_all_file_checks_guard( sub { + my ( $check, $file ) = @_; + return CHECK_IS_TRUE if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + ok( -e $fake, "-e mocked inside eval" ); + die "simulated failure"; + }; + ok( !-e $fake, "-e unmocked after die" ); +}; + +# =========================================================== +# mock_all_from_stat_guard +# =========================================================== + +subtest 'mock_all_from_stat_guard: active inside scope' => sub { + { + my $guard = mock_all_from_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file( size => 42 ) if $file eq $fake; + return stat_as_directory() if $file eq "${fake}.dir"; + return FALLBACK_TO_REAL_OP; + }); + isa_ok( $guard, 'Overload::FileCheck::Guard' ); + + # file checks work + ok( -e $fake, "-e mocked" ); + ok( -f $fake, "-f mocked" ); + is( -s $fake, 42, "-s returns mocked size" ); + + # directory works + ok( -d "${fake}.dir", "-d mocked for directory" ); + + # stat works + my @st = stat($fake); + is( $st[ST_SIZE], 42, "stat returns mocked size" ); + } + ok( !-e $fake, "-e unmocked after guard destroyed" ); + my @st = stat($fake); + ok( !@st, "stat unmocked after guard destroyed" ); +}; + +subtest 'mock_all_from_stat_guard: cancel preserves all mocks' => sub { + { + my $guard = mock_all_from_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file() if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + $guard->cancel; + } + ok( -e $fake, "-e still mocked after cancelled guard" ); + my @st = stat($fake); + ok( scalar @st, "stat still mocked after cancelled guard" ); + Overload::FileCheck::unmock_all_file_checks(); +}; + +subtest 'mock_all_from_stat_guard: cleanup on die' => sub { + eval { + my $guard = mock_all_from_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file() if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + ok( -e $fake, "-e mocked inside eval" ); + die "simulated failure"; + }; + ok( !-e $fake, "-e unmocked after die" ); + my @st = stat($fake); + ok( !@st, "stat unmocked after die" ); +}; + +# =========================================================== +# edge case: sequential guards in same scope +# =========================================================== + +subtest 'sequential guards: second guard after first expires' => sub { + { + my $guard1 = mock_all_from_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file( size => 10 ) if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + is( -s $fake, 10, "first guard active" ); + } + ok( !-e $fake, "first guard cleaned up" ); + { + my $guard2 = mock_all_from_stat_guard( sub { + my ( $op, $file ) = @_; + return stat_as_file( size => 20 ) if $file eq $fake; + return FALLBACK_TO_REAL_OP; + }); + is( -s $fake, 20, "second guard active with different value" ); + } + ok( !-e $fake, "second guard cleaned up" ); +}; + +done_testing;