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
54 changes: 54 additions & 0 deletions lib/Test/MockFile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ sub file_arg_position_for_command { # can also be used by user hooks
'stat' => 0,
'sysopen' => 1,
'unlink' => 0,
'utime' => 2,
'readdir' => 0,
};

Expand Down Expand Up @@ -2429,6 +2430,58 @@ sub __chmod (@) {
return $num_changed;
}

sub __utime (@) {
my ( $atime, $mtime, @files ) = @_;

# Not an error, report we changed zero files
@files
or return 0;

my %mocked_files = map +( $_ => _get_file_object($_) ), @files;
my @unmocked_files = grep !$mocked_files{$_}, @files;
my @mocked_files = map ref $_ ? $_->{'path'} : (), values %mocked_files;

if ( @mocked_files && @mocked_files != @files ) {
confess(
sprintf 'You called utime() on a mix of mocked (%s) and unmocked files (%s) ' . ' - this is very likely a bug on your side',
( join ', ', @mocked_files ),
( join ', ', @unmocked_files ),
);
}

# If no files are mocked, fall through to the real utime
if ( !@mocked_files ) {
_real_file_access_hook( 'utime', \@_ );
goto \&CORE::utime if _goto_is_available();
return CORE::utime( $atime, $mtime, @files );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is good but I think we should have also preserved the call and check later when $mocked_files{$file} is undefined then we should also fall back to the CORE::utime

Can we extend coverage when mock is enabled (no strict) to call utime on a mocked file but also on a none mocked file to cover that scenario line 2462 when my $mock = $mocked_files{$file}; is undef

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added:

  1. Per-file if (!$mock) fallback inside the foreach loop (lines 2464-2468) — mirrors the __chmod pattern. Falls through to CORE::utime if a file has no mock object.

  2. New test: utime on unmocked file while mocked files exist — creates a mocked file at /mocked/for_utime, then calls utime on a real tempfile. Verifies the passthrough works when mocks are active but the target file is unmocked.

Tempfiles are created in BEGIN before use Test::MockFile to avoid File::Temp's internal stat/chmod hitting our overrides.

Force-pushed as a single squashed commit (678bd07).


my $now = time;
my $num_changed = 0;
foreach my $file (@files) {
my $mock = $mocked_files{$file};

if ( !$mock ) {
_real_file_access_hook( 'utime', \@_ );
goto \&CORE::utime if _goto_is_available();
return CORE::utime( $atime, $mtime, @files );
}

if ( !$mock->exists() ) {
$! = ENOENT;
next;
}

$mock->{'atime'} = defined $atime ? $atime : $now;
$mock->{'mtime'} = defined $mtime ? $mtime : $now;
$mock->{'ctime'} = $now;

$num_changed++;
}

return $num_changed;
}

BEGIN {
*CORE::GLOBAL::glob = !$^V || $^V lt 5.18.0
? sub {
Expand All @@ -2452,6 +2505,7 @@ BEGIN {
*CORE::GLOBAL::rmdir = \&__rmdir;
*CORE::GLOBAL::chown = \&__chown;
*CORE::GLOBAL::chmod = \&__chmod;
*CORE::GLOBAL::utime = \&__utime;
}

=head1 CAEATS AND LIMITATIONS
Expand Down
164 changes: 164 additions & 0 deletions t/utime.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/perl -w

use strict;
use warnings;

use Test2::Bundle::Extended;
use Test2::Tools::Explain;
use Test2::Plugin::NoWarnings;
use Test2::Tools::Exception qw< lives dies >;

use File::Temp qw< tempfile >;
use Cwd ();
use Errno qw< ENOENT >;

# Create tempfiles before Test::MockFile overrides are installed,
# so File::Temp's internal stat/chmod calls hit real CORE functions.
my ( $passthrough_tempfile, $nostrict_tempfile );
BEGIN {
( undef, $passthrough_tempfile ) = tempfile( UNLINK => 0 );
( undef, $nostrict_tempfile ) = tempfile( UNLINK => 0 );
}

use Test::MockFile qw< nostrict >;

subtest(
'utime on mocked file' => sub {
my $file = Test::MockFile->file( '/foo/bar', 'content' );
ok( -f '/foo/bar', 'File exists' );

my $new_atime = 1000000;
my $new_mtime = 2000000;

is( utime( $new_atime, $new_mtime, '/foo/bar' ), 1, 'utime returns 1 for success' );

my @stat = stat('/foo/bar');
is( $stat[8], $new_atime, 'atime was updated' );
is( $stat[9], $new_mtime, 'mtime was updated' );
}
);

subtest(
'utime updates ctime to current time' => sub {
my $file = Test::MockFile->file( '/foo/baz', 'content' );

my $before = time;
utime( 1000, 2000, '/foo/baz' );
my $after = time;

my @stat = stat('/foo/baz');
ok( $stat[10] >= $before && $stat[10] <= $after, 'ctime was updated to current time' );
}
);

subtest(
'utime with undef uses current time' => sub {
my $file = Test::MockFile->file( '/foo/undef_test', 'content' );

my $before = time;
is( utime( undef, undef, '/foo/undef_test' ), 1, 'utime with undef returns 1' );
my $after = time;

my @stat = stat('/foo/undef_test');
ok( $stat[8] >= $before && $stat[8] <= $after, 'atime set to current time when undef' );
ok( $stat[9] >= $before && $stat[9] <= $after, 'mtime set to current time when undef' );
}
);

subtest(
'utime on multiple mocked files' => sub {
my $file1 = Test::MockFile->file( '/multi/a', 'aaa' );
my $file2 = Test::MockFile->file( '/multi/b', 'bbb' );

is( utime( 5000, 6000, '/multi/a', '/multi/b' ), 2, 'utime returns 2 for two files' );

my @stat_a = stat('/multi/a');
my @stat_b = stat('/multi/b');
is( $stat_a[8], 5000, 'file a atime updated' );
is( $stat_a[9], 6000, 'file a mtime updated' );
is( $stat_b[8], 5000, 'file b atime updated' );
is( $stat_b[9], 6000, 'file b mtime updated' );
}
);

subtest(
'utime on nonexistent mocked file' => sub {
my $file = Test::MockFile->file('/no/exist');
ok( !-f '/no/exist', 'File does not exist' );

$! = 0;
is( utime( 1000, 2000, '/no/exist' ), 0, 'utime returns 0 for nonexistent file' );
is( $! + 0, ENOENT, '$! is set to ENOENT' );
}
);

subtest(
'utime on mocked directory' => sub {
my $dir = Test::MockFile->dir('/mydir');
ok( mkdir('/mydir'), 'Created directory' );
ok( -d '/mydir', 'Directory exists' );

is( utime( 3000, 4000, '/mydir' ), 1, 'utime on directory returns 1' );

my @stat = stat('/mydir');
is( $stat[8], 3000, 'dir atime updated' );
is( $stat[9], 4000, 'dir mtime updated' );
}
);

subtest(
'utime with no files returns 0' => sub {
is( utime( 1000, 2000 ), 0, 'utime with no files returns 0' );
}
);

subtest(
'utime on mix of mocked and unmocked files' => sub {
my $filename = __FILE__;
my $mocked = Cwd::getcwd() . "/$filename";
my $unmocked = '/foo_DOES_NOT_EXIST.znxc';

my $file = Test::MockFile->file( $filename, 'whatevs' );

like(
dies( sub { utime 1000, 2000, $filename, $unmocked } ),
qr/^\QYou called utime() on a mix of mocked ($mocked) and unmocked files ($unmocked)\E/xms,
'Cannot mix mocked and unmocked files with utime',
);
}
);

subtest(
'utime on unmocked file passes through' => sub {
my $new_atime = 1000000;
my $new_mtime = 2000000;

is( utime( $new_atime, $new_mtime, $passthrough_tempfile ), 1, 'utime on real file returns 1' );

my @stat = CORE::stat($passthrough_tempfile);
is( $stat[8], $new_atime, 'real file atime was updated' );
is( $stat[9], $new_mtime, 'real file mtime was updated' );

CORE::unlink $passthrough_tempfile;
}
);

subtest(
'utime on unmocked file while mocked files exist' => sub {
my $mock = Test::MockFile->file( '/mocked/for_utime', 'data' );

my $new_atime = 3000000;
my $new_mtime = 4000000;

is( utime( $new_atime, $new_mtime, $nostrict_tempfile ), 1, 'utime on unmocked file returns 1' );

my @stat = CORE::stat($nostrict_tempfile);
is( $stat[8], $new_atime, 'unmocked file atime was updated' );
is( $stat[9], $new_mtime, 'unmocked file mtime was updated' );

CORE::unlink $nostrict_tempfile;
}
);

done_testing();
exit;
45 changes: 45 additions & 0 deletions t/utime_strict.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/perl -w

use strict;
use warnings;

use Test2::Bundle::Extended;
use Test2::Plugin::NoWarnings;
use Test2::Tools::Exception qw< dies lives >;

use File::Temp qw< tempfile >;

my ( $temp_fh, $tempfile );
BEGIN { ( $temp_fh, $tempfile ) = tempfile( UNLINK => 0 ); close $temp_fh; }

use Test::MockFile;

subtest(
'utime on unmocked file in strict mode dies' => sub {
like(
dies( sub { utime 1000, 2000, $tempfile } ),
qr/\Qutime\E/,
'utime on unmocked file in strict mode triggers violation',
);

CORE::unlink($tempfile);
}
);

subtest(
'utime on mocked file in strict mode succeeds' => sub {
my $file = Test::MockFile->file( '/strict/test', 'content' );

ok(
lives( sub { utime 1000, 2000, '/strict/test' } ),
'utime on mocked file in strict mode works',
) or note $@;

my @stat = stat('/strict/test');
is( $stat[8], 1000, 'atime set correctly in strict mode' );
is( $stat[9], 2000, 'mtime set correctly in strict mode' );
}
);

done_testing();
exit;