The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use Getopt::Long;
use Test::More;
use strict;
my $tests;
BEGIN { $tests = 0 }
use lib "lib";

my %Opt;
GetOptions(
           "verbose!",
          ) or die;
$Opt{verbose} ||= $ENV{PERL_RERSYNCRECENT_TEST_VERBOSE};

my $HAVE;
BEGIN {
    # neither LibMagic nor MMagic tell them apart
    for my $package (
                     # "File::LibMagic",
                     "File::MMagic",
                    ) {
        $HAVE->{$package} = eval qq{ require $package; };
    }
}

use Dumpvalue;
use File::Basename qw(dirname);
use File::Copy qw(cp);
use File::Path qw(mkpath rmtree);
use File::Rsync::Mirror::Recent;
use File::Rsync::Mirror::Recentfile;
use List::MoreUtils qw(uniq);
use Storable;
use Time::HiRes qw(time sleep);
use YAML::Syck;

my $root_from = "t/ta";
my $root_to = "t/tb";
my $statusfile = "t/recent-rmirror-state.yml";
rmtree [$root_from, $root_to];

{
    my @serializers;
    my $test_counter;
    BEGIN {
        $test_counter = $tests;
        @serializers = (
                        [".yaml","YAML::Syck"],
                        [".json","JSON"],
                        [".sto","Storable"],
                        [".dd","Data::Dumper"],
                       );
        $tests += @serializers;
        if ($HAVE->{"File::LibMagic"}||$HAVE->{"File::MMagic"}) {
            $tests += @serializers;
        }
    }
    printf "#test_counter[%d]\n", $test_counter;
    mkpath $root_from;
    my $ttt = "$root_from/ttt";
    open my $fh, ">", $ttt or die "Could not open: $!";
    print $fh time;
    close $fh or die "Could not close: $!";
    my $fm;
    if ($HAVE->{"File::LibMagic"}) {
        $fm = File::LibMagic->new();
    } elsif ($HAVE->{"File::MMagic"}) {
        $fm = File::MMagic->new();
    }
    for my $serializer (@serializers) {
        my($s,$module) = @$serializer;
        unless (eval "require $module; 1") {
            ok(1, "Skipping because $module not installed");
            if ($fm) {
                ok(1, "Skipping the magic test for same reason");
            }
            next;
        }
        my $rf = File::Rsync::Mirror::Recentfile->new
            (
             filenameroot   => "RECENT",
             interval       => q(1m),
             localroot      => $root_from,
             serializer_suffix => $s,
            );
        $rf->update($ttt,"new");
        if ($fm) {
            my $magic = $fm->checktype_filename("$root_from/RECENT-1m$s");
            ok($magic, sprintf
               ("Got a magic[%s] for s[%s]: [%s]",
                ref $fm,
                $s,
                $magic,
               ));
        }
        my $content = do {open my $fh, "$root_from/RECENT-1m$s";local $/;<$fh>};
        $content = Dumpvalue->new()->stringify($content);
        my $want_length = 42; # or maybe 3 more
        substr($content,$want_length) = "..." if length $content > 3+$want_length;
        ok($content, "Got a substr for s[$s]: [$content]");
    }
}

rmtree [$root_from, $root_to];

