Test-MockFile
view release on metacpan or search on metacpan
lib/Test/MockFile.pm view on Meta::CPAN
no strict 'refs';
my $caller_pkg = caller(1);
$handle = *{"$caller_pkg\::$args[1]"};
}
# Check that the upgrading worked
ref \$handle eq 'GLOB'
or return @args;
# Set to bareword
$args[0] = 1;
# Override original handle variable/string
$args[1] = $handle;
return @args;
}
=head2 authorized_strict_mode_for_package( $pkg )
Add a package namespace to the list of authorize namespaces.
authorized_strict_mode_for_package( 'Your::Package' );
=cut
our %authorized_strict_mode_packages;
sub authorized_strict_mode_for_package {
my ($pkg) = @_;
$authorized_strict_mode_packages{$pkg} = 1;
return;
}
BEGIN {
authorized_strict_mode_for_package($_) for qw{ DynaLoader lib };
}
=head2 file_arg_position_for_command
Args: ($command)
Provides a hint with the position of the argument most likely holding
the file name for the current C<$command> call.
This is used internaly to provide better error messages. This can be
used when plugging hooks to know what's the filename we currently try
to access.
=cut
my $_file_arg_post;
sub file_arg_position_for_command { # can also be used by user hooks
my ( $command, $at_under_ref ) = @_;
$_file_arg_post //= {
'chmod' => 1,
'chown' => 2,
'lstat' => 0,
'mkdir' => 0,
'open' => 2,
'opendir' => 1,
'link' => 0,
'readlink' => 0,
'rename' => 0,
'rmdir' => 0,
'stat' => 0,
'symlink' => 1,
'sysopen' => 1,
'truncate' => 0,
'unlink' => 0,
'utime' => 2,
'readdir' => 0,
};
return -1 unless defined $command && defined $_file_arg_post->{$command};
# exception for open
return 1 if $command eq 'open' && ref $at_under_ref && scalar @$at_under_ref == 2;
return $_file_arg_post->{$command};
}
use constant _STACK_ITERATION_MAX => 100;
sub _get_stack {
my @stack;
foreach my $stack_level ( 1 .. _STACK_ITERATION_MAX ) {
@stack = caller($stack_level);
last if !scalar @stack;
last if !defined $stack[0]; # We don't know when this would ever happen.
next if $stack[0] eq __PACKAGE__;
next if $stack[0] eq 'Overload::FileCheck'; # companion package
return if $authorized_strict_mode_packages{ $stack[0] };
last;
}
return @stack;
}
=head2 add_strict_rule( $command_rule, $file_rule, $action )
Args: ($command_rule, $file_rule, $action)
Add a custom rule to validate strictness mode. This is the fundation to
add strict rules. You should use it, when none of the other helper to
add rules work for you.
=over
=item C<$command_rule> a string or regexp or list of any to indicate
which command to match
=item C<$file_rule> a string or regexp or undef or list of any to indicate
lib/Test/MockFile.pm view on Meta::CPAN
}
return;
}
# Install a sub into a package, replicating the delete-glob trick used by
# autodie/Fatal.pm's install_subs. Simple glob assignment (*pkg::func = \&sub)
# does not override builtins when autodie has already installed its wrapper â
# the glob entry must be deleted and recreated for Perl to pick up the new sub.
sub _install_sub {
my ( $pkg, $name, $ref ) = @_;
no strict 'refs';
no warnings qw(redefine once);
my $full_name = "${pkg}::${name}";
my $pkg_sym = "${pkg}::";
my $old_glob = *$full_name;
# Delete the stash entry so Perl re-resolves the symbol.
delete $pkg_sym->{$name};
# Restore non-CODE slots (SCALAR, ARRAY, HASH, IO) from the old glob
# so we don't clobber unrelated data in the same symbol.
local *alias = *$full_name;
foreach my $slot (qw( SCALAR ARRAY HASH IO )) {
next unless defined( *$old_glob{$slot} );
*alias = *$old_glob{$slot};
}
*$full_name = $ref;
}
# Install goto-transparent wrappers into the caller's package namespace.
# These use goto to preserve @_ aliasing and caller() transparency.
# Uses the delete-glob technique so that Perl properly picks up our
# overrides even when autodie/Fatal.pm has already installed wrappers.
sub _install_package_overrides {
my ($caller) = @_;
return if $caller eq __PACKAGE__;
return if $caller eq 'Test::MockFile::FileHandle';
return if $caller eq 'Test::MockFile::DirHandle';
push @_tmf_callers, $caller
unless grep { $_ eq $caller } @_tmf_callers;
my %subs = (
'open' => sub (*;$@) { goto \&__open },
'sysopen' => sub (*$$;$) { goto \&__sysopen },
'opendir' => sub (*$) { goto \&__opendir },
'readdir' => sub (*) { goto \&__readdir },
'telldir' => sub (*) { goto \&__telldir },
'rewinddir' => sub (*) { goto \&__rewinddir },
'seekdir' => sub (*$) { goto \&__seekdir },
'closedir' => sub (*) { goto \&__closedir },
'unlink' => sub (@) { goto \&__unlink },
'readlink' => sub (_) { goto \&__readlink },
'mkdir' => sub (_;$) { goto \&__mkdir },
'rmdir' => sub (_) { goto \&__rmdir },
'chown' => sub (@) { goto \&__chown },
'chmod' => sub (@) { goto \&__chmod },
'rename' => sub ($$) { goto \&__rename },
'link' => sub ($$) { goto \&__link },
'symlink' => sub ($$) { goto \&__symlink },
'truncate' => sub ($$) { goto \&__truncate },
'flock' => sub (*$) { goto \&__flock },
'utime' => sub (@) { goto \&__utime },
);
_install_sub( $caller, $_, $subs{$_} ) for keys %subs;
}
# Check if autodie is active for a given function in the caller's scope.
# autodie stores its state in the lexical hints hash (%^H),
# accessible via (caller($depth))[10]. The keys vary by version.
sub _caller_has_autodie_for {
my ($func) = @_;
return unless $INC{'autodie.pm'} || $INC{'Fatal.pm'};
# Primary: walk the caller stack for lexical hints set by autodie.
for my $depth ( 1 .. 10 ) {
my @c = caller($depth);
last unless @c;
my $hints = $c[10];
next unless ref $hints eq 'HASH';
return 1
if $hints->{'autodie'}
|| $hints->{"Fatal::$func"}
|| $hints->{"autodie::$func"};
}
# Fallback: check if the calling package had autodie active at import
# time. On some Perl versions, caller(N)[10] hints may not propagate
# reliably through goto &sub. This is less precise (doesn't respect
# "no autodie" sub-scopes) but catches the common case.
my $caller_pkg = caller(1);
return $_autodie_callers{$caller_pkg} if $caller_pkg;
return;
}
# Check-and-throw for autodie: combines _caller_has_autodie_for + _throw_autodie
# into a single call to reduce boilerplate at every error return site.
sub _maybe_throw_autodie {
my ($func, @args) = @_;
_throw_autodie($func, @args) if _caller_has_autodie_for($func);
}
# Throw an autodie-compatible exception for a failed CORE function.
# Creates a real autodie::exception if available, otherwise a plain die.
# $! must be saved before the eval since eval can clobber it.
sub _throw_autodie {
my ($func, @args) = @_;
my $saved_errno = int($!);
my $saved_errstr = "$!";
if ( eval { require autodie::exception; 1 } ) {
local $! = $saved_errno;
die autodie::exception->new(
function => "CORE::$func",
args => \@args,
lib/Test/MockFile.pm view on Meta::CPAN
# Overload::FileCheck should always send something and be handling undef on its own??
if ( !defined $file_or_fh || !length $file_or_fh ) {
_real_file_access_hook( $type, [$file_or_fh] );
return FALLBACK_TO_REAL_OP();
}
# Find the path, following the symlink if required.
my $file = _find_file_or_fh( $file_or_fh, $follow_link );
# Broken symlink: target doesn't exist â ENOENT
if ( defined $file && defined BROKEN_SYMLINK && $file eq BROKEN_SYMLINK ) {
$! = ENOENT;
return 0;
}
# Circular symlink: too many levels of indirection â ELOOP
if ( defined $file && defined CIRCULAR_SYMLINK && $file eq CIRCULAR_SYMLINK ) {
$! = ELOOP;
return 0;
}
if ( !defined $file or !length $file ) {
_real_file_access_hook( $type, [$file_or_fh] );
return FALLBACK_TO_REAL_OP();
}
my $file_data = _get_file_object($file);
if ( !$file_data ) {
$file_data = _maybe_autovivify($file);
}
if ( !$file_data ) {
_real_file_access_hook( $type, [$file_or_fh] ) unless ref $file_or_fh;
return FALLBACK_TO_REAL_OP();
}
# File is not present so no stats for you!
if ( !$file_data->exists() ) {
$! = ENOENT;
return 0;
}
# Make sure the file size is correct in the stats before returning its contents.
return [ $file_data->stat ];
}
sub _is_path_mocked {
my ($file_path) = @_;
my $absolute_path_to_file = _find_file_or_fh($file_path) or return;
return $files_being_mocked{$absolute_path_to_file} ? 1 : 0;
}
sub _get_file_object {
my ($file_path) = @_;
my $file = _find_file_or_fh($file_path) or return;
return $files_being_mocked{$file};
}
# Like _get_file_object but follows symlinks (for chmod, chown, utime, truncate).
# Returns BROKEN_SYMLINK or CIRCULAR_SYMLINK sentinels on symlink errors,
# the mock object on success, or undef if not mocked.
sub _get_file_object_follow_link {
my ($file_path) = @_;
my $resolved = _find_file_or_fh( $file_path, 1 ); # follow symlinks
# Propagate symlink error sentinels
return $resolved if ref $resolved && ( $resolved == BROKEN_SYMLINK || $resolved == CIRCULAR_SYMLINK );
return unless $resolved;
return $files_being_mocked{$resolved};
}
# Creates a file mock at the target of a broken symlink chain.
# Used when open/sysopen with a create-capable mode needs to create the target.
# The new mock is attached to the last symlink in the chain (which holds the
# strong ref) so it stays alive as long as the symlink mock does.
# Returns the absolute path of the newly created mock, or undef on failure.
sub _create_file_through_broken_symlink {
my ($path) = @_;
my $abs = _abs_path_to_file($path);
return unless defined $abs;
my $depth = 0;
my $last_link_abs;
while ( my $mock = $files_being_mocked{$abs} ) {
return unless $mock->is_link; # Not a symlink â nothing to resolve
$last_link_abs = $abs;
my $target = $mock->readlink;
return unless defined $target && length $target;
$abs = _abs_path_to_file($target);
return unless defined $abs;
return if ++$depth > FOLLOW_LINK_MAX_DEPTH; # Circular â give up
last unless $files_being_mocked{$abs}; # Found the broken end
}
return unless $last_link_abs; # Original path wasn't a symlink
# If autovivify can handle it, prefer that path
my $mock = _maybe_autovivify($abs);
return $abs if $mock;
# Create a non-existent file mock at the target path
$mock = _new_nonexistent_file_mock($abs);
$files_being_mocked{$abs} = $mock;
Scalar::Util::weaken( $files_being_mocked{$abs} );
# The last symlink in the chain holds the strong ref
my $symlink_mock = $files_being_mocked{$last_link_abs};
$symlink_mock->{'_autovivified_children'} //= [];
push @{ $symlink_mock->{'_autovivified_children'} }, $mock;
return $abs;
}
# This subroutine finds the absolute path to a file, returning the absolute path of what it ultimately points to.
# If it is a broken link or what was passed in is undef or '', then we return undef.
lib/Test/MockFile.pm view on Meta::CPAN
# Transfer autovivify settings from old dir to new dir
if ( $mock_old->{'autovivify'} ) {
$mock_new->{'autovivify'} = delete $mock_old->{'autovivify'};
delete $_autovivify_dirs{ $mock_old->{'path'} };
$_autovivify_dirs{ $mock_new->{'path'} } = $mock_new;
Scalar::Util::weaken( $_autovivify_dirs{ $mock_new->{'path'} } );
}
# Transfer ownership of autovivified children
if ( $mock_old->{'_autovivified_children'} ) {
$mock_new->{'_autovivified_children'} = delete $mock_old->{'_autovivified_children'};
}
# Re-key all children from old path prefix to new path prefix
# in %files_being_mocked (and %_autovivify_dirs if applicable).
# This ensures files under the renamed directory remain accessible.
my $old_prefix = $mock_old->{'path'};
my $new_prefix = $mock_new->{'path'};
for my $key ( grep { m{^\Q$old_prefix/\E} } keys %files_being_mocked ) {
my $child = $files_being_mocked{$key};
( my $new_key = $key ) =~ s{^\Q$old_prefix/\E}{$new_prefix/};
delete $files_being_mocked{$key};
$files_being_mocked{$new_key} = $child;
$child->{'path'} = $new_key;
# Update autovivify tracking for child directories
if ( $_autovivify_dirs{$key} ) {
$_autovivify_dirs{$new_key} = delete $_autovivify_dirs{$key};
}
}
}
else {
delete $mock_new->{'readlink'};
delete $mock_new->{'has_content'};
$mock_new->{'contents'} = $mock_old->{'contents'};
$mock_old->{'contents'} = undef;
}
# Copy mode, ownership, and inode metadata
$mock_new->{'mode'} = $mock_old->{'mode'};
$mock_new->{'uid'} = $mock_old->{'uid'};
$mock_new->{'gid'} = $mock_old->{'gid'};
$mock_new->{'inode'} = $mock_old->{'inode'};
$mock_new->{'nlink'} = $mock_old->{'nlink'};
$mock_new->{'mtime'} = $mock_old->{'mtime'};
$mock_new->{'atime'} = $mock_old->{'atime'};
# rename updates ctime on both source and destination
my $now = time;
$mock_new->{'ctime'} = $now;
$mock_old->{'ctime'} = $now;
# Update parent directory timestamps (old dir loses entry, new dir gains entry)
_update_parent_dir_times($old);
_update_parent_dir_times($new);
return 1;
}
sub __chown (@) {
my ( $uid, $gid, @files ) = @_;
$^O eq 'MSWin32'
and return 0; # does nothing on Windows
# Not an error, report we changed zero files
@files
or return 0;
# Follow symlinks: chown operates on the target, not the symlink itself
my %mocked_files = map +( $_ => _get_file_object_follow_link($_) ), @files;
my @unmocked_files = grep !$mocked_files{$_}, @files;
my @mocked_files = map { ref $_ && ref $_ ne 'A::BROKEN::SYMLINK' && ref $_ ne 'A::CIRCULAR::SYMLINK' ? $_->{'path'} : () } values %mocked_files;
# The idea is that if some are mocked and some are not,
# it's probably a mistake. Broken/circular symlinks are mocked paths
# (handled per-file below), so they don't count as unmocked.
if ( @mocked_files && @unmocked_files ) {
confess(
sprintf 'You called chown() on a mix of mocked (%s) and unmocked files (%s) ' . ' - this is very likely a bug on your side',
( join ', ', @mocked_files ),
( join ', ', @unmocked_files ),
);
}
# Permission check uses the actual target uid/gid (not -1).
# Use mock user identity if set, otherwise real process credentials (GH #3)
my $eff_uid = defined $_mock_uid ? $_mock_uid : $>;
my $eff_gids = defined $_mock_uid ? join( ' ', @_mock_gids ) : $);
# -1 means "keep as is" and is handled per-file below.
my $target_uid = $uid == -1 ? $eff_uid : $uid;
my ($primary_gid) = split /\s/, $eff_gids;
my $target_gid = $gid == -1 ? $primary_gid : $gid;
my $is_root = $eff_uid == 0 || $eff_gids =~ /( ^ | \s ) 0 ( \s | $)/xms;
my $is_in_group = grep /(^ | \s ) \Q$target_gid\E ( \s | $ )/xms, $eff_gids;
# Only check permissions once (before the loop), not per-file.
# -1 means "keep as is" â no permission needed for unchanged fields.
# POSIX: non-root cannot change uid; can only change gid to a group they belong to.
if ( !$is_root ) {
if ( $uid != -1 && $eff_uid != $target_uid ) {
$! = EPERM;
_maybe_throw_autodie( 'chown', @_ );
return 0;
}
if ( $gid != -1 && !$is_in_group ) {
$! = EPERM;
_maybe_throw_autodie( 'chown', @_ );
return 0;
}
}
my $num_changed = 0;
foreach my $file (@files) {
my $mock = $mocked_files{$file};
# If this file is not mocked, none of the files are
# which means we can send them all and let the CORE function handle it
if ( !$mock ) {
_real_file_access_hook( 'chown', \@_ );
goto \&CORE::chown if _goto_is_available();
return CORE::chown( $uid, $gid, @files );
}
# Handle broken/circular symlink errors
if ( ref $mock eq 'A::BROKEN::SYMLINK' ) {
$! = ENOENT;
next;
}
if ( ref $mock eq 'A::CIRCULAR::SYMLINK' ) {
$! = ELOOP;
next;
}
# Even if you're root, nonexistent file is nonexistent
if ( !$mock->exists() ) {
$! = ENOENT;
next;
}
# -1 means "keep as is" â preserve the file's current value
$mock->{'uid'} = $uid == -1 ? $mock->{'uid'} : $uid;
$mock->{'gid'} = $gid == -1 ? $mock->{'gid'} : $gid;
$mock->{'ctime'} = time;
$num_changed++;
}
if ( $num_changed < scalar(@files) ) {
_maybe_throw_autodie( 'chown', @_ );
}
return $num_changed;
}
sub __chmod (@) {
my ( $mode, @files ) = @_;
# Not an error, report we changed zero files
@files
or return 0;
# Grab numbers - nothing means "0" (which is the behavior of CORE::chmod)
# (This will issue a warning, that's also the expected behavior)
{
no warnings;
$mode =~ /^[0-9]+/xms
or warn "Argument \"$mode\" isn't numeric in chmod";
$mode = int $mode;
}
# Follow symlinks: chmod operates on the target, not the symlink itself
my %mocked_files = map +( $_ => _get_file_object_follow_link($_) ), @files;
my @unmocked_files = grep !$mocked_files{$_}, @files;
my @mocked_files = map { ref $_ && ref $_ ne 'A::BROKEN::SYMLINK' && ref $_ ne 'A::CIRCULAR::SYMLINK' ? $_->{'path'} : () } values %mocked_files;
# The idea is that if some are mocked and some are not,
# it's probably a mistake. Broken/circular symlinks are mocked paths
# (handled per-file below), so they don't count as unmocked.
if ( @mocked_files && @unmocked_files ) {
confess(
sprintf 'You called chmod() on a mix of mocked (%s) and unmocked files (%s) ' . ' - this is very likely a bug on your side',
( join ', ', @mocked_files ),
( join ', ', @unmocked_files ),
);
}
my $num_changed = 0;
foreach my $file (@files) {
my $mock = $mocked_files{$file};
if ( !$mock ) {
_real_file_access_hook( 'chmod', \@_ );
goto \&CORE::chmod if _goto_is_available();
return CORE::chmod( $mode, @files );
}
# Handle broken/circular symlink errors
if ( ref $mock eq 'A::BROKEN::SYMLINK' ) {
$! = ENOENT;
next;
}
if ( ref $mock eq 'A::CIRCULAR::SYMLINK' ) {
$! = ELOOP;
next;
}
# chmod is less specific in such errors
# chmod $mode, '/foo/' still yields ENOENT
if ( !$mock->exists() ) {
lib/Test/MockFile.pm view on Meta::CPAN
if ( ref $file_or_fh ) {
my $tied = tied( *{$file_or_fh} );
if ( $tied && !$tied->{'write'} ) {
$! = EINVAL;
_maybe_throw_autodie( 'truncate', @_ );
return 0;
}
}
if ( $length < 0 ) {
$! = EINVAL;
_maybe_throw_autodie( 'truncate', @_ );
return 0;
}
my $contents = $mock->contents() // '';
my $cur_len = length $contents;
if ( $length < $cur_len ) {
$contents = substr( $contents, 0, $length );
}
elsif ( $length > $cur_len ) {
$contents .= "\0" x ( $length - $cur_len );
}
$mock->contents($contents);
# POSIX truncate(2): marks mtime and ctime for update
my $now = time;
$mock->{'mtime'} = $now;
$mock->{'ctime'} = $now;
return 1;
}
BEGIN {
no warnings 'redefine';
*CORE::GLOBAL::glob = !$^V || $^V lt 5.18.0
? sub {
pop;
goto &__glob;
}
: sub (_;) { goto &__glob; };
*CORE::GLOBAL::open = \&__open;
*CORE::GLOBAL::sysopen = \&__sysopen;
*CORE::GLOBAL::opendir = \&__opendir;
*CORE::GLOBAL::readdir = \&__readdir;
*CORE::GLOBAL::telldir = \&__telldir;
*CORE::GLOBAL::rewinddir = \&__rewinddir;
*CORE::GLOBAL::seekdir = \&__seekdir;
*CORE::GLOBAL::closedir = \&__closedir;
*CORE::GLOBAL::unlink = \&__unlink;
*CORE::GLOBAL::readlink = \&__readlink;
*CORE::GLOBAL::symlink = \&__symlink;
*CORE::GLOBAL::link = \&__link;
*CORE::GLOBAL::mkdir = \&__mkdir;
*CORE::GLOBAL::rename = \&__rename;
*CORE::GLOBAL::rmdir = \&__rmdir;
*CORE::GLOBAL::chown = \&__chown;
*CORE::GLOBAL::chmod = \&__chmod;
*CORE::GLOBAL::flock = \&__flock;
*CORE::GLOBAL::utime = \&__utime;
*CORE::GLOBAL::truncate = \&__truncate;
# Override Cwd functions to resolve mocked symlinks (GH #139)
$_original_cwd_abs_path = \&Cwd::abs_path;
{
no warnings 'redefine';
*Cwd::abs_path = \&__cwd_abs_path;
*Cwd::realpath = \&__cwd_abs_path;
*Cwd::fast_abs_path = \&__cwd_abs_path;
*Cwd::fast_realpath = \&__cwd_abs_path;
}
# Override IO::File::open to intercept mocked files.
# IO::File uses CORE::open internally which bypasses CORE::GLOBAL::open.
$_orig_io_file_open = \&IO::File::open;
{
no warnings 'redefine';
*IO::File::open = \&_io_file_open_override;
}
}
=head1 CAVEATS AND LIMITATIONS
=head2 DEBUGGER UNDER STRICT MODE
If you want to use the Perl debugger (L<perldebug>) on any code that
uses L<Test::MockFile> in strict mode, you will need to load
L<Term::ReadLine> beforehand, because it loads a file. Under the
debugger, the debugger will load the module after L<Test::MockFile> and
get mad.
# Load it from the command line
perl -MTerm::ReadLine -d code.pl
# Or alternatively, add this to the top of your code:
use Term::ReadLine
=head2 HARD LINKS
The C<link()> override copies file contents and metadata from the
source to the destination mock. However, unlike real hard links,
writes to one file will B<not> be reflected in the other. The
C<nlink> count is incremented on both files.
The destination path must be a pre-declared mock (via C<file()> or
C<dir()>). Attempting to C<link()> a mocked source to an unmocked
destination will fail with C<EXDEV>.
=head2 FILENO IS UNSUPPORTED
Filehandles can provide the file descriptor (in number) using the
C<fileno> keyword but this is purposefully unsupported in
L<Test::MockFile>.
The reason is that by mocking a file, we're creating an alternative
file system. Returning a C<fileno> (file descriptor number) would
require creating file descriptor numbers that would possibly conflict
( run in 1.093 second using v1.01-cache-2.11-cpan-5511b514fd6 )