From 56e672c10eaa2254b669fddb312e29b5cdc6c305 Mon Sep 17 00:00:00 2001 From: Toddr Bot Date: Fri, 3 Apr 2026 06:58:03 +0000 Subject: [PATCH] fix: report blocks() in 512-byte units per POSIX stat(2) blocks() was computing the number of filesystem blocks (ceil(size/blksize)) but stat(2) st_blocks is always reported in 512-byte units regardless of st_blksize. A 5-byte file with blksize=4096 should report blocks=8, not 1. Updated blocks() to multiply by (blksize/512) and fixed tests that encoded the incorrect expectations. Co-Authored-By: Claude Opus 4.6 --- lib/Test/MockFile.pm | 9 +++++++-- t/Test-MockFile_file.t | 4 ++-- t/blocks.t | 22 ++++++++++++++-------- t/mock_stat.t | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/Test/MockFile.pm b/lib/Test/MockFile.pm index 6c5d4cfb..20e5a7de 100644 --- a/lib/Test/MockFile.pm +++ b/lib/Test/MockFile.pm @@ -2517,11 +2517,16 @@ Calculates the block count of the file based on its size. sub blocks { my ($self) = @_; - my $size = $self->size; + my $size = $self->size; return 0 unless $size; my $blksize = abs( $self->{'blksize'} ); - return int( ( $size + $blksize - 1 ) / $blksize ); + + # Number of filesystem blocks needed (ceiling division) + my $fs_blocks = int( ( $size + $blksize - 1 ) / $blksize ); + + # stat(2) st_blocks is always in 512-byte units, regardless of st_blksize + return $fs_blocks * int( $blksize / 512 ); } =head2 chmod diff --git a/t/Test-MockFile_file.t b/t/Test-MockFile_file.t index d5c00915..40b5d2f1 100644 --- a/t/Test-MockFile_file.t +++ b/t/Test-MockFile_file.t @@ -56,8 +56,8 @@ subtest 'size() and blocks()' => sub { is( $mock->size(), 5000, 'size() matches content length' ); - # blocks() = ceil(size / blksize) — no 512-byte conversion - my $expected_blocks = int( ( 5000 + 4096 - 1 ) / 4096 ); + # blocks() = ceil(size / blksize) * (blksize / 512) — stat(2) uses 512-byte units + my $expected_blocks = int( ( 5000 + 4096 - 1 ) / 4096 ) * int( 4096 / 512 ); is( $mock->blocks(), $expected_blocks, 'blocks() computes correctly from size and blksize' ); }; diff --git a/t/blocks.t b/t/blocks.t index 484ec332..abbd40bd 100644 --- a/t/blocks.t +++ b/t/blocks.t @@ -12,7 +12,7 @@ use Test::MockFile qw< nostrict >; # Assures testers don't mess up with our hard coded perms expectations. umask 022; -note "blocks() calculation"; +note "blocks() calculation — stat(2) st_blocks is always in 512-byte units"; { note "empty file has 0 blocks"; @@ -24,34 +24,36 @@ note "blocks() calculation"; } { - note "small file has 1 block"; + note "small file with default 4096 blksize"; + # 1 filesystem block = 4096 bytes = 8 * 512-byte units my $mock = Test::MockFile->file( '/tmp/small', 'x' ); - is $mock->blocks, 1, "1-byte file: 1 block"; + is $mock->blocks, 8, "1-byte file: 8 blocks (1 fs block * 4096/512)"; my $mock2 = Test::MockFile->file( '/tmp/small2', 'x' x 100 ); - is $mock2->blocks, 1, "100-byte file: 1 block"; + is $mock2->blocks, 8, "100-byte file: 8 blocks (1 fs block * 4096/512)"; } { note "file exactly at blksize boundary"; my $mock = Test::MockFile->file( '/tmp/exact', 'x' x 4096 ); - is $mock->blocks, 1, "4096-byte file with 4096 blksize: exactly 1 block"; + is $mock->blocks, 8, "4096-byte file with 4096 blksize: exactly 8 blocks"; } { note "file one byte over blksize boundary"; my $mock = Test::MockFile->file( '/tmp/over', 'x' x 4097 ); - is $mock->blocks, 2, "4097-byte file: 2 blocks"; + is $mock->blocks, 16, "4097-byte file: 16 blocks (2 fs blocks * 4096/512)"; } { note "file at exactly 2x blksize"; my $mock = Test::MockFile->file( '/tmp/double', 'x' x 8192 ); - is $mock->blocks, 2, "8192-byte file with 4096 blksize: exactly 2 blocks"; + is $mock->blocks, 16, "8192-byte file with 4096 blksize: exactly 16 blocks"; } { note "custom blksize"; + # blksize=512: 1 fs block = 1 * 512-byte unit my $mock = Test::MockFile->file( '/tmp/custom_blk', 'x' x 1024, { blksize => 512 } ); is $mock->blocks, 2, "1024-byte file with 512 blksize: 2 blocks"; @@ -60,13 +62,17 @@ note "blocks() calculation"; my $mock3 = Test::MockFile->file( '/tmp/custom_blk3', 'x' x 512, { blksize => 512 } ); is $mock3->blocks, 1, "512-byte file with 512 blksize: exactly 1 block"; + + # blksize=1024: 1 fs block = 2 * 512-byte units + my $mock4 = Test::MockFile->file( '/tmp/custom_blk4', 'x' x 100, { blksize => 1024 } ); + is $mock4->blocks, 2, "100-byte file with 1024 blksize: 2 blocks (1 fs block * 1024/512)"; } { note "blocks from stat()"; my $mock = Test::MockFile->file( '/tmp/stat_blocks', 'hello' ); my @stat = stat('/tmp/stat_blocks'); - is $stat[12], 1, "stat[12] (blocks) is 1 for a 5-byte file"; + is $stat[12], 8, "stat[12] (blocks) is 8 for a 5-byte file (1 fs block in 512-byte units)"; my $mock_empty = Test::MockFile->file( '/tmp/stat_empty', '' ); my @stat_e = stat('/tmp/stat_empty'); diff --git a/t/mock_stat.t b/t/mock_stat.t index 9f0a9f80..63a27cf2 100644 --- a/t/mock_stat.t +++ b/t/mock_stat.t @@ -136,7 +136,7 @@ my $symlink_lstat_return = array { item match qr/^[0-9]{3,}$/; item match qr/^[0-9]{3,}$/; item 4096; - item 1; + item 8; # blocks: 1 fs block in 512-byte units (4096/512 = 8) }; is( Test::MockFile::_mock_stat( 'lstat', '/broken_link' ), $symlink_lstat_return, "lstat on /broken_link returns the stat on the symlink itself." );