{
    # very small tree, aggregate it
    my @intervals;
    my $test_counter;
    BEGIN {
        $test_counter = $tests;
        @intervals = qw( 2s 4s 8s 16s 32s Z );
        $tests += 2 + 2 * (10 + 14 * @intervals); # test_counter
    }
    printf "#test_counter[%d]\n", $test_counter;
    ok(1, "starting smalltree block");
    is 6, scalar @intervals, "array has 6 elements: @intervals";
    printf "#test_counter[%d]\n", $test_counter+=2;
    for my $pass (0,1) {
        my $rf0 = File::Rsync::Mirror::Recentfile->new
            (
             aggregator     => [@intervals[1..$#intervals]],
             interval       => $intervals[0],
             localroot      => $root_from,
             rsync_options  => [
                                compress          => 0,
                                links             => 1,
                                times             => 1,
                                checksum          => 0,
                               ],
            );
        my $timestampfutured = 0;
        for my $iv (@intervals) {
            for my $i (0..3) {
                my $file = sprintf
                    (
                     "%s/A%s-%02d",
                     $root_from,
                     $iv,
                     $i,
                    );
                mkpath dirname $file;
                open my $fh, ">", $file or die "Could not open '$file': $!";
                print $fh time, ":", $file, "\n";
                close $fh or die "Could not close '$file': $!";
                $rf0->update($file,"new");
                if ($pass==1 && !$timestampfutured) {
                    my $recent_events = $rf0->recent_events;
                    $recent_events->[0]{epoch} += 987654321;
                    $rf0->write_recent($recent_events);
                    $timestampfutured++;
                }
            }
        }
        my $recent_events = $rf0->recent_events;
        # faking internals as if the contents were wide-spread in time
        for my $evi (0..$#$recent_events) {
            my $ev = $recent_events->[$evi];
            $ev->{epoch} -= 2**($evi*.25);
        }
        $rf0->write_recent($recent_events);
        $rf0->aggregate;
        my $filesize_threshold = 1750; # XXX may be system dependent
        my %size_before;
        for my $iv (@intervals) {
            my $rf = "$root_from/RECENT-$iv.yaml";
            my $filesize = -s $rf;
            $size_before{$iv} = $filesize;
            # now they have $filesize_threshold+ bytes because they were merged for the
            # first time ever and could not be truncated for this reason.
            ok( $filesize > $filesize_threshold, "file $iv (before merging) has good size[$filesize]");
            utime 0, 0, $rf; # so that the next aggregate isn't skipped
        }
        printf "#test_counter[%d]\n", $test_counter+=6;
        open my $fh, ">", "$root_from/finissage" or die "Could not open: $!";
        print $fh "fin";
        close $fh or die "Could not close: $!";
        $rf0->update("$root_from/finissage","new");
        $rf0 = File::Rsync::Mirror::Recentfile->new_from_file("$root_from/RECENT-2s.yaml");
        $rf0->aggregate;
        for my $iv (@intervals) {
            my $filesize = -s "$root_from/RECENT-$iv.yaml";
            # now they have <$filesize_threshold bytes because the second aggregate could
            # truncate them
            ok($iv eq "Z" || $filesize<$size_before{$iv}, "file $iv (after merging) has good size[$filesize<$size_before{$iv}]");
        }
        printf "#test_counter[%d]\n", $test_counter+=6;
        my $dagg1 = $rf0->_debug_aggregate;
        Time::HiRes::sleep 1.2;
        $rf0->aggregate;
        my $dagg2 = $rf0->_debug_aggregate;
        {
            my $recc = File::Rsync::Mirror::Recent->new
                (
                 local => "$root_from/RECENT-2s.yaml",
                );
            ok $recc->overview, "overview created";
            # diag $recc->overview;
        }
        printf "#test_counter[%d]\n", $test_counter+=1;
        for my $dirti (0,1,2) {
            open my $fh2, ">", "$root_from/dirty$dirti" or die "Could not open: $!";
            print $fh2 "dirty$dirti";
            close $fh2 or die "Could not close: $!";
            my $timestamp = $dirti <= 1 ? "999.999" : "1999.999";
            my $becomes_i = $dirti <= 1 ? -1 : -3;
            $rf0->update("$root_from/dirty$dirti","new",$timestamp);
            $recent_events = $rf0->recent_events;
            is $recent_events->[-1]{epoch}, $timestamp, "found the dirty timestamp during dirti[$dirti]";
            printf "#test_counter[%d]\n", $test_counter+=1;
            $rf0->aggregate(force => 1);
            my $recc = File::Rsync::Mirror::Recent->new
                (
                 localroot => $root_from,
                 local => "$root_from/RECENT.recent",
                );
            my %seen;
            for my $rf (@{$recc->recentfiles}) {
                my $isec = $rf->interval_secs;
                my $re = $rf->recent_events;
                like $re->[-1]{epoch}, qr/999\.999/, "found some dirty timestamp[$re->[-1]{epoch}] in isec[$isec]";
                my $dirtymark = $rf->dirtymark;
                ok $dirtymark, "dirtymark[$dirtymark]";
                $seen{ $rf->dirtymark }++;
            }
            printf "#test_counter[%d]\n", $test_counter+=12;
            is scalar keys %seen, 1, "all recentfiles have the same dirtymark";
            printf "#test_counter[%d]\n", $test_counter+=1;
            sleep 0.2;
            $rf0->aggregate(force => 1);
            my $rfs = $recc->recentfiles;
            for my $i (0..$#$rfs) {
                my $rf = $rfs->[$i];
                my $re = $rf->recent_events;
                if ($i == 0) {
                    unlike $re->[-1]{epoch}, qr/999\.999/, "dirty file events already moved up i[$i]";
                } elsif ($i == $#$rfs) {
                    is $re->[$becomes_i]{epoch}, $timestamp, "found the dirty timestamp on i[$i]";
                } else {
                    isnt $re->[-1]{epoch}, $timestamp, "dirty timestamp gone on i[$i]";
                }
                my $dirtymark = $rf->dirtymark;
                ok $dirtymark, "dirtymark[$dirtymark]";
                $seen{ $rf->dirtymark }++;
            }
            printf "#test_counter[%d]\n", $test_counter+=12;
            is scalar keys %seen, 1, "all recentfiles have the same dirtymark";
            printf "#test_counter[%d]\n", $test_counter+=1;
        }
        # $DB::single++;
        rmtree [$root_from];
    }
}

rmtree [$root_from, $root_to];

{
    # replay a short history, run aggregate on it, add files, aggregate again
    my $test_counter;
    BEGIN {
        $test_counter = $tests;
        $tests += 208;
    }
    printf "#test_counter[%d]\n", $test_counter;
    ok(1, "starting short history block");
    my $rf = File::Rsync::Mirror::Recentfile->new_from_file("t/RECENT-6h.yaml");
    my $recent_events = $rf->recent_events;
    my $recent_events_cnt = scalar @$recent_events;
    is (
        92,
        $recent_events_cnt,
        "found $recent_events_cnt events",
       );
    $rf->interval("5s");
    $rf->localroot($root_from);
    $rf->comment("produced during the test 02-operation.t");
    $rf->aggregator([qw(10s 30s 1m 1h Z)]);
    $rf->verbose(0);
    my $start = Time::HiRes::time;
    for my $e (@$recent_events) {
        for my $pass (0,1) {
            my $file = sprintf
                (
                 "%s/%s",
                 $pass==0 ? $root_from : $root_to,
                 $e->{path},
                );
            mkpath dirname $file;
            open my $fh, ">", $file or die "Could not open '$file': $!";
            print $fh time, ":", $file, "\n";
            close $fh or die "Could not close '$file': $!";
            if ($pass==0) {
                $rf->update($file,$e->{type});
            }
        }
    }
    $rf->aggregate;
    my $took = Time::HiRes::time - $start;
    ok $took > 0, "creating the tree and aggregate took $took seconds";
    my $dagg1 = $rf->_debug_aggregate;
    for my $i (1..5) {
        my $file_from = "$root_from/anotherfilefromtesting$i";
        open my $fh, ">", $file_from or die "Could not open: $!";
        print $fh time, ":", $file_from;
        close $fh or die "Could not close: $!";
        $rf->update($file_from,"new");
    }
    $rf->aggregate;
    my $dagg2 = $rf->_debug_aggregate;
    undef $rf;
    ok($dagg1->[0]{size} < $dagg2->[0]{size}, "The second 5s file size larger: $dagg1->[0]{size} < $dagg2->[0]{size}");
    ok($dagg1->[1]{mtime} <= $dagg2->[1]{mtime}, "The second 30s file timestamp larger: $dagg1->[1]{mtime} <= $dagg2->[1]{mtime}");
    is $dagg1->[2]{size}, $dagg2->[2]{size}, "The 1m file size unchanged";
    is $dagg1->[3]{mtime}, $dagg2->[3]{mtime}, "The 1h file timestamp unchanged";
    ok -l "t/ta/RECENT.recent", "found the symlink";
    my $have_slept = my $have_worked = 0;
    $start = Time::HiRes::time;
    my $debug = +[];
    for my $i (0..99) {
        my $file = sprintf
            (
             "%s/secscnt%03d",
             $root_from,
             ($i<25) ? ($i%12) : $i,
            );
        open my $fh, ">", $file or die "Could not open '$file': $!";
        print $fh time, ":", $file, "\n";
        close $fh or die "Could not close '$file': $!";
        my $another_rf = File::Rsync::Mirror::Recentfile->new
            (
             interval => "5s",
             localroot => $root_from,
             aggregator => [qw(10s 30s 1m Z)],
            );
        $another_rf->update($file,"new");
        my $should_have = 97 + (($i<25) ? ($i < 12 ? ($i+1) : 12) : ($i-12));
        my($news,$filtered_news);
        if ($i < 50) {
            $another_rf->aggregate;
        }
        {
            my $recc = File::Rsync::Mirror::Recent->new
                (
                 local => "$root_from/RECENT-5s.yaml",
                );
            $news = $recc->news ();
            $filtered_news = [ uniq map { $_->{path} } @$news ];
        }
        is scalar @$filtered_news, $should_have, "should_have[$should_have]" or die;
        $debug->[$i] = $news;
        my $rf2 = File::Rsync::Mirror::Recentfile->new_from_file("$root_from/RECENT-5s.yaml");
        my $rece = $rf2->recent_events;
        my $rececnt = @$rece;
        my $span = $rece->[0]{epoch} - $rece->[-1]{epoch};
        $have_worked = Time::HiRes::time - $start - $have_slept;
        ok($rececnt > 0
           && ($i<50 ? $span <= 5 # we have run aggregate, so it guaranteed(*)
               : $i < 90 ? 1      # we have not yet spent 5 seconds, so cannot predict
               : $span > 5        # we have certainly written enough files now, must happen
              ),
           sprintf
           ("i[%s]cnt[%s]span[%s]worked[%6.4f]",
            $i,
            $rececnt,
            $span,
            $have_worked,
           ));
        $have_slept += Time::HiRes::sleep 0.2;
    }
    # (*) "<=" instead of "<" because of rounding errors
}

{
    # running mirror
    my $test_counter;
    BEGIN {
        $test_counter = $tests;
        $tests += 3;
    }
    printf "#test_counter[%d]\n", $test_counter;
    my $rf = File::Rsync::Mirror::Recentfile->new
        (
         filenameroot              => "RECENT",
         interval                  => q(30s),
         localroot                 => $root_to,
         max_rsync_errors          => 0,
         remote_dir                => $root_from,
         # verbose                 => 1,
         max_files_per_connection  => 65,
         rsync_options  => {
                            compress          => 0,
                            links             => 1,
                            times             => 1,
                            # not available in rsync 3.0.3: 'omit-dir-times'  => 1,
                            checksum          => 0,
                           },
        );
    my $somefile_epoch;
    for my $pass (0,1) {
        my $success;
        if (0 == $pass) {
            $success = $rf->mirror;
            my $re = $rf->recent_events;
            $somefile_epoch = $re->[24]{epoch};
        } elsif (1 == $pass) {
            $success = $rf->mirror(after => $somefile_epoch);
        }
        ok($success, "mirrored pass[$pass] without dying");
    }
    {
        my $recc = File::Rsync::Mirror::Recent->new
            (  # ($root_from, $root_to)
             local => "$root_from/RECENT-5s.yaml",
            );
        diag "\n" if $Opt{verbose};
        diag $recc->overview if $Opt{verbose};
    }
    {
        my $recc = File::Rsync::Mirror::Recent->new
            (
             # ignore_link_stat_errors => 1,
             localroot => $root_to,
             remote => "$root_from/RECENT-5s.yaml",
             # verbose => 1,
             max_files_per_connection => 512,
             rsync_options => {
                               links => 1,
                               times => 1,
                               compress => 1,
                               checksum => 1,
                              },
            );
        $recc->rmirror;
    }
    {
        my $recc = File::Rsync::Mirror::Recent->new
            (  # ($root_from, $root_to)
             local => "$root_to/RECENT-5s.yaml",
            );
        diag "\n" if $Opt{verbose};
        diag $recc->overview if $Opt{verbose};
    }
    {
        BEGIN {
            $tests += 2;
        }
        my $recc = File::Rsync::Mirror::Recent->new
            (
             # order matters!
             # ignore_link_stat_errors => 1,
             localroot                   => $root_to,
             remote                      => "$root_from/RECENT.recent",
             max_files_per_connection    => 65,
             rsync_options               =>
             {
              links     => 1,
              times     => 1,
              compress  => 1,
              checksum  => 1,
             },
             _runstatusfile              => $statusfile,
             verbose                     => $Opt{verbose},
            );
        $recc->rmirror;
        my $rf2 = File::Rsync::Mirror::Recentfile->new_from_file("$root_from/RECENT-5s.yaml");
        my $file = "$root_from/about-re-mirroring.txt";
        open my $fh, ">", $file or die "Could not open '$file': $!";
        print $fh time;
        close $fh or die "Could not close '$file': $!";
        $rf2->update($file, "new");
        $recc->rmirror;
        ok -e "$root_to/about-re-mirroring.txt", "picked up the update";
        $file = "$root_from/about-re2-mirroring.txt";
        undef $fh;
        open $fh, ">", $file or die "Could not open '$file': $!";
        print $fh time;
        close $fh or die "Could not close '$file': $!";
        $rf2->update($file, "new", 123456789);
        $rf2->aggregate(force => 1);
        $rf2->aggregate(force => 1);
        $recc->verbose(1) if $Opt{verbose};

        # { no warnings 'once'; $DB::single++; }
        # x map { $_->dirtymark } @{$self->recentfiles}
        # x map { $_->_seeded } @{$self->recentfiles}
        # x sort keys %$rf
        # $recc->verbose(1)

        $recc->rmirror;
        ok -e "$root_to/about-re2-mirroring.txt", "picked up a dirty update";
    }
    {
        my $recc = File::Rsync::Mirror::Recent->new
            (  # ($root_from, $root_to)
             local => "$root_to/RECENT-5s.yaml",
            );
        diag "\n" if $Opt{verbose};
        diag $recc->overview if $Opt{verbose};
    }
    {
        my $recc = File::Rsync::Mirror::Recent->new
            (
             # ignore_link_stat_errors => 1,
             localroot => $root_to,
             local => "$root_to/RECENT.recent",
            );
        my %seen;
        for my $rf (@{$recc->recentfiles}) {
            my $dirtymark = $rf->dirtymark or next;
            $seen{ $dirtymark }++;
        }
        is scalar keys %seen, 1, "all recentfiles have the same dirtymark or we don't know it";
    }
}

rmtree [$root_from, $root_to, $statusfile] unless $Opt{verbose};

BEGIN {
    plan tests => $tests
}

# Local Variables:
# mode: cperl
# cperl-indent-level: 4
# End: