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