Skip to content
Merged
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
15 changes: 12 additions & 3 deletions lib/Test/MockFile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1036,8 +1036,14 @@ sub _mock_stat {
# Find the path, following the symlink if required.
my $file = _find_file_or_fh( $file_or_fh, $follow_link );

return [] if defined $file && defined BROKEN_SYMLINK && $file eq BROKEN_SYMLINK; # Allow an ELOOP to fall through here.
return [] if defined $file && defined CIRCULAR_SYMLINK && $file eq CIRCULAR_SYMLINK; # Allow an ELOOP to fall through here.
if ( defined $file && defined BROKEN_SYMLINK && $file eq BROKEN_SYMLINK ) {
$! = ELOOP;
return 0;
}
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] );
Expand All @@ -1051,7 +1057,10 @@ sub _mock_stat {
}

# File is not present so no stats for you!
return [] if !$file_data->is_link && !defined $file_data->contents();
if ( !$file_data->is_link && !defined $file_data->contents() ) {
$! = ENOENT;
return 0;
}

# Make sure the file size is correct in the stats before returning its contents.
return [ $file_data->stat ];
Expand Down
106 changes: 106 additions & 0 deletions t/enoent_on_nonexistent.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/perl -w

# Regression test for Overload::FileCheck GH#13
# When a file is mocked as non-existent (undef contents),
# -e should set $! to ENOENT, not EBADF.

use strict;
use warnings;

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

use Errno qw/ENOENT EBADF/;

use Test::MockFile;

subtest '-e on non-existent mock sets ENOENT' => sub {
my $path = '/some/nonexistent/path';
my $mock = Test::MockFile->file( $path, undef );

$! = 0;
my $exists = -e $path;

ok( !$exists, '-e returns false for file mocked with undef contents' );
is( $! + 0, ENOENT, '$! is ENOENT (not EBADF) after -e on non-existent mock' );
};

subtest '-e on non-existent mock (no content arg)' => sub {
my $path = '/another/missing/file';
my $mock = Test::MockFile->file($path);

$! = 0;
my $exists = -e $path;

ok( !$exists, '-e returns false for file mocked without content' );
is( $! + 0, ENOENT, '$! is ENOENT after -e on file mocked without content' );
};

subtest '-f on non-existent mock sets ENOENT' => sub {
my $path = '/mock/not/a/file';
my $mock = Test::MockFile->file( $path, undef );

$! = 0;
my $is_file = -f $path;

ok( !$is_file, '-f returns false for file mocked with undef contents' );
is( $! + 0, ENOENT, '$! is ENOENT after -f on non-existent mock' );
};

subtest '-d on non-existent mock sets ENOENT' => sub {
my $path = '/mock/not/a/dir';
my $mock = Test::MockFile->dir($path);

$! = 0;
my $is_dir = -d $path;

ok( !$is_dir, '-d returns false for non-existent dir mock' );
is( $! + 0, ENOENT, '$! is ENOENT after -d on non-existent dir mock' );
};

subtest 'stat on non-existent mock fails cleanly' => sub {
my $path = '/mock/no/stat';
my $mock = Test::MockFile->file( $path, undef );

$! = 0;
my @st = stat($path);

is( scalar @st, 0, 'stat returns empty list for non-existent mock' );

# errno after stat() on non-existent mock depends on Overload::FileCheck's XS
# cleanup behavior. The _check() Perl code sets ENOENT correctly, but the XS
# FREETMPS/LEAVE in _overload_ft_stat() may clobber errno before returning.
# File checks (-e, -f, etc.) are unaffected because _check_from_stat checks
# array length rather than errno. See cpanel/Overload-FileCheck for the fix.
todo 'Overload::FileCheck XS clobbers errno on stat failure path' => sub {
is( $! + 0, ENOENT, '$! is ENOENT after stat on non-existent mock' );
};
};

subtest 'lstat on non-existent mock fails cleanly' => sub {
my $path = '/mock/no/lstat';
my $mock = Test::MockFile->file( $path, undef );

$! = 0;
my @st = lstat($path);

is( scalar @st, 0, 'lstat returns empty list for non-existent mock' );

todo 'Overload::FileCheck XS clobbers errno on stat failure path' => sub {
is( $! + 0, ENOENT, '$! is ENOENT after lstat on non-existent mock' );
};
};

subtest '-e succeeds for existing mock' => sub {
my $path = '/mock/exists';
my $mock = Test::MockFile->file( $path, 'content' );

$! = 0;
my $exists = -e $path;

ok( $exists, '-e returns true for file mocked with content' );
is( $! + 0, 0, '$! is not set after successful -e' );
};

done_testing();
9 changes: 5 additions & 4 deletions t/mock_stat.t
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use Test2::Plugin::NoWarnings;

use Test::MockFile qw< nostrict >;
use Overload::FileCheck qw/:check/;
use Errno qw/ELOOP/;
use Errno qw/ELOOP ENOENT/;

use Cwd ();

Expand Down Expand Up @@ -97,11 +97,12 @@ my $basic_stat_return = array {
};

is( Test::MockFile::_mock_stat( 'lstat', '/foo/bar' ), $basic_stat_return, "/foo/bar mock stat" );
is( Test::MockFile::_mock_stat( 'stat', '/aaa' ), [], "/aaa mock stat when looped." );
is( Test::MockFile::_mock_stat( 'stat', '/aaa' ), 0, "/aaa mock stat when looped." );
is( $! + 0, ELOOP, "Throws an ELOOP error" );

push @mocked_files, Test::MockFile->file('/foo/baz'); # Missing file but mocked.
is( Test::MockFile::_mock_stat( 'lstat', '/foo/baz' ), [], "/foo/baz mock stat when missing." );
is( Test::MockFile::_mock_stat( 'lstat', '/foo/baz' ), 0, "/foo/baz mock stat when missing." );
is( $! + 0, ENOENT, "Throws an ENOENT error for missing file" );

my $symlink_lstat_return = array {
item 0;
Expand All @@ -120,7 +121,7 @@ my $symlink_lstat_return = array {
};

is( Test::MockFile::_mock_stat( 'lstat', '/broken_link' ), $symlink_lstat_return, "lstat on /broken_link returns the stat on the symlink itself." );
is( Test::MockFile::_mock_stat( 'stat', '/broken_link' ), [], "stat on /broken_link is an empty array since what it points to doesn't exist." );
is( Test::MockFile::_mock_stat( 'stat', '/broken_link' ), 0, "stat on /broken_link returns 0 since what it points to doesn't exist." );

{
my $exe = q[/tmp/custom.exe];
Expand Down