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
81 changes: 79 additions & 2 deletions lib/Overload/FileCheck.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) = @_;

Expand Down Expand Up @@ -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<mock_stat>, but returns a guard object. When the guard goes out of
scope, both C<stat> and C<lstat> 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<mock_all_file_checks>, 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<mock_all_from_stat>, 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.
Expand Down
184 changes: 184 additions & 0 deletions t/guard-variants.t
Original file line number Diff line number Diff line change
@@ -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;
Loading