The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# support module for helping test brackup
package Brackup::Test;
require Exporter;
use strict;
use vars qw(@ISA @EXPORT);
@ISA = qw(Exporter);
@EXPORT = qw(do_backup do_restore ok_dirs_match ok_files_match);

use Test::More;
use FindBin qw($Bin);
use Brackup::Util qw(tempdir tempfile);
use File::Find;
use File::stat ();
use Cwd;
use Brackup::DecryptedFile;
use Digest::SHA1 qw/sha1_hex/;

use Brackup;

my $has_diff = eval "use Text::Diff; 1;";

my @to_unlink;
my $par_pid = $$;
END {
    if ($$ == $par_pid) {
        my $rv = unlink @to_unlink unless $ENV{BRACKUP_TEST_NOCLEANUP};
    }
}

# Set the gpg directory, so we don't rely on users having a ~/.gnupg
$ENV{GNUPGHOME} = tempdir( CLEANUP => 1 );

sub do_backup {
    my %opts = @_;
    my $with_confsec    = delete $opts{'with_confsec'} || sub {};
    my $with_targetsec  = delete $opts{'with_targetsec'} || sub {};
    my $with_root       = delete $opts{'with_root'}    || sub {};
    my $target          = delete $opts{'with_target'};
    die if %opts;

    my $initer = shift;

    my $conf = Brackup::Config->new;
    my $confsec;

    $confsec = Brackup::ConfigSection->new("SOURCE:test_root");
    $with_confsec->($confsec);
    $conf->add_section($confsec);

    my $root = $conf->load_root("test_root");
    ok($root, "have a source root");
    $with_root->($root);

    unless ($target) {
        my $backup_dir = tempdir( CLEANUP => $ENV{BRACKUP_TEST_NOCLEANUP} ? 0 : 1 );
        ok_dir_empty($backup_dir);

        my ($inv_fh, $inv_filename) = tempfile();
        close($inv_fh);
        push @to_unlink, $inv_filename;


        $confsec = Brackup::ConfigSection->new("TARGET:test_restore");
        $with_targetsec->($confsec);
        $confsec->add("type" => "Filesystem") unless exists $confsec->{type};
        $confsec->add("inventorydb_file" => $inv_filename);
        $confsec->add("path" => $backup_dir);
        $conf->add_section($confsec);
        $target = $conf->load_target("test_restore", testmode => 1);
    }

    ok($target, "have a target ($target)");

    my $backup = Brackup::Backup->new(
                                      root      => $root,
                                      target    => $target,
                                      savefiles => 1,
                                      );
    ok($backup, "have a backup object");

    my ($meta_fh, $meta_filename) = tempfile();
    ok(-e $meta_filename, "metafile exists");
    push @to_unlink, $meta_filename;

    ok(eval { $backup->backup($meta_filename) }, "backup succeeded");
    if ($@) {
        warn "Died running backup: $@\n";
    }
    ok(-s $meta_filename, "backup file has size");

    check_inventory_db($target, [$root->gpg_args]);

    return wantarray ? ($meta_filename, $backup, $target) : $meta_filename;
}

sub check_inventory_db {
    my ($target, $gpg_args) = @_;

    my $inv_db_file;
    eval {
        my $inv_db = $target->inventory_db                      or die 'cannot open inventory db';
        $inv_db_file = $inv_db->backing_file ? (' ' . $inv_db->backing_file) : '';

        while (my ($key, $value) = $inv_db->each) {
            my ($raw_dig, $gpg_rcpt) = split /;/, $key;
            my ($enc_dig, $enc_size, $range) = split /\s+/, $value;

            # check the stored data
            my $dataref = $target->load_chunk($enc_dig)         or die "cannot load chunk $enc_dig";
            length $$dataref == $enc_size                       or die "chunk $enc_dig has wrong size, not $enc_size";
            $enc_dig eq "sha1:".sha1_hex($$dataref)             or die "chunk $enc_dig has wrong digest";
 
            # if we are in a composite chunk, keep only the part we want
            if($range) {
                my ($from, $to) = split '-', $range;
                my $part = substr $$dataref, $from, $to-$from;
                $dataref = \$part;
            }

            # decrypt if encrypted
            my $dec_ref;
            if($gpg_rcpt =~ /^to=(.*)$/) {
                my $meta = { 'GPG-Recipient' => $1 };
                local @Brackup::GPG_ARGS = @$gpg_args;

                $dec_ref = Brackup::Decrypt::decrypt_data($dataref, meta => $meta);
            } else {
                $dec_ref = $dataref;
            }

            # check the raw data
            $raw_dig eq "sha1:".sha1_hex($$dec_ref)       or die "chunk $enc_dig has wrong raw digest";
        }
    };
    ok(!$@, "inventory db$inv_db_file is good")   or diag($@);
}

