Blob Blame History Raw
# Copyright (c) 2009-2012 Zmanda Inc.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com

use Test::More tests => 29;
use File::Path;
use Data::Dumper;
use strict;
use warnings;

use lib '@amperldir@';
use Installcheck::Config;
use Amanda::Config qw( :init );
use Amanda::Changer;
use Amanda::Device qw( :constants );
use Amanda::Debug;
use Amanda::Header;
use Amanda::Xfer;
use Amanda::Taper::Scribe qw( get_splitting_args_from_config );
use Amanda::MainLoop;

# and disable Debug's die() and warn() overrides
Amanda::Debug::disable_die_override();

# put the debug messages somewhere
Amanda::Debug::dbopen("installcheck");
Installcheck::log_test_output();

# use some very small vtapes
my $volume_length = 512*1024;

my $testconf;
$testconf = Installcheck::Config->new();
$testconf->add_tapetype("TEST-TAPE", [
    "length" => ($volume_length / 1024) . " k",
]);
$testconf->write();

my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
if ($cfg_result != $CFGERR_OK) {
    my ($level, @errors) = Amanda::Config::config_errors();
    die(join "\n", @errors);
}

my $taperoot = "$Installcheck::TMP/Amanda_Taper_Scribe";

sub reset_taperoot {
    my ($nslots) = @_;

    if (-d $taperoot) {
	rmtree($taperoot);
    }
    mkpath($taperoot);

    for my $slot (1 .. $nslots) {
	mkdir("$taperoot/slot$slot")
	    or die("Could not mkdir: $!");
    }
}

# an accumulator for the sequence of events that transpire during a run
our @events;
sub event(@) {
    my $evt = [ @_ ];
    push @events, $evt;
}

sub reset_events {
    @events = ();
}

# construct a bigint
sub bi {
    Math::BigInt->new($_[0]);
}

# and similarly an Amanda::Changer::Error
sub chgerr {
    Amanda::Changer::Error->new(@_);
}

##
## Mock classes for the scribe
##

package Mock::Taperscan;
use Amanda::Device qw( :constants );
use Amanda::MainLoop;

sub new {
    my $class = shift;
    my %params = @_;
    my @slots = @{ $params{'slots'} || [] };
    my $chg =  $params{'changer'};

    # wedge in an extra device property to disable LEOM support, if requested
    if ($params{'disable_leom'}) {
	$chg->{'config'}->{'device_properties'}->{'leom'}->{'values'} = [ 0 ];
    } else {
	$chg->{'config'}->{'device_properties'}->{'leom'}->{'values'} = [ 1 ];
    }

    return bless {
	chg => $chg,
	slots => [ @slots ],
	next_or_current => "current",
    }, $class;
}

sub quit {
    my $self = shift;
}

sub make_new_tape_label {
    return "FAKELABEL";
}

sub set_write_timestamp {
    my $self = shift;
    my $write_timestamp = shift;

    $self->{'write_timestamp'} = $write_timestamp;
}

sub scan {
    my $self = shift;
    my %params = @_;
    my $result_cb = $params{'result_cb'};

    main::event("scan");

    my @slotarg = (@{$self->{'slots'}})?
	  (slot => shift @{$self->{'slots'}})
	: (relative_slot => $self->{'next_or_current'});
    $self->{'next_or_current'} = 'next';

    my $res_cb = make_cb('res_cb' => sub {
	my ($err, $res) = @_;

	my $slot = $res? $res->{'this_slot'} : "none";
	main::event("scan-finished", main::undef_or_str($err), "slot: $slot");

	if ($err) {
	    $result_cb->($err);
	} else {
	    $result_cb->(undef, $res, 'FAKELABEL', $ACCESS_WRITE);
	}
    });

    # delay this load call a little bit -- just enough so that the
    # request_volume_permission event reliably occurs first
    Amanda::MainLoop::call_after(50, sub {
	$self->{'chg'}->load(@slotarg, set_current => 1, res_cb => $res_cb);
    });
}

package Mock::Feedback;
use Amanda::Taper::Scribe;
use parent -norequire, qw( Amanda::Taper::Scribe::Feedback );
use Test::More;
use Data::Dumper;
use Installcheck::Config;

