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