sub do_restore {
    my ($backup_file, %opts) = @_;
    my $prefix     = delete $opts{'prefix'} || "";   # default is restore everything
    my $restore_should_die = delete $opts{'restore_should_die'};
    die if %opts;
    my $restore_dir = tempdir( CLEANUP => $ENV{BRACKUP_TEST_NOCLEANUP} ? 0 : 1 );
    ok_dir_empty($restore_dir);

    my $restore = Brackup::Restore->new(
                                        to     => $restore_dir,
                                        prefix => $prefix,
                                        file   => $backup_file,
                                        );
    ok($restore, "have restore object");
    my $rv = eval { $restore->restore; };
    if ($restore_should_die) {
        ok(! defined $rv, "restore died: $@") 
            or die "restore unexpectedly succeeded";
        return;
    }
    else {
        ok($rv, "did the restore") 
            or die "restore failed: $@";
        return $restore_dir;
    }
}

sub ok_dirs_match {
    my ($after, $before) = @_;

    my $pre_ls  = dir_structure($before);
    my $post_ls = dir_structure($after);

    if ($has_diff) {
        use Data::Dumper;
        my $pre_dump = Dumper($pre_ls);
        my $post_dump = Dumper($post_ls);
        my $diff = Text::Diff::diff(\$pre_dump, \$post_dump);
        is($diff, "", "dirs match");
    } else {
        is_deeply($post_ls, $pre_ls, "dirs match");
    }
}

sub ok_files_match {
    my ($after, $before) = @_;

    my $pre_ls  = file_meta($before);
    my $post_ls = file_meta($after);

    if ($has_diff) {
        use Data::Dumper;
        my $pre_dump = Dumper($pre_ls);
        my $post_dump = Dumper($post_ls);
        my $diff = Text::Diff::diff(\$pre_dump, \$post_dump);
        is($diff, "", "files match");
    } else {
        is_deeply($post_ls, $pre_ls, "files match");
    }
}

sub ok_dir_empty {
    my $dir = shift;
    unless (-d $dir) { ok(0, "not a dir"); return; }
    opendir(my $dh, $dir) or die "failed to opendir: $!";
    is_deeply([ sort readdir($dh) ], ['.', '..'], "dir is empty: $dir");
}

sub file_meta {
    my $path = shift;
    my $st = File::stat::lstat($path);

    my $meta = {};
    $meta->{size} = $st->size unless -d $path;
    $meta->{is_file} = 1 if -f $path;
    $meta->{is_link} = 1 if -l $path;
    if ($meta->{is_link}) {
        $meta->{link} = readlink $path;
    } else {
        # we ignore these for links, since Linux doesn't let us restore anyway,
        # as Linux as no lutimes(2) syscall, as of Linux 2.6.16 at least
        $meta->{atime} = $st->atime if 0; # TODO: make tests work with atimes
        $meta->{mtime} = $st->mtime;
        $meta->{mode}  = sprintf('%#o', $st->mode & 0777);
    }

    # the gpg tests open/close the rings in the root, so
    # mtimes get bumped around or something.  the proper fix
    # is too ugly for what it's worth, so let's just ignore
    # the mtime of top-level
    delete $meta->{mtime} if $path eq ".";

    return $meta;
}

# given a directory, returns a hashref of its contentn
sub dir_structure {
    my $dir = shift;
    my %files;  # "filename" -> {metadata => ...}
    my $cwd = getcwd;
    chdir($dir) or die "Failed to chdir to $dir";

    find({
        no_chdir => 1,
        preprocess => sub { return sort @_ },
        wanted => sub {
            my $path = $_;
            $files{$path} = file_meta($path);
        },
    }, ".");

    chdir($cwd) or die "Failed to chdir back to $cwd";
    return \%files;
}

# add a random number of orphan chunks to $target
sub add_orphan_chunks {
    my ($root, $target, $orphan_chunks_count) = @_;

    for (1..$orphan_chunks_count) {
        # HACK: to avoid worse hacks, we need a pchunk to store an orphan chunk.
        # We use small segments of 'pubring-test.gpg' so that they are different 
        # than all other chunks
        my $pchunk = Brackup::PositionedChunk->new(
            file => Brackup::File->new(root => $root,
                                       path => 'pubring-test.gpg'),
            offset => $_ * 10,
            length => 10,
        );

        # no encryption, copy raw data and store schunk
        my $schunk = Brackup::StoredChunk->new($pchunk);
#       $schunk->copy_raw_data;
        $target->store_chunk($schunk);
    }
}


1;

# vim:et:sw=4