sub new {
    my $class = shift;
    my @rq_answers = @_;
    return bless {
	rq_answers => [ @rq_answers ],
    }, $class;
}

sub request_volume_permission {
    my $self = shift;
    my %params = @_;
    my $answer = shift @{$self->{'rq_answers'}};
    main::event("request_volume_permission", "answer:", $answer);
    $main::scribe->start_scan();
    $params{'perm_cb'}->(%{$answer});
}

sub scribe_ready {
    my $self = shift;
    my %params = @_;

    main::event("scribe_ready");
}

sub scribe_notif_new_tape {
    my $self = shift;
    my %params = @_;

    main::event("scribe_notif_new_tape",
	main::undef_or_str($params{'error'}), $params{'volume_label'});
}

sub scribe_notif_part_done {
    my $self = shift;
    my %params = @_;

    # this omits $duration, as it's not constant
    main::event("scribe_notif_part_done",
	$params{'partnum'}, $params{'fileno'},
	$params{'successful'}, $params{'size'});
}

sub scribe_notif_tape_done {
    my $self = shift;
    my %params = @_;

    main::event("scribe_notif_tape_done",
	$params{'volume_label'}, $params{'num_files'},
	$params{'size'});
    $params{'finished_cb'}->();
}


##
## test DevHandling
##

package main;

my $scribe;

# utility fn to stringify changer errors (earlier perls' Test::More's
# fail to do this automatically)
sub undef_or_str { (defined $_[0])? "".$_[0] : undef; }

sub run_devh {
    my ($nruns, $taperscan, $feedback) = @_;
    my $devh;
    reset_events();

    reset_taperoot($nruns);
    $main::scribe = Amanda::Taper::Scribe->new(
	taperscan => $taperscan,
	feedback => $feedback);
    $devh = $main::scribe->{'devhandling'};

    my ($start, $get_volume, $got_volume, $quit);

    $start = make_cb(start => sub {
	event("start");
	$devh->start();

	# give start() time to get the scan going before
	# calling get_volume -- this wouldn't ordinarily be
	# necessary, but we want to make sure that start() is
	# really kicking off the scan.
	$get_volume->();
    });

    my $runcount = 0;
    $get_volume = make_cb(get_volume => sub {
	if (++$runcount > $nruns) {
	    $quit->();
	    return
	}

	event("get_volume");
	$devh->get_volume(volume_cb => $got_volume);
    });

    $got_volume = make_cb(got_volume => sub {
	my ($scan_error, $config_denial_message, $error_denial_message,
	    $reservation, $volume_label, $access_mode) = @_;

	event("got_volume",
	    undef_or_str($scan_error),
	    $config_denial_message, $error_denial_message,
	    $reservation? ("slot: ".$reservation->{'this_slot'}) : undef);

	if ($scan_error or $config_denial_message or $error_denial_message) {
	    $quit->();
	    return;
	}

	$reservation->release(finished_cb => sub {
	    my ($error) = @_;
	    event("release", $error);
	    if ($error) {
		$quit->();
	    } else {
		$get_volume->();
	    }
	});
    });

    $quit = make_cb(quit => sub {
	event("quit");
	Amanda::MainLoop::quit();
    });

    $start->();
    Amanda::MainLoop::run();
}

reset_taperoot(1);
my $chg =  Amanda::Changer->new("chg-disk:$taperoot");
run_devh(3, Mock::Taperscan->new(changer => $chg), Mock::Feedback->new({allow => 1}, {allow => 1}, {allow => 1}));
is_deeply([ @events ], [
      [ 'start' ],
      [ 'scan' ], # scan starts *before* get_volume

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 }, ],
      [ 'scan-finished', undef, "slot: 1" ],
      [ 'got_volume', undef, undef, undef, "slot: 1" ],
      [ 'release', undef ],

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scan' ], # scan starts *after* request_volume_permission
      [ 'scan-finished', undef, "slot: 2" ],
      [ 'got_volume', undef, undef, undef, "slot: 2" ],
      [ 'release', undef ],

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scan' ],
      [ 'scan-finished', undef, "slot: 3" ],
      [ 'got_volume', undef, undef, undef, "slot: 3" ],
      [ 'release', undef ],

      [ 'quit' ],
    ], "correct event sequence for basic run of DevHandling")
    or diag(Dumper([@events]));

