Skip to content
Open
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
20 changes: 19 additions & 1 deletion lib/Test/MockFile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,22 @@ sub unlink {
$self->{'contents'} = undef;
}

# Decrement nlink on this mock and any other hard links sharing the same inode
if ( $self->{'nlink'} > 0 ) {
my $inode = $self->{'inode'};
if ( $inode && $self->{'nlink'} > 1 ) {
for my $path ( keys %files_being_mocked ) {
my $m = $files_being_mocked{$path};
next if !$m || $m == $self;
next if !$m->exists;
if ( defined $m->{'inode'} && $m->{'inode'} == $inode ) {
$m->{'nlink'}-- if $m->{'nlink'} > 0;
}
}
}
$self->{'nlink'}--;
}

_update_parent_dir_times( $self->path );
return 1;
}
Expand Down Expand Up @@ -3395,10 +3411,12 @@ sub __rename ($$) {
$mock_old->{'contents'} = undef;
}

# Copy mode and ownership
# 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'};

Expand Down
12 changes: 12 additions & 0 deletions t/rename.t
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,16 @@ note "-------------- rename: dir over existing file fails --------------";
is( $! + 0, ENOTDIR, 'errno is ENOTDIR' );
}

note "-------------- rename: preserves inode and nlink --------------";
{
my $old = Test::MockFile->file( '/mock/ino_old', 'data', { inode => 42, nlink => 3 } );
my $new = Test::MockFile->file('/mock/ino_new');

ok( rename( '/mock/ino_old', '/mock/ino_new' ), 'rename preserves inode metadata' );

my @st = stat('/mock/ino_new');
is( $st[1], 42, 'inode preserved after rename' );
is( $st[3], 3, 'nlink preserved after rename' );
}

done_testing();
32 changes: 32 additions & 0 deletions t/symlink_link.t
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,36 @@ note "-------------- link() builtin on mocked paths --------------";
is( \@entries, [qw< . .. newhard >], 'parent dir lists the new hard link' );
}

{
note "unlink() decrements nlink on the unlinked file";
my $src = Test::MockFile->file( '/mock/ul_src', 'data', { nlink => 1, inode => 90001 } );
my $dest = Test::MockFile->file('/mock/ul_dst');

link( '/mock/ul_src', '/mock/ul_dst' );

my $nlink_before = ( stat('/mock/ul_src') )[3];
is( $nlink_before, 2, 'source nlink is 2 after link' );

unlink('/mock/ul_src');

my $src_nlink_after = ( stat('/mock/ul_src') )[3];
is( $src_nlink_after, undef, 'stat on unlinked file returns undef (no longer exists)' );

my $dst_nlink = ( stat('/mock/ul_dst') )[3];
is( $dst_nlink, 1, 'remaining hard link nlink decremented after unlink' );
}

{
note "unlink() on a file with nlink=1 decrements to 0";
my $file = Test::MockFile->file( '/mock/ul_single', 'data', { nlink => 1 } );

my $nlink_before = ( stat('/mock/ul_single') )[3];
is( $nlink_before, 1, 'nlink is 1 before unlink' );

unlink('/mock/ul_single');

# File no longer exists, but the mock object's nlink should be decremented
ok( !-e '/mock/ul_single', 'file no longer exists after unlink' );
}

done_testing();