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
18 changes: 18 additions & 0 deletions lib/Test/MockFile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4125,6 +4125,24 @@ sub __rename ($$) {
$mock_new->{'mtime'} = $mock_old->{'mtime'};
$mock_new->{'atime'} = $mock_old->{'atime'};

# Migrate open file handles from old mock to new mock.
# Unix semantics: open fds follow the inode, not the directory entry.
# Each tied FileHandle has a weak 'data' ref that must be re-pointed.
if ( $mock_old->{'fhs'} && @{ $mock_old->{'fhs'} } ) {
$mock_new->{'fhs'} //= [];
for my $fh_ref ( @{ $mock_old->{'fhs'} } ) {
next unless defined $fh_ref;
my $tied = ref $fh_ref ? tied( *{$fh_ref} ) : undef;
if ($tied) {
$tied->{'data'} = $mock_new;
Scalar::Util::weaken( $tied->{'data'} );
$tied->{'file'} = $mock_new->{'path'};
}
push @{ $mock_new->{'fhs'} }, $fh_ref;
}
$mock_old->{'fhs'} = [];
}

# rename updates ctime on both source and destination
my $now = time;
$mock_new->{'ctime'} = $now;
Expand Down
99 changes: 99 additions & 0 deletions t/rename_open_handle.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use strict;
use warnings;

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

use Test::MockFile qw< nostrict >;

subtest 'read continues after rename' => sub {
my $old = Test::MockFile->file( '/fake/old.txt', 'Hello World' );
my $new = Test::MockFile->file( '/fake/new.txt', '' );

open( my $fh, '<', '/fake/old.txt' ) or die "open: $!";
my $buf;
read( $fh, $buf, 5 );
is( $buf, 'Hello', 'read before rename' );

rename( '/fake/old.txt', '/fake/new.txt' ) or die "rename: $!";

# Unix semantics: open fd survives rename — data follows the inode.
read( $fh, $buf, 6 );
is( $buf, ' World', 'read after rename sees remaining data' );

close $fh;
};

subtest 'write continues after rename' => sub {
my $old = Test::MockFile->file( '/fake/w_old.txt', 'AB' );
my $new = Test::MockFile->file( '/fake/w_new.txt', '' );

open( my $fh, '+<', '/fake/w_old.txt' ) or die "open: $!";

rename( '/fake/w_old.txt', '/fake/w_new.txt' ) or die "rename: $!";

# Write should go to the new location's data.
print $fh 'XY';
close $fh;

is( $new->contents, 'XY', 'write after rename lands in new mock' );
};

subtest 'tell preserved after rename' => sub {
my $old = Test::MockFile->file( '/fake/t_old.txt', 'ABCDEF' );
my $new = Test::MockFile->file( '/fake/t_new.txt', '' );

open( my $fh, '<', '/fake/t_old.txt' ) or die "open: $!";
my $buf;
read( $fh, $buf, 3 );
is( tell($fh), 3, 'tell before rename' );

rename( '/fake/t_old.txt', '/fake/t_new.txt' ) or die "rename: $!";

is( tell($fh), 3, 'tell unchanged after rename' );
read( $fh, $buf, 3 );
is( $buf, 'DEF', 'continue reading from correct position' );

close $fh;
};

subtest 'multiple handles survive rename' => sub {
my $old = Test::MockFile->file( '/fake/m_old.txt', 'DATA' );
my $new = Test::MockFile->file( '/fake/m_new.txt', '' );

open( my $fh1, '<', '/fake/m_old.txt' ) or die "open1: $!";
open( my $fh2, '<', '/fake/m_old.txt' ) or die "open2: $!";

# Advance fh1 but not fh2
my $buf;
read( $fh1, $buf, 2 );

rename( '/fake/m_old.txt', '/fake/m_new.txt' ) or die "rename: $!";

read( $fh1, $buf, 2 );
is( $buf, 'TA', 'handle 1 continues from tell=2' );

read( $fh2, $buf, 4 );
is( $buf, 'DATA', 'handle 2 reads full contents from tell=0' );

close $fh1;
close $fh2;
};

subtest 'eof works after rename' => sub {
my $old = Test::MockFile->file( '/fake/e_old.txt', 'AB' );
my $new = Test::MockFile->file( '/fake/e_new.txt', '' );

open( my $fh, '<', '/fake/e_old.txt' ) or die "open: $!";
my $buf;
read( $fh, $buf, 2 );

rename( '/fake/e_old.txt', '/fake/e_new.txt' ) or die "rename: $!";

ok( eof($fh), 'eof returns true after rename when at end' );

close $fh;
};

done_testing();
Loading