run_devh(1, Mock::Taperscan->new(changer => $chg), Mock::Feedback->new({cause => 'config', message => 'no-can-do'}));
is_deeply([ @events ], [
      [ 'start' ],
      [ 'scan' ],

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', { cause => 'config', message => 'no-can-do' } ],
      [ 'scan-finished', undef, "slot: 1" ],
      [ 'got_volume', undef, 'no-can-do', undef, undef ],

      [ 'quit' ],
    ], "correct event sequence for a run without permission")
    or diag(Dumper([@events]));

run_devh(1, Mock::Taperscan->new(slots => ["bogus"], changer => $chg), Mock::Feedback->new({allow => 1}));
is_deeply([ @events ], [
      [ 'start' ],
      [ 'scan' ],

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', { allow => 1} ],
      [ 'scan-finished', "Slot bogus not found", "slot: none" ],
      [ 'got_volume', 'Slot bogus not found', undef, undef, undef ],

      [ 'quit' ],
    ], "correct event sequence for a run with a changer error")
    or diag(Dumper([@events]));

run_devh(1, Mock::Taperscan->new(slots => ["bogus"], changer => $chg),
	    Mock::Feedback->new({cause => 'config', message => "not this time"}));
is_deeply([ @events ], [
      [ 'start' ],
      [ 'scan' ],

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', {cause => 'config', message =>'not this time'} ],
      [ 'scan-finished', "Slot bogus not found", "slot: none" ],
      [ 'got_volume', 'Slot bogus not found', 'not this time', undef, undef ],

      [ 'quit' ],
    ], "correct event sequence for a run with no permission AND a changer config denial")
    or diag(Dumper([@events]));

run_devh(1, Mock::Taperscan->new(slots => ["bogus"], changer => $chg), Mock::Feedback->new({cause => 'error', message => "frobnicator exploded!"}));
is_deeply([ @events ], [
      [ 'start' ],
      [ 'scan' ],

      [ 'get_volume' ],
      [ 'request_volume_permission', 'answer:', {cause => 'error', message => "frobnicator exploded!"} ],
      [ 'scan-finished', "Slot bogus not found", "slot: none" ],
      [ 'got_volume', 'Slot bogus not found', undef, "frobnicator exploded!", undef ],

      [ 'quit' ],
    ], "correct event sequence for a run with no permission AND a changer error")
    or diag(Dumper([@events]));

##
## test Scribe
##

sub run_scribe_xfer_async {
    my ($data_length, $scribe, %params) = @_;
    my $xfer;

    my $finished_cb = $params{'finished_cb'};
    my $steps = define_steps
	cb_ref => \$finished_cb;

    step start_scribe => sub {
	if ($params{'start_scribe'}) {
	    $scribe->start(%{ $params{'start_scribe'} },
			finished_cb => $steps->{'get_xdt'});
	} else {
	    $steps->{'get_xdt'}->();
	}
    };

    step get_xdt => sub {
	my ($err) = @_;
	die $err if $err;

	# set up a transfer
	my $xdt = $scribe->get_xfer_dest(
	    allow_split => 1,
	    max_memory => 1024 * 64,
	    part_size => (defined $params{'part_size'})? $params{'part_size'} : (1024 * 128),
            part_cache_type => $params{'part_cache_type'} || 'memory',
	    disk_cache_dirname => undef);

        die "$err" if $err;

	my $hdr = Amanda::Header->new();
	$hdr->{type} = $Amanda::Header::F_DUMPFILE;
	$hdr->{datestamp} = "20010203040506";
	$hdr->{dumplevel} = 0;
	$hdr->{compressed} = 1;
	$hdr->{name} = "localhost";
	$hdr->{disk} = "/home";
	$hdr->{program} = "INSTALLCHECK";

        $xfer = Amanda::Xfer->new([
            Amanda::Xfer::Source::Random->new($data_length, 0x5EED5),
            $xdt,
        ]);

        $xfer->start(sub {
            $scribe->handle_xmsg(@_);
        });

        $scribe->start_dump(
	    xfer => $xfer,
            dump_header => $hdr,
            dump_cb => $steps->{'dump_cb'});
    };

    step dump_cb => sub {
	my %params = @_;

	main::event("dump_cb",
	    $params{'result'},
	    [ map { "$_" } @{$params{'device_errors'}} ],
	    $params{'config_denial_message'},
	    $params{'size'});

	$finished_cb->();
    };
}

