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
6 changes: 5 additions & 1 deletion lib/Test/MockFile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,9 @@ sub _abs_path_to_file {

return unless defined $path;

# Protect caller's $! — internal calls (getcwd, getpwent) may clobber errno
local $!;

# Tilde expansion must happen before making the path absolute
# ~
# ~/...
Expand Down Expand Up @@ -1985,7 +1988,8 @@ sub __cwd_abs_path {

# Make absolute without collapsing .. (symlink-aware resolution does that)
if ( $path !~ m{^/} ) {
$path = Cwd::getcwd() . "/$path";
my $cwd = do { local $!; Cwd::getcwd() };
$path = $cwd . "/$path";
}

my @remaining = grep { $_ ne '' && $_ ne '.' } split( m{/}, $path );
Expand Down
86 changes: 86 additions & 0 deletions t/errno_preservation.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/perl -w

use strict;
use warnings;

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

use Errno qw( ENOENT EACCES );
use Test::MockFile qw< nostrict >;
use Cwd ();

# ==========================================================================
# GH #396: CORE::GLOBAL overrides must not clobber $! on success paths
# ==========================================================================

# _abs_path_to_file calls getcwd() for relative paths and getpwent() for
# tilde paths. Both can set $! as a side effect. The fix wraps these in
# local $! so callers see the errno they set, not internal noise.

# --------------------------------------------------------------------------
# open() on absolute path should not clobber $!
# --------------------------------------------------------------------------
{
my $mock = Test::MockFile->file( '/tmp/errno_test.txt', 'data' );

$! = ENOENT; # Set a known errno
open( my $fh, '<', '/tmp/errno_test.txt' ) or die "open failed: $!";
is( $! + 0, ENOENT, 'open (absolute path) preserves $! on success' );
close $fh;
}

# --------------------------------------------------------------------------
# stat() on absolute path should not clobber $!
# --------------------------------------------------------------------------
{
my $mock = Test::MockFile->file( '/tmp/errno_stat.txt', 'data' );

$! = EACCES;
my @st = stat('/tmp/errno_stat.txt');
ok( @st > 0, 'stat succeeds on mocked file' );
is( $! + 0, EACCES, 'stat (absolute path) preserves $! on success' );
}

# --------------------------------------------------------------------------
# unlink() should not clobber $! on success
# --------------------------------------------------------------------------
{
my $mock = Test::MockFile->file( '/tmp/errno_unlink.txt', 'data' );

$! = ENOENT;
my $ret = unlink('/tmp/errno_unlink.txt');
is( $ret, 1, 'unlink succeeds' );
is( $! + 0, ENOENT, 'unlink preserves $! on success' );
}

# --------------------------------------------------------------------------
# Cwd::abs_path override should not clobber $! on success
# --------------------------------------------------------------------------
{
my $mock = Test::MockFile->file( '/tmp/errno_cwd.txt', 'data' );

$! = EACCES;
my $abs = Cwd::abs_path('/tmp/errno_cwd.txt');
is( $abs, '/tmp/errno_cwd.txt', 'abs_path resolves correctly' );
is( $! + 0, EACCES, 'Cwd::abs_path preserves $! on success' );
}

# --------------------------------------------------------------------------
# readdir should not clobber $! on success
# --------------------------------------------------------------------------
{
my $mock_dir = Test::MockFile->dir('/tmp/errno_dir');
my $mock_f = Test::MockFile->file( '/tmp/errno_dir/a.txt', 'hello' );

$! = ENOENT;
opendir( my $dh, '/tmp/errno_dir' ) or die "opendir: $!";
# opendir may legitimately change $! — reset after
$! = ENOENT;
my @entries = readdir($dh);
ok( @entries > 0, 'readdir returns entries' );
closedir($dh);
}

done_testing;
Loading