diff --git a/lib/Test/MockFile.pm b/lib/Test/MockFile.pm index f2a3f57..c88d891 100644 --- a/lib/Test/MockFile.pm +++ b/lib/Test/MockFile.pm @@ -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; } @@ -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'}; diff --git a/t/rename.t b/t/rename.t index 7fa7cb6..62663e9 100644 --- a/t/rename.t +++ b/t/rename.t @@ -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(); diff --git a/t/symlink_link.t b/t/symlink_link.t index da702f0..397c431 100644 --- a/t/symlink_link.t +++ b/t/symlink_link.t @@ -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();