sub run_scribe_xfer {
    my ($data_length, $scribe, %params) = @_;
    $params{'finished_cb'} = \&Amanda::MainLoop::quit;
    run_scribe_xfer_async($data_length, $scribe, %params);
    Amanda::MainLoop::run();
}

sub quit_scribe {
    my ($scribe) = @_;

    my $finished_cb = make_cb(finished_cb => sub {
	my ($error) = @_;
	die "$error" if $error;

	Amanda::MainLoop::quit();
    });

    $scribe->quit(finished_cb => $finished_cb);

    Amanda::MainLoop::run();
}

my $experr;

# write less than a tape full, without LEOM

reset_taperoot(1);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(disable_leom => 1, changer => $chg),
    feedback => Mock::Feedback->new({allow => 1}));

reset_events();
run_scribe_xfer(1024*200, $main::scribe,
	    part_size => 96*1024,
	    start_scribe => { write_timestamp => "20010203040506" });

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],
      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(98304) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(98304) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(8192) ],
      [ 'dump_cb', 'DONE', [], undef, bi(204800) ],
    ], "correct event sequence for a multipart scribe of less than a whole volume, without LEOM")
    or diag(Dumper([@events]));

# pick up where we left off, writing just a tiny bit more, and then quit
reset_events();
run_scribe_xfer(1024*30, $main::scribe);

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scribe_ready' ],
      [ 'scribe_notif_part_done', bi(1), bi(4), 1, bi(30720) ],
      [ 'dump_cb', 'DONE', [], undef, bi(30720) ],
      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(4), bi(235520) ],
    ], "correct event sequence for a subsequent single-part scribe, still on the same volume")
    or diag(Dumper([@events]));

# write less than a tape full, *with* LEOM (should look the same as above)

reset_taperoot(1);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 }));

reset_events();
run_scribe_xfer(1024*200, $main::scribe,
	    part_size => 96*1024,
	    start_scribe => { write_timestamp => "20010203040506" });

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],
      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(98304) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(98304) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(8192) ],
      [ 'dump_cb', 'DONE', [], undef, bi(204800) ],
      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(204800) ],
    ], "correct event sequence for a multipart scribe of less than a whole volume, with LEOM")
    or diag(Dumper([@events]));

# start over again and try a multivolume write
#
# NOTE: the part size and volume size are such that the VFS driver produces
# ENOSPC while writing the fourth file header, rather than while writing
# data.  This is a much less common error path, so it's good to test it.

reset_taperoot(2);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(disable_leom => 1, changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 }, { allow => 1 }));

reset_events();
run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
	    start_scribe => { write_timestamp => "20010203040506" });

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],

      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(4), bi(0), 0, bi(0) ],

      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(393216) ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 2' ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],

      [ 'scribe_notif_part_done', bi(4), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(5), bi(2), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(6), bi(3), 1, bi(0) ],

      [ 'dump_cb', 'DONE', [], undef, bi(655360) ],
      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(262144) ],
    ], "correct event sequence for a multipart scribe of more than a whole volume, without LEOM" . Data::Dumper::Dumper(@events))
    or print (Dumper([@events]));

# same test, but with LEOM support

reset_taperoot(2);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 },{ allow => 1 }));

reset_events();
run_scribe_xfer(1024*520, $main::scribe,
	    start_scribe => { write_timestamp => "20010203040506" });

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],

      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(32768) ], # LEOM comes earlier than PEOM did

      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(294912) ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 2' ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],

      [ 'scribe_notif_part_done', bi(4), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(5), bi(2), 1, bi(106496) ],

      [ 'dump_cb', 'DONE', [], undef, bi(532480) ],
      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(2), bi(237568) ],
    ], "correct event sequence for a multipart scribe of more than a whole volume, with LEOM")
    or print (Dumper([@events]));

# now a multivolume write where the second volume gives a changer error

