diff --git a/src/main/perl/FileEditor.pm b/src/main/perl/FileEditor.pm index 432283b5..af5d0bf7 100644 --- a/src/main/perl/FileEditor.pm +++ b/src/main/perl/FileEditor.pm @@ -47,24 +47,82 @@ the class constructor. =item new Returns a new object it accepts the same arguments as the constructor -for C +for C with one additional option: + +=over + +=item source + +This option, when present, must be a file name whose contents will be used +as the initial contents for the edited file if the source modification time +is more recent than the edited file modification time. This allows to rebuild +the file contents based on a new version of the reference file. + +The C can be a pipe: in this case, it is always considered more recent +than the edited file. + +=back =cut # FileEditor supports reading/editing a file -sub _is_valid_source +sub _is_valid_file { my ($self, $fn) = @_; return -f $fn; } +# This method is only intended to be used in the context of the constructor +# and entirely relies on the internal structure of the FileEditor object. +# 'filename` is the file name passed with instantiating the FileEditor and +# 'options/sources' the 'source' parameter value (called a reference file +# internally). +sub _is_reference_newer +{ + my ($self) = @_; + my $is_newer = 0; # Assume false + if ( exists(*$self->{options}->{source}) ) { + # It is valid for the source value to be a pipe: in this case consider it + # as newer than an existing file. + if ( -p *$self->{options}->{source} ) { + $is_newer = 1 + } elsif ( $self->_is_valid_file(*$self->{options}->{source}) ) { + # stat()[9] is modification time + if ( !$self->_is_valid_file(*$self->{filename}) || + ((stat(*$self->{options}->{source}))[9] > (stat(*$self->{filename}))[9]) ) { + $is_newer = 1 + } + } + } + #FIXME: replace by $self->debug() after PR #154 has been merged... + if ( *$self->{LOG} ) { + if ( $is_newer ) { + *$self->{LOG}->debug(1, "File ", *$self->{filename}, " older than reference file (", *$self->{options}->{source}, ","); + } else { + *$self->{LOG}->debug(1, "Reference file (", *$self->{options}->{source}, ") older than ", *$self->{filename}); + }; + }; + return $is_newer; +} sub new { my $class = shift; my $self = $class->SUPER::new (@_); - if ($self->_is_valid_source(*$self->{filename})) { - my $txt = LC::File::file_contents (*$self->{filename}); + my $src_file; + my ($path, %opts) = @_; + + *$self->{options}->{source} = $opts{source} if exists ($opts{source}); + if ( $self->_is_reference_newer() ) { + # As this is a non reproducible event, be sure to log it when it happens + *$self->{LOG}->info("File ", *$self->{filename}, " contents reset to reference file (", *$self->{options}->{source}, ") contents.") if *$self->{LOG}; + $src_file = *$self->{options}->{source}; + } elsif ($self->_is_valid_file(*$self->{filename})) { + $src_file = *$self->{filename}; + } + if ( $src_file ) { + *$self->{LOG}->debug(2, "Reading initial contents from $src_file") if *$self->{LOG}; + my $txt = LC::File::file_contents ($src_file); $self->IO::String::open ($txt); $self->seek(IO_SEEK_END); } diff --git a/src/main/perl/FileReader.pm b/src/main/perl/FileReader.pm index 8d20bb1b..8f2ad477 100644 --- a/src/main/perl/FileReader.pm +++ b/src/main/perl/FileReader.pm @@ -41,7 +41,7 @@ seek to the beginning and C any (future) changes. =cut # FileReader supports reading a file or pipe -sub _is_valid_source +sub _is_valid_file { my ($self, $fn) = @_; return -f $fn || -p $fn; diff --git a/src/test/perl/test-caffileeditor_source.t b/src/test/perl/test-caffileeditor_source.t new file mode 100644 index 00000000..eaa30fff --- /dev/null +++ b/src/test/perl/test-caffileeditor_source.t @@ -0,0 +1,79 @@ +#!/usr/bin/perl +use strict; +use warnings; +use FindBin qw($Bin); +use lib "$Bin/modules"; +use CAF::FileEditor; +use Test::More tests => 7; +use Test::MockModule; +use Test::Quattor::Object; +use Carp qw(confess); +use File::Path; +use File::Temp qw(tempfile); +use Readonly; + +$SIG{__DIE__} = \&confess; + +# FIXME: +# LC::File::file_contents (used by CAF::FileEditor constructor) doesn't work +# in the unit test context. Mock it until we find a better solution... +our $lcfile = Test::MockModule->new("LC::File"); + +sub read_file_contents { + my $fname = shift; + my $fh; + open($fh, "<", $fname) || die ("failed to open $fname"); + my $contents = join('', <$fh>); + return $contents; +} +$lcfile->mock("file_contents", \&read_file_contents); + + +my $testdir = 'target/test/test_caffileeditor_source'; +mkpath($testdir); +(undef, my $filename) = tempfile(DIR => $testdir); + +Readonly my $TEXT => < "adarga antigua, rocín flaco y galgo corredor."; + +my $fh; +my $obj = Test::Quattor::Object->new(); + + +# Create a file and check that it is empty +($fh, $filename) = tempfile(DIR => $testdir); +$fh->close(); +$fh = CAF::FileEditor->new($filename, log => $obj); +is("$fh","","Existing file ($filename) empty"); +$fh->close(); + +# Check that reference file contents is used as the initial contents when it +# is newer than the file edited. +my $time=time(); +utime(undef, $time - 10, $filename); # make filename old enough +my ($ref_fh, $ref_filename) = tempfile(DIR => $testdir); +print $ref_fh $TEXT; +$ref_fh->close(); +$fh = CAF::FileEditor->new($filename, log => $obj, source => $ref_filename); +is(*$fh->{options}->{source},$ref_filename,"File source is correctly defined"); +ok($fh->_is_reference_newer(),"Source file ($ref_filename) is newer than actual file ($filename)"); +is("$fh",$TEXT,"Reference file ($filename) contents used"); +$fh->close(); + +# Check that reference file contents is not used as the initial contents when it +# is older than the file edited. +$time=time(); +utime(undef, $time - 10, $ref_filename); # make ref_filename old enough +my ($new_fh, $new_filename) = tempfile(DIR => $testdir); +print $new_fh $ANOTHER_TEXT; +$new_fh->close(); +$new_fh = CAF::FileEditor->new($new_filename, log => $obj, source => $ref_filename); +is(*$new_fh->{options}->{source},$ref_filename,"File source is correctly defined"); +ok(!$new_fh->_is_reference_newer(),"Source file ($ref_filename) is older than actual file ($filename)"); +is("$new_fh",$ANOTHER_TEXT,"Existing file ($new_filename) contents used"); +$fh->close(); + +done_testing();