Imported Upstream version 3.3.3
[debian/amanda] / installcheck / Amanda_Taper_Scribe.pl
1 # Copyright (c) 2009-2012 Zmanda Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use Test::More tests => 29;
21 use File::Path;
22 use Data::Dumper;
23 use strict;
24 use warnings;
25
26 use lib "@amperldir@";
27 use Installcheck::Config;
28 use Amanda::Config qw( :init );
29 use Amanda::Changer;
30 use Amanda::Device qw( :constants );
31 use Amanda::Debug;
32 use Amanda::Header;
33 use Amanda::Xfer;
34 use Amanda::Taper::Scribe qw( get_splitting_args_from_config );
35 use Amanda::MainLoop;
36
37 # and disable Debug's die() and warn() overrides
38 Amanda::Debug::disable_die_override();
39
40 # put the debug messages somewhere
41 Amanda::Debug::dbopen("installcheck");
42 Installcheck::log_test_output();
43
44 # use some very small vtapes
45 my $volume_length = 512*1024;
46
47 my $testconf;
48 $testconf = Installcheck::Config->new();
49 $testconf->add_tapetype("TEST-TAPE", [
50     "length" => ($volume_length / 1024) . " k",
51 ]);
52 $testconf->write();
53
54 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
55 if ($cfg_result != $CFGERR_OK) {
56     my ($level, @errors) = Amanda::Config::config_errors();
57     die(join "\n", @errors);
58 }
59
60 my $taperoot = "$Installcheck::TMP/Amanda_Taper_Scribe";
61
62 sub reset_taperoot {
63     my ($nslots) = @_;
64
65     if (-d $taperoot) {
66         rmtree($taperoot);
67     }
68     mkpath($taperoot);
69
70     for my $slot (1 .. $nslots) {
71         mkdir("$taperoot/slot$slot")
72             or die("Could not mkdir: $!");
73     }
74 }
75
76 # an accumulator for the sequence of events that transpire during a run
77 our @events;
78 sub event(@) {
79     my $evt = [ @_ ];
80     push @events, $evt;
81 }
82
83 sub reset_events {
84     @events = ();
85 }
86
87 # construct a bigint
88 sub bi {
89     Math::BigInt->new($_[0]);
90 }
91
92 # and similarly an Amanda::Changer::Error
93 sub chgerr {
94     Amanda::Changer::Error->new(@_);
95 }
96
97 ##
98 ## Mock classes for the scribe
99 ##
100
101 package Mock::Taperscan;
102 use Amanda::Device qw( :constants );
103 use Amanda::MainLoop;
104
105 sub new {
106     my $class = shift;
107     my %params = @_;
108     my @slots = @{ $params{'slots'} || [] };
109     my $chg =  $params{'changer'};
110
111     # wedge in an extra device property to disable LEOM support, if requested
112     if ($params{'disable_leom'}) {
113         $chg->{'config'}->{'device_properties'}->{'leom'}->{'values'} = [ 0 ];
114     } else {
115         $chg->{'config'}->{'device_properties'}->{'leom'}->{'values'} = [ 1 ];
116     }
117
118     return bless {
119         chg => $chg,
120         slots => [ @slots ],
121         next_or_current => "current",
122     }, $class;
123 }
124
125 sub quit {
126     my $self = shift;
127 }
128
129 sub make_new_tape_label {
130     return "FAKELABEL";
131 }
132
133 sub scan {
134     my $self = shift;
135     my %params = @_;
136     my $result_cb = $params{'result_cb'};
137
138     main::event("scan");
139
140     my @slotarg = (@{$self->{'slots'}})?
141           (slot => shift @{$self->{'slots'}})
142         : (relative_slot => $self->{'next_or_current'});
143     $self->{'next_or_current'} = 'next';
144
145     my $res_cb = make_cb('res_cb' => sub {
146         my ($err, $res) = @_;
147
148         my $slot = $res? $res->{'this_slot'} : "none";
149         main::event("scan-finished", main::undef_or_str($err), "slot: $slot");
150
151         if ($err) {
152             $result_cb->($err);
153         } else {
154             $result_cb->(undef, $res, 'FAKELABEL', $ACCESS_WRITE);
155         }
156     });
157
158     # delay this load call a little bit -- just enough so that the
159     # request_volume_permission event reliably occurs first
160     Amanda::MainLoop::call_after(50, sub {
161         $self->{'chg'}->load(@slotarg, set_current => 1, res_cb => $res_cb);
162     });
163 }
164
165 package Mock::Feedback;
166 use base qw( Amanda::Taper::Scribe::Feedback );
167 use Test::More;
168 use Data::Dumper;
169 use Installcheck::Config;
170
171 sub new {
172     my $class = shift;
173     my @rq_answers = @_;
174     return bless {
175         rq_answers => [ @rq_answers ],
176     }, $class;
177 }
178
179 sub request_volume_permission {
180     my $self = shift;
181     my %params = @_;
182     my $answer = shift @{$self->{'rq_answers'}};
183     main::event("request_volume_permission", "answer:", $answer);
184     $main::scribe->start_scan();
185     $params{'perm_cb'}->(%{$answer});
186 }
187
188 sub scribe_notif_new_tape {
189     my $self = shift;
190     my %params = @_;
191
192     main::event("scribe_notif_new_tape",
193         main::undef_or_str($params{'error'}), $params{'volume_label'});
194 }
195
196 sub scribe_notif_part_done {
197     my $self = shift;
198     my %params = @_;
199
200     # this omits $duration, as it's not constant
201     main::event("scribe_notif_part_done",
202         $params{'partnum'}, $params{'fileno'},
203         $params{'successful'}, $params{'size'});
204 }
205
206 sub scribe_notif_tape_done {
207     my $self = shift;
208     my %params = @_;
209
210     main::event("scribe_notif_tape_done",
211         $params{'volume_label'}, $params{'num_files'},
212         $params{'size'});
213     $params{'finished_cb'}->();
214 }
215
216
217 ##
218 ## test DevHandling
219 ##
220
221 package main;
222
223 my $scribe;
224
225 # utility fn to stringify changer errors (earlier perls' Test::More's
226 # fail to do this automatically)
227 sub undef_or_str { (defined $_[0])? "".$_[0] : undef; }
228
229 sub run_devh {
230     my ($nruns, $taperscan, $feedback) = @_;
231     my $devh;
232     reset_events();
233
234     reset_taperoot($nruns);
235     $main::scribe = Amanda::Taper::Scribe->new(
236         taperscan => $taperscan,
237         feedback => $feedback);
238     $devh = $main::scribe->{'devhandling'};
239
240     my ($start, $get_volume, $got_volume, $quit);
241
242     $start = make_cb(start => sub {
243         event("start");
244         $devh->start();
245
246         # give start() time to get the scan going before
247         # calling get_volume -- this wouldn't ordinarily be
248         # necessary, but we want to make sure that start() is
249         # really kicking off the scan.
250         $get_volume->();
251     });
252
253     my $runcount = 0;
254     $get_volume = make_cb(get_volume => sub {
255         if (++$runcount > $nruns) {
256             $quit->();
257             return
258         }
259
260         event("get_volume");
261         $devh->get_volume(volume_cb => $got_volume);
262     });
263
264     $got_volume = make_cb(got_volume => sub {
265         my ($scan_error, $config_denial_message, $error_denial_message,
266             $reservation, $volume_label, $access_mode) = @_;
267
268         event("got_volume",
269             undef_or_str($scan_error),
270             $config_denial_message, $error_denial_message,
271             $reservation? ("slot: ".$reservation->{'this_slot'}) : undef);
272
273         if ($scan_error or $config_denial_message or $error_denial_message) {
274             $quit->();
275             return;
276         }
277
278         $reservation->release(finished_cb => sub {
279             my ($error) = @_;
280             event("release", $error);
281             if ($error) {
282                 $quit->();
283             } else {
284                 $get_volume->();
285             }
286         });
287     });
288
289     $quit = make_cb(quit => sub {
290         event("quit");
291         Amanda::MainLoop::quit();
292     });
293
294     $start->();
295     Amanda::MainLoop::run();
296 }
297
298 reset_taperoot(1);
299 my $chg =  Amanda::Changer->new("chg-disk:$taperoot");
300 run_devh(3, Mock::Taperscan->new(changer => $chg), Mock::Feedback->new({allow => 1}, {allow => 1}, {allow => 1}));
301 is_deeply([ @events ], [
302       [ 'start' ],
303       [ 'scan' ], # scan starts *before* get_volume
304
305       [ 'get_volume' ],
306       [ 'request_volume_permission', 'answer:', { allow => 1 }, ],
307       [ 'scan-finished', undef, "slot: 1" ],
308       [ 'got_volume', undef, undef, undef, "slot: 1" ],
309       [ 'release', undef ],
310
311       [ 'get_volume' ],
312       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
313       [ 'scan' ], # scan starts *after* request_volume_permission
314       [ 'scan-finished', undef, "slot: 2" ],
315       [ 'got_volume', undef, undef, undef, "slot: 2" ],
316       [ 'release', undef ],
317
318       [ 'get_volume' ],
319       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
320       [ 'scan' ],
321       [ 'scan-finished', undef, "slot: 3" ],
322       [ 'got_volume', undef, undef, undef, "slot: 3" ],
323       [ 'release', undef ],
324
325       [ 'quit' ],
326     ], "correct event sequence for basic run of DevHandling")
327     or diag(Dumper([@events]));
328
329 run_devh(1, Mock::Taperscan->new(changer => $chg), Mock::Feedback->new({cause => 'config', message => 'no-can-do'}));
330 is_deeply([ @events ], [
331       [ 'start' ],
332       [ 'scan' ],
333
334       [ 'get_volume' ],
335       [ 'request_volume_permission', 'answer:', { cause => 'config', message => 'no-can-do' } ],
336       [ 'scan-finished', undef, "slot: 1" ],
337       [ 'got_volume', undef, 'no-can-do', undef, undef ],
338
339       [ 'quit' ],
340     ], "correct event sequence for a run without permission")
341     or diag(Dumper([@events]));
342
343 run_devh(1, Mock::Taperscan->new(slots => ["bogus"], changer => $chg), Mock::Feedback->new({allow => 1}));
344 is_deeply([ @events ], [
345       [ 'start' ],
346       [ 'scan' ],
347
348       [ 'get_volume' ],
349       [ 'request_volume_permission', 'answer:', { allow => 1} ],
350       [ 'scan-finished', "Slot bogus not found", "slot: none" ],
351       [ 'got_volume', 'Slot bogus not found', undef, undef, undef ],
352
353       [ 'quit' ],
354     ], "correct event sequence for a run with a changer error")
355     or diag(Dumper([@events]));
356
357 run_devh(1, Mock::Taperscan->new(slots => ["bogus"], changer => $chg),
358             Mock::Feedback->new({cause => 'config', message => "not this time"}));
359 is_deeply([ @events ], [
360       [ 'start' ],
361       [ 'scan' ],
362
363       [ 'get_volume' ],
364       [ 'request_volume_permission', 'answer:', {cause => 'config', message =>'not this time'} ],
365       [ 'scan-finished', "Slot bogus not found", "slot: none" ],
366       [ 'got_volume', 'Slot bogus not found', 'not this time', undef, undef ],
367
368       [ 'quit' ],
369     ], "correct event sequence for a run with no permission AND a changer config denial")
370     or diag(Dumper([@events]));
371
372 run_devh(1, Mock::Taperscan->new(slots => ["bogus"], changer => $chg), Mock::Feedback->new({cause => 'error', message => "frobnicator exploded!"}));
373 is_deeply([ @events ], [
374       [ 'start' ],
375       [ 'scan' ],
376
377       [ 'get_volume' ],
378       [ 'request_volume_permission', 'answer:', {cause => 'error', message => "frobnicator exploded!"} ],
379       [ 'scan-finished', "Slot bogus not found", "slot: none" ],
380       [ 'got_volume', 'Slot bogus not found', undef, "frobnicator exploded!", undef ],
381
382       [ 'quit' ],
383     ], "correct event sequence for a run with no permission AND a changer error")
384     or diag(Dumper([@events]));
385
386 ##
387 ## test Scribe
388 ##
389
390 sub run_scribe_xfer_async {
391     my ($data_length, $scribe, %params) = @_;
392     my $xfer;
393
394     my $finished_cb = $params{'finished_cb'};
395     my $steps = define_steps
396         cb_ref => \$finished_cb;
397
398     step start_scribe => sub {
399         if ($params{'start_scribe'}) {
400             $scribe->start(%{ $params{'start_scribe'} },
401                         finished_cb => $steps->{'get_xdt'});
402         } else {
403             $steps->{'get_xdt'}->();
404         }
405     };
406
407     step get_xdt => sub {
408         my ($err) = @_;
409         die $err if $err;
410
411         # set up a transfer
412         my $xdt = $scribe->get_xfer_dest(
413             allow_split => 1,
414             max_memory => 1024 * 64,
415             part_size => (defined $params{'part_size'})? $params{'part_size'} : (1024 * 128),
416             part_cache_type => $params{'part_cache_type'} || 'memory',
417             disk_cache_dirname => undef);
418
419         die "$err" if $err;
420
421         my $hdr = Amanda::Header->new();
422         $hdr->{type} = $Amanda::Header::F_DUMPFILE;
423         $hdr->{datestamp} = "20010203040506";
424         $hdr->{dumplevel} = 0;
425         $hdr->{compressed} = 1;
426         $hdr->{name} = "localhost";
427         $hdr->{disk} = "/home";
428         $hdr->{program} = "INSTALLCHECK";
429
430         $xfer = Amanda::Xfer->new([
431             Amanda::Xfer::Source::Random->new($data_length, 0x5EED5),
432             $xdt,
433         ]);
434
435         $xfer->start(sub {
436             $scribe->handle_xmsg(@_);
437         });
438
439         $scribe->start_dump(
440             xfer => $xfer,
441             dump_header => $hdr,
442             dump_cb => $steps->{'dump_cb'});
443     };
444
445     step dump_cb => sub {
446         my %params = @_;
447
448         main::event("dump_cb",
449             $params{'result'},
450             [ map { "$_" } @{$params{'device_errors'}} ],
451             $params{'config_denial_message'},
452             $params{'size'});
453
454         $finished_cb->();
455     };
456 }
457
458 sub run_scribe_xfer {
459     my ($data_length, $scribe, %params) = @_;
460     $params{'finished_cb'} = \&Amanda::MainLoop::quit;
461     run_scribe_xfer_async($data_length, $scribe, %params);
462     Amanda::MainLoop::run();
463 }
464
465 sub quit_scribe {
466     my ($scribe) = @_;
467
468     my $finished_cb = make_cb(finished_cb => sub {
469         my ($error) = @_;
470         die "$error" if $error;
471
472         Amanda::MainLoop::quit();
473     });
474
475     $scribe->quit(finished_cb => $finished_cb);
476
477     Amanda::MainLoop::run();
478 }
479
480 my $experr;
481
482 # write less than a tape full, without LEOM
483
484 reset_taperoot(1);
485 $main::scribe = Amanda::Taper::Scribe->new(
486     taperscan => Mock::Taperscan->new(disable_leom => 1, changer => $chg),
487     feedback => Mock::Feedback->new({allow => 1}));
488
489 reset_events();
490 run_scribe_xfer(1024*200, $main::scribe,
491             part_size => 96*1024,
492             start_scribe => { write_timestamp => "20010203040506" });
493
494 is_deeply([ @events ], [
495       [ 'scan' ],
496       [ 'scan-finished', undef, 'slot: 1' ],
497       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
498       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
499       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(98304) ],
500       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(98304) ],
501       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(8192) ],
502       [ 'dump_cb', 'DONE', [], undef, bi(204800) ],
503     ], "correct event sequence for a multipart scribe of less than a whole volume, without LEOM")
504     or diag(Dumper([@events]));
505
506 # pick up where we left off, writing just a tiny bit more, and then quit
507 reset_events();
508 run_scribe_xfer(1024*30, $main::scribe);
509
510 quit_scribe($main::scribe);
511
512 is_deeply([ @events ], [
513       [ 'scribe_notif_part_done', bi(1), bi(4), 1, bi(30720) ],
514       [ 'dump_cb', 'DONE', [], undef, bi(30720) ],
515       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(4), bi(235520) ],
516     ], "correct event sequence for a subsequent single-part scribe, still on the same volume")
517     or diag(Dumper([@events]));
518
519 # write less than a tape full, *with* LEOM (should look the same as above)
520
521 reset_taperoot(1);
522 $main::scribe = Amanda::Taper::Scribe->new(
523     taperscan => Mock::Taperscan->new(changer => $chg),
524     feedback => Mock::Feedback->new({ allow => 1 }));
525
526 reset_events();
527 run_scribe_xfer(1024*200, $main::scribe,
528             part_size => 96*1024,
529             start_scribe => { write_timestamp => "20010203040506" });
530
531 quit_scribe($main::scribe);
532
533 is_deeply([ @events ], [
534       [ 'scan' ],
535       [ 'scan-finished', undef, 'slot: 1' ],
536       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
537       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
538       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(98304) ],
539       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(98304) ],
540       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(8192) ],
541       [ 'dump_cb', 'DONE', [], undef, bi(204800) ],
542       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(204800) ],
543     ], "correct event sequence for a multipart scribe of less than a whole volume, with LEOM")
544     or diag(Dumper([@events]));
545
546 # start over again and try a multivolume write
547 #
548 # NOTE: the part size and volume size are such that the VFS driver produces
549 # ENOSPC while writing the fourth file header, rather than while writing
550 # data.  This is a much less common error path, so it's good to test it.
551
552 reset_taperoot(2);
553 $main::scribe = Amanda::Taper::Scribe->new(
554     taperscan => Mock::Taperscan->new(disable_leom => 1, changer => $chg),
555     feedback => Mock::Feedback->new({ allow => 1 }, { allow => 1 }));
556
557 reset_events();
558 run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
559             start_scribe => { write_timestamp => "20010203040506" });
560
561 quit_scribe($main::scribe);
562
563 is_deeply([ @events ], [
564       [ 'scan' ],
565       [ 'scan-finished', undef, 'slot: 1' ],
566       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
567       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
568
569       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
570       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
571       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(131072) ],
572       [ 'scribe_notif_part_done', bi(4), bi(0), 0, bi(0) ],
573
574       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(393216) ],
575       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
576       [ 'scan' ],
577       [ 'scan-finished', undef, 'slot: 2' ],
578       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
579
580       [ 'scribe_notif_part_done', bi(4), bi(1), 1, bi(131072) ],
581       [ 'scribe_notif_part_done', bi(5), bi(2), 1, bi(131072) ],
582       # empty part is written but not notified, although it is counted
583       # in scribe_notif_tape_done
584
585       [ 'dump_cb', 'DONE', [], undef, bi(655360) ],
586       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(262144) ],
587     ], "correct event sequence for a multipart scribe of more than a whole volume, without LEOM" . Data::Dumper::Dumper(@events))
588     or print (Dumper([@events]));
589
590 # same test, but with LEOM support
591
592 reset_taperoot(2);
593 $main::scribe = Amanda::Taper::Scribe->new(
594     taperscan => Mock::Taperscan->new(changer => $chg),
595     feedback => Mock::Feedback->new({ allow => 1 },{ allow => 1 }));
596
597 reset_events();
598 run_scribe_xfer(1024*520, $main::scribe,
599             start_scribe => { write_timestamp => "20010203040506" });
600
601 quit_scribe($main::scribe);
602
603 is_deeply([ @events ], [
604       [ 'scan' ],
605       [ 'scan-finished', undef, 'slot: 1' ],
606       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
607       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
608
609       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
610       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
611       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(32768) ], # LEOM comes earlier than PEOM did
612
613       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(294912) ],
614       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
615       [ 'scan' ],
616       [ 'scan-finished', undef, 'slot: 2' ],
617       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
618
619       [ 'scribe_notif_part_done', bi(4), bi(1), 1, bi(131072) ],
620       [ 'scribe_notif_part_done', bi(5), bi(2), 1, bi(106496) ],
621
622       [ 'dump_cb', 'DONE', [], undef, bi(532480) ],
623       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(2), bi(237568) ],
624     ], "correct event sequence for a multipart scribe of more than a whole volume, with LEOM")
625     or print (Dumper([@events]));
626
627 # now a multivolume write where the second volume gives a changer error
628
629 reset_taperoot(1);
630 $main::scribe = Amanda::Taper::Scribe->new(
631     taperscan => Mock::Taperscan->new(slots => ["1", "bogus"], disable_leom => 1, changer => $chg),
632     feedback => Mock::Feedback->new({ allow => 1 },{ allow => 1 }));
633
634 reset_events();
635 run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
636             start_scribe => { write_timestamp => "20010203040507" });
637
638 quit_scribe($main::scribe);
639
640 $experr = 'Slot bogus not found';
641 is_deeply([ @events ], [
642       [ 'scan' ],
643       [ 'scan-finished', undef, 'slot: 1' ],
644       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
645       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
646
647       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
648       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
649       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(131072) ],
650       [ 'scribe_notif_part_done', bi(4), bi(0), 0, bi(0) ],
651
652       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(393216) ],
653       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
654       [ 'scan' ],
655       [ 'scan-finished', $experr, 'slot: none' ],
656       [ 'scribe_notif_new_tape', $experr, undef ],
657
658       [ 'dump_cb', 'PARTIAL', [$experr], undef, bi(393216) ],
659       # (no scribe_notif_tape_done)
660     ], "correct event sequence for a multivolume scribe with no second vol, without LEOM")
661     or print (Dumper([@events]));
662
663 reset_taperoot(1);
664 $main::scribe = Amanda::Taper::Scribe->new(
665     taperscan => Mock::Taperscan->new(slots => ["1", "bogus"], changer => $chg),
666     feedback => Mock::Feedback->new({ allow => 1 }, { allow => 1 }));
667
668 reset_events();
669 run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
670             start_scribe => { write_timestamp => "20010203040507" });
671
672 quit_scribe($main::scribe);
673
674 $experr = 'Slot bogus not found';
675 is_deeply([ @events ], [
676       [ 'scan' ],
677       [ 'scan-finished', undef, 'slot: 1' ],
678       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
679       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
680
681       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
682       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
683       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(32768) ], # LEOM comes long before PEOM
684
685       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(294912) ],
686       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
687       [ 'scan' ],
688       [ 'scan-finished', $experr, 'slot: none' ],
689       [ 'scribe_notif_new_tape', $experr, undef ],
690
691       [ 'dump_cb', 'PARTIAL', [$experr], undef, bi(294912) ],
692       # (no scribe_notif_tape_done)
693     ], "correct event sequence for a multivolume scribe with no second vol, with LEOM")
694     or print (Dumper([@events]));
695
696 # now a multivolume write where the second volume does not have permission
697
698 reset_taperoot(2);
699 $main::scribe = Amanda::Taper::Scribe->new(
700     taperscan => Mock::Taperscan->new(changer => $chg),
701     feedback => Mock::Feedback->new({ allow => 1 }, { cause => 'config', message => "sorry!" }));
702
703 reset_events();
704 run_scribe_xfer($volume_length + $volume_length / 4, $main::scribe,
705             start_scribe => { write_timestamp => "20010203040507" });
706
707 quit_scribe($main::scribe);
708
709 is_deeply([ @events ], [
710       [ 'scan' ],
711       [ 'scan-finished', undef, 'slot: 1' ],
712       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
713       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
714
715       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(131072) ],
716       [ 'scribe_notif_part_done', bi(2), bi(2), 1, bi(131072) ],
717       [ 'scribe_notif_part_done', bi(3), bi(3), 1, bi(32768) ],
718
719       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(3), bi(294912) ],
720       [ 'request_volume_permission', 'answer:', { cause => 'config', message => "sorry!" } ],
721       [ 'scan' ],
722       [ 'scan-finished', undef, 'slot: 2' ],
723
724       [ 'dump_cb', 'PARTIAL', [], "sorry!", bi(294912) ],
725     ], "correct event sequence for a multivolume scribe with next vol denied")
726     or print (Dumper([@events]));
727
728 # a non-splitting xfer on a single volume
729
730 reset_taperoot(2);
731 $main::scribe = Amanda::Taper::Scribe->new(
732     taperscan => Mock::Taperscan->new(disable_leom => 1, changer => $chg),
733     feedback => Mock::Feedback->new({ allow => 1 }));
734
735 reset_events();
736 run_scribe_xfer(1024*300, $main::scribe, part_size => 0, part_cache_type => 'none',
737             start_scribe => { write_timestamp => "20010203040506" });
738
739 quit_scribe($main::scribe);
740
741 is_deeply([ @events ], [
742       [ 'scan' ],
743       [ 'scan-finished', undef, 'slot: 1' ],
744       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
745       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
746       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(307200) ],
747       [ 'dump_cb', 'DONE', [], undef, bi(307200) ],
748       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(1), bi(307200) ],
749     ], "correct event sequence for a non-splitting scribe of less than a whole volume, without LEOM")
750     or diag(Dumper([@events]));
751
752 reset_taperoot(2);
753 $main::scribe = Amanda::Taper::Scribe->new(
754     taperscan => Mock::Taperscan->new(changer => $chg),
755     feedback => Mock::Feedback->new({ allow => 1 }));
756 $Amanda::Config::debug_taper = 9;
757 reset_events();
758 run_scribe_xfer(1024*300, $main::scribe, part_size => 0, part_cache_type => 'none',
759             start_scribe => { write_timestamp => "20010203040506" });
760
761 quit_scribe($main::scribe);
762
763 is_deeply([ @events ], [
764       [ 'scan' ],
765       [ 'scan-finished', undef, 'slot: 1' ],
766       [ 'request_volume_permission', 'answer:', { allow => 1 } ],
767       [ 'scribe_notif_new_tape', undef, 'FAKELABEL' ],
768       [ 'scribe_notif_part_done', bi(1), bi(1), 1, bi(307200) ],
769       [ 'dump_cb', 'DONE', [], undef, bi(307200) ],
770       [ 'scribe_notif_tape_done', 'FAKELABEL', bi(1), bi(307200) ],
771     ], "correct event sequence for a non-splitting scribe of less than a whole volume, with LEOM")
772     or diag(Dumper([@events]));
773
774 # DirectTCP support is tested through the taper installcheck
775
776 # test get_splitting_args_from_config thoroughly
777 my $maxint64 = Math::BigInt->new("9223372036854775808");
778
779 is_deeply(
780     { get_splitting_args_from_config(
781     ) },
782     { allow_split => 0 },
783     "default is only allow_split set to 0");
784
785 is_deeply(
786     { get_splitting_args_from_config(
787         dle_tape_splitsize => 0,
788         dle_split_diskbuffer => $Installcheck::TMP,
789         dle_fallback_splitsize => 100,
790     ) },
791     { allow_split => 0, part_size => 0, part_cache_type => 'none' },
792     "tape_splitsize = 0 indicates no splitting");
793
794 is_deeply(
795     { get_splitting_args_from_config(
796         dle_allow_split => 0,
797         part_size => 100,
798         part_cache_dir => "/tmp",
799     ) },
800     { allow_split => 0 },
801     "default if dle_allow_split is false, no splitting");
802
803 is_deeply(
804     { get_splitting_args_from_config(
805         dle_tape_splitsize => 200,
806         dle_fallback_splitsize => 150,
807     ) },
808     { allow_split => 1,part_cache_type => 'memory', part_size => 200, part_cache_max_size => 150 },
809     "when cache_inform is available, tape_splitsize is used, not fallback");
810
811 is_deeply(
812     { get_splitting_args_from_config(
813         dle_tape_splitsize => 200,
814     ) },
815     { allow_split => 1, part_size => 200, part_cache_type => 'memory', part_cache_max_size => 1024*1024*10, },
816     "no split_diskbuffer and no fallback_splitsize, fall back to default (10M)");
817
818 is_deeply(
819     { get_splitting_args_from_config(
820         dle_tape_splitsize => 200,
821         dle_split_diskbuffer => "$Installcheck::TMP/does!not!exist!",
822         dle_fallback_splitsize => 150,
823     ) },
824     { allow_split => 1, part_size => 200, part_cache_type => 'memory', part_cache_max_size => 150 },
825     "invalid split_diskbuffer => fall back (silently)");
826
827 is_deeply(
828     { get_splitting_args_from_config(
829         dle_tape_splitsize => 200,
830         dle_split_diskbuffer => "$Installcheck::TMP/does!not!exist!",
831     ) },
832     { allow_split => 1, part_size => 200, part_cache_type => 'memory', part_cache_max_size => 1024*1024*10 },
833     ".. even to the default fallback (10M)");
834
835 is_deeply(
836     { get_splitting_args_from_config(
837         dle_tape_splitsize => $maxint64,
838         dle_split_diskbuffer => "$Installcheck::TMP",
839         dle_fallback_splitsize => 250,
840     ) },
841     { allow_split => 1, part_size => $maxint64, part_cache_type => 'memory', part_cache_max_size => 250,
842       warning => "falling back to memory buffer for splitting: " .
843                  "insufficient space in disk cache directory" },
844     "not enough space in split_diskbuffer => fall back (with warning)");
845
846 is_deeply(
847     { get_splitting_args_from_config(
848         can_cache_inform => 0,
849         dle_tape_splitsize => 200,
850         dle_split_diskbuffer => "$Installcheck::TMP",
851         dle_fallback_splitsize => 150,
852     ) },
853     { allow_split => 1, part_size => 200, part_cache_type => 'disk', part_cache_dir => "$Installcheck::TMP" },
854     "if split_diskbuffer exists and splitsize is nonzero, use it");
855
856 is_deeply(
857     { get_splitting_args_from_config(
858         dle_tape_splitsize => 0,
859         dle_split_diskbuffer => "$Installcheck::TMP",
860         dle_fallback_splitsize => 250,
861     ) },
862     { allow_split => 0, part_size => 0, part_cache_type => 'none' },
863     ".. but if splitsize is zero, no splitting");
864
865 is_deeply(
866     { get_splitting_args_from_config(
867         dle_split_diskbuffer => "$Installcheck::TMP",
868         dle_fallback_splitsize => 250,
869     ) },
870     { allow_split => 0, part_size => 0, part_cache_type => 'none' },
871     ".. and if splitsize is missing, no splitting");
872
873 is_deeply(
874     { get_splitting_args_from_config(
875         part_size => 300,
876         part_cache_type => 'none',
877     ) },
878     { allow_split => 1, part_size => 300, part_cache_type => 'none' },
879     "part_* parameters handled correctly when missing");
880
881 is_deeply(
882     { get_splitting_args_from_config(
883         part_size => 300,
884         part_cache_type => 'disk',
885         part_cache_dir => $Installcheck::TMP,
886         part_cache_max_size => 250,
887     ) },
888     { allow_split => 1, part_size => 300, part_cache_type => 'disk',
889       part_cache_dir => $Installcheck::TMP, part_cache_max_size => 250, },
890     "part_* parameters handled correctly when specified");
891
892 is_deeply(
893     { get_splitting_args_from_config(
894         part_size => 300,
895         part_cache_type => 'disk',
896         part_cache_dir => "$Installcheck::TMP/does!not!exist!",
897         part_cache_max_size => 250,
898     ) },
899     { allow_split => 1, part_size => 300, part_cache_type => 'none',
900       part_cache_max_size => 250,
901       warning => "part-cache-dir '$Installcheck::TMP/does!not!exist! does not exist; "
902                . "using part cache type 'none'"},
903     "part_* parameters handled correctly when specified");
904
905 $chg->quit();
906 rmtree($taperoot);