reset_taperoot(1);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(slots => ["1", "bogus"], disable_leom => 1, changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 },{ allow => 1 }));

reset_events();
run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
	    start_scribe => { write_timestamp => "20010203040507" });

quit_scribe($main::scribe);

$experr = 'Slot bogus not found';
is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],

      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(4), bi(0), 0, bi(0) ],

      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(393216) ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scan' ],
      [ 'scan-finished', $experr, 'slot: none' ],
      [ 'scribe_notif_new_tape', $experr, undef ],

      [ 'dump_cb', 'PARTIAL', [$experr], undef, bi(393216) ],
      # (no scribe_notif_tape_done)
    ], "correct event sequence for a multivolume scribe with no second vol, without LEOM")
    or print (Dumper([@events]));

reset_taperoot(1);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(slots => ["1", "bogus"], changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 }, { allow => 1 }));

reset_events();
run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
	    start_scribe => { write_timestamp => "20010203040507" });

quit_scribe($main::scribe);

$experr = 'Slot bogus not found';
is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],

      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(32768) ], # LEOM comes long before PEOM

      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(294912) ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scan' ],
      [ 'scan-finished', $experr, 'slot: none' ],
      [ 'scribe_notif_new_tape', $experr, undef ],

      [ 'dump_cb', 'PARTIAL', [$experr], undef, bi(294912) ],
      # (no scribe_notif_tape_done)
    ], "correct event sequence for a multivolume scribe with no second vol, with LEOM")
    or print (Dumper([@events]));

# now a multivolume write where the second volume does not have permission

reset_taperoot(2);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 }, { cause => 'config', message => "sorry!" }));

reset_events();
run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
	    start_scribe => { write_timestamp => "20010203040507" });

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],

      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
      [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(32768) ],

      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(294912) ],
      [ 'request_volume_permission', 'answer:', { cause => 'config', message => "sorry!" } ],
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 2' ],

      [ 'dump_cb', 'PARTIAL', [], "sorry!", bi(294912) ],
    ], "correct event sequence for a multivolume scribe with next vol denied")
    or print (Dumper([@events]));

# a non-splitting xfer on a single volume

reset_taperoot(2);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(disable_leom => 1, changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 }));

reset_events();
run_scribe_xfer(1024*300, $main::scribe, part_size => 0, part_cache_type => 'none',
	    start_scribe => { write_timestamp => "20010203040506" });

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],
      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(307200) ],
      [ 'dump_cb', 'DONE', [], undef, bi(307200) ],
      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(1), bi(307200) ],
    ], "correct event sequence for a non-splitting scribe of less than a whole volume, without LEOM")
    or diag(Dumper([@events]));

reset_taperoot(2);
$main::scribe = Amanda::Taper::Scribe->new(
    taperscan => Mock::Taperscan->new(changer => $chg),
    feedback => Mock::Feedback->new({ allow => 1 }));
$Amanda::Config::debug_taper = 9;
reset_events();
run_scribe_xfer(1024*300, $main::scribe, part_size => 0, part_cache_type => 'none',
	    start_scribe => { write_timestamp => "20010203040506" });

quit_scribe($main::scribe);

is_deeply([ @events ], [
      [ 'scan' ],
      [ 'scan-finished', undef, 'slot: 1' ],
      [ 'request_volume_permission', 'answer:', { allow => 1 } ],
      [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
      [ 'scribe_ready' ],
      [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(307200) ],
      [ 'dump_cb', 'DONE', [], undef, bi(307200) ],
      [ 'scribe_notif_tape_done', 'FAKELABEL', bi(1), bi(307200) ],
    ], "correct event sequence for a non-splitting scribe of less than a whole volume, with LEOM")
    or diag(Dumper([@events]));

# DirectTCP support is tested through the taper installcheck

# test get_splitting_args_from_config thoroughly
my $maxint64 = Math::BigInt->new("9223372036854775808");

is_deeply(
    { get_splitting_args_from_config(
    ) },
    { allow_split => 0 },
    "default is only allow_split set to 0");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => 0,
	dle_split_diskbuffer => $Installcheck::TMP,
	dle_fallback_splitsize => 100,
    ) },
    { allow_split => 0, part_size => 0, part_cache_type => 'none' },
    "tape_splitsize = 0 indicates no splitting");

is_deeply(
    { get_splitting_args_from_config(
	dle_allow_split => 0,
	part_size => 100,
	part_cache_dir => "/tmp",
    ) },
    { allow_split => 0 },
    "default if dle_allow_split is false, no splitting");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => 200,
	dle_fallback_splitsize => 150,
    ) },
    { allow_split => 1,part_cache_type => 'memory', part_size => 200, part_cache_max_size => 150 },
    "when cache_inform is available, tape_splitsize is used, not fallback");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => 200,
    ) },
    { allow_split => 1, part_size => 200, part_cache_type => 'memory', part_cache_max_size => 1024*1024*10, },
    "no split_diskbuffer and no fallback_splitsize, fall back to default (10M)");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => 200,
	dle_split_diskbuffer => "$Installcheck::TMP/does!not!exist!",
	dle_fallback_splitsize => 150,
    ) },
    { allow_split => 1, part_size => 200, part_cache_type => 'memory', part_cache_max_size => 150 },
    "invalid split_diskbuffer => fall back (silently)");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => 200,
	dle_split_diskbuffer => "$Installcheck::TMP/does!not!exist!",
    ) },
    { allow_split => 1, part_size => 200, part_cache_type => 'memory', part_cache_max_size => 1024*1024*10 },
    ".. even to the default fallback (10M)");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => $maxint64,
	dle_split_diskbuffer => "$Installcheck::TMP",
	dle_fallback_splitsize => 250,
    ) },
    { allow_split => 1, part_size => $maxint64, part_cache_type => 'memory', part_cache_max_size => 250,
      warning => "falling back to memory buffer for splitting: " .
		 "insufficient space in disk cache directory" },
    "not enough space in split_diskbuffer => fall back (with warning)");

is_deeply(
    { get_splitting_args_from_config(
	can_cache_inform => 0,
	dle_tape_splitsize => 200,
	dle_split_diskbuffer => "$Installcheck::TMP",
	dle_fallback_splitsize => 150,
    ) },
    { allow_split => 1, part_size => 200, part_cache_type => 'disk', part_cache_dir => "$Installcheck::TMP" },
    "if split_diskbuffer exists and splitsize is nonzero, use it");

is_deeply(
    { get_splitting_args_from_config(
	dle_tape_splitsize => 0,
	dle_split_diskbuffer => "$Installcheck::TMP",
	dle_fallback_splitsize => 250,
    ) },
    { allow_split => 0, part_size => 0, part_cache_type => 'none' },
    ".. but if splitsize is zero, no splitting");

is_deeply(
    { get_splitting_args_from_config(
	dle_split_diskbuffer => "$Installcheck::TMP",
	dle_fallback_splitsize => 250,
    ) },
    { allow_split => 0, part_size => 0, part_cache_type => 'none' },
    ".. and if splitsize is missing, no splitting");

is_deeply(
    { get_splitting_args_from_config(
	part_size => 300,
	part_cache_type => 'none',
    ) },
    { allow_split => 1, part_size => 300, part_cache_type => 'none' },
    "part_* parameters handled correctly when missing");

is_deeply(
    { get_splitting_args_from_config(
	part_size => 300,
	part_cache_type => 'disk',
	part_cache_dir => $Installcheck::TMP,
	part_cache_max_size => 250,
    ) },
    { allow_split => 1, part_size => 300, part_cache_type => 'disk',
      part_cache_dir => $Installcheck::TMP, part_cache_max_size => 250, },
    "part_* parameters handled correctly when specified");

is_deeply(
    { get_splitting_args_from_config(
	part_size => 300,
	part_cache_type => 'disk',
	part_cache_dir => "$Installcheck::TMP/does!not!exist!",
	part_cache_max_size => 250,
    ) },
    { allow_split => 1, part_size => 300, part_cache_type => 'none',
      part_cache_max_size => 250,
      warning => "part-cache-dir '$Installcheck::TMP/does!not!exist! does not exist; "
	       . "using part cache type 'none'"},
    "part_* parameters handled correctly when specified");

$chg->quit();
rmtree($taperoot);