1 # Copyright (c) 2009, 2010 Zmanda Inc. All Rights Reserved.
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.
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
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
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19 use Test::More tests => 10;
24 use lib "@amperldir@";
25 use Installcheck::Config;
26 use Amanda::Config qw( :init );
28 use Amanda::Device qw( :constants );
32 use Amanda::Taper::Scribe;
35 # and disable Debug's die() and warn() overrides
36 Amanda::Debug::disable_die_override();
38 # put the debug messages somewhere
39 Amanda::Debug::dbopen("installcheck");
40 Installcheck::log_test_output();
42 # use some very small vtapes
43 my $volume_length = 512*1024;
46 $testconf = Installcheck::Config->new();
47 $testconf->add_tapetype("TEST-TAPE", [
48 "length" => ($volume_length / 1024) . " k",
52 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
53 if ($cfg_result != $CFGERR_OK) {
54 my ($level, @errors) = Amanda::Config::config_errors();
55 die(join "\n", @errors);
58 my $taperoot = "$Installcheck::TMP/Amanda_Taper_Scribe";
68 for my $slot (1 .. $nslots) {
69 mkdir("$taperoot/slot$slot")
70 or die("Could not mkdir: $!");
74 # an accumulator for the sequence of events that transpire during a run
87 Math::BigInt->new($_[0]);
90 # and similarly an Amanda::Changer::Error
92 Amanda::Changer::Error->new(@_);
96 ## Mock classes for the scribe
99 package Mock::Taperscan;
100 use Amanda::Device qw( :constants );
101 use Amanda::MainLoop;
106 my $chg = Amanda::Changer->new("chg-disk:$taperoot");
107 die $chg if $chg->isa("Amanda::Changer::Error");
112 next_or_current => "current",
119 my $result_cb = $params{'result_cb'};
123 my @slotarg = (@{$self->{'slots'}})?
124 (slot => shift @{$self->{'slots'}})
125 : (relative_slot => $self->{'next_or_current'});
126 $self->{'next_or_current'} = 'next';
128 my $res_cb = make_cb('res_cb' => sub {
129 my ($err, $res) = @_;
131 my $slot = $res? $res->{'this_slot'} : "none";
132 main::event("scan-finished", main::undef_or_str($err), "slot: $slot");
137 $result_cb->(undef, $res, 'FAKELABEL', $ACCESS_WRITE);
141 # delay this load call a little bit -- just enough so that the
142 # request_volume_permission event reliably occurs first
143 Amanda::MainLoop::call_after(50, sub {
144 $self->{'chg'}->load(@slotarg, set_current => 1, res_cb => $res_cb);
148 package Mock::Feedback;
149 use base qw( Amanda::Taper::Scribe::Feedback );
155 rq_answers => [ @rq_answers ],
159 sub request_volume_permission {
162 my $answer = shift @{$self->{'rq_answers'}};
163 main::event("request_volume_permission", "answer:", $answer);
164 $params{'perm_cb'}->($answer);
171 main::event("notif_new_tape",
172 main::undef_or_str($params{'error'}), $params{'volume_label'});
175 sub notif_part_done {
179 # this omits $duration, as it's not constant
180 main::event("notif_part_done",
181 $params{'partnum'}, $params{'fileno'},
182 $params{'successful'}, $params{'size'});
192 # utility fn to stringify changer errors (earlier perls' Test::More's
193 # fail to do this automatically)
194 sub undef_or_str { (defined $_[0])? "".$_[0] : undef; }
197 my ($nruns, $taperscan, $feedback) = @_;
201 reset_taperoot($nruns);
202 $devh = Amanda::Taper::Scribe::DevHandling->new(
203 taperscan => $taperscan,
204 feedback => $feedback);
206 my ($start, $get_volume, $got_volume, $quit);
208 $start = make_cb(start => sub {
212 # give start() time to get the scan going before
213 # calling get_volume -- this wouldn't ordinarily be
214 # necessary, but we want to make sure that start() is
215 # really kicking off the scan.
220 $get_volume = make_cb(get_volume => sub {
221 if (++$runcount > $nruns) {
227 $devh->get_volume(volume_cb => $got_volume);
230 $got_volume = make_cb(got_volume => sub {
231 my ($scan_error, $request_denied_reason, $reservation, $volume_label, $access_mode) = @_;
234 undef_or_str($scan_error),
235 $request_denied_reason,
236 $reservation? ("slot: ".$reservation->{'this_slot'}) : undef);
238 if ($scan_error or $request_denied_reason) {
243 $reservation->release(finished_cb => sub {
245 event("release", $error);
254 $quit = make_cb(quit => sub {
256 Amanda::MainLoop::quit();
260 Amanda::MainLoop::run();
264 run_devh(3, Mock::Taperscan->new(), Mock::Feedback->new());
265 is_deeply([ @events ], [
267 [ 'scan' ], # scan starts *before* get_volume
270 [ 'request_volume_permission', 'answer:', undef ],
271 [ 'scan-finished', undef, "slot: 1" ],
272 [ 'got_volume', undef, undef, "slot: 1" ],
273 [ 'release', undef ],
276 [ 'scan' ], # scan starts *after* get_volume this time
277 [ 'request_volume_permission', 'answer:', undef ],
278 [ 'scan-finished', undef, "slot: 2" ],
279 [ 'got_volume', undef, undef, "slot: 2" ],
280 [ 'release', undef ],
284 [ 'request_volume_permission', 'answer:', undef ],
285 [ 'scan-finished', undef, "slot: 3" ],
286 [ 'got_volume', undef, undef, "slot: 3" ],
287 [ 'release', undef ],
290 ], "correct event sequence for basic run of DevHandling")
291 or diag(Dumper([@events]));
293 run_devh(1, Mock::Taperscan->new(), Mock::Feedback->new('no-can-do'));
294 is_deeply([ @events ], [
299 [ 'request_volume_permission', 'answer:', 'no-can-do' ],
300 [ 'scan-finished', undef, "slot: 1" ],
301 [ 'got_volume', undef, 'no-can-do', undef ],
304 ], "correct event sequence for a run without permission")
305 or diag(Dumper([@events]));
307 run_devh(1, Mock::Taperscan->new("bogus"), Mock::Feedback->new());
308 is_deeply([ @events ], [
313 [ 'request_volume_permission', 'answer:', undef ],
314 [ 'scan-finished', "Slot bogus not found", "slot: none" ],
315 [ 'got_volume', 'Slot bogus not found', undef, undef ],
318 ], "correct event sequence for a run with a changer error")
319 or diag(Dumper([@events]));
321 run_devh(1, Mock::Taperscan->new("bogus"), Mock::Feedback->new("not this time"));
322 is_deeply([ @events ], [
327 [ 'request_volume_permission', 'answer:', 'not this time' ],
328 [ 'scan-finished', "Slot bogus not found", "slot: none" ],
329 [ 'got_volume', 'Slot bogus not found', 'not this time', undef ],
332 ], "correct event sequence for a run with no permission AND a changer error")
333 or diag(Dumper([@events]));
339 sub run_scribe_xfer_async {
340 my ($data_length, $scribe, %params) = @_;
343 my $finished_cb = $params{'finished_cb'};
344 my $steps = define_steps
345 cb_ref => \$finished_cb;
347 step start_scribe => sub {
348 if ($params{'start_scribe'}) {
349 $scribe->start(%{ $params{'start_scribe'} },
350 finished_cb => $steps->{'get_xdt'});
352 $steps->{'get_xdt'}->();
356 step get_xdt => sub {
361 my $xdt = $scribe->get_xfer_dest(
362 max_memory => 1024 * 64,
363 split_method => ($params{'split_method'} or 'memory'),
364 part_size => 1024 * 128,
366 disk_cache_dirname => undef);
370 my $hdr = Amanda::Header->new();
371 $hdr->{type} = $Amanda::Header::F_DUMPFILE;
372 $hdr->{datestamp} = "20010203040506";
373 $hdr->{dumplevel} = 0;
374 $hdr->{compressed} = 1;
375 $hdr->{name} = "localhost";
376 $hdr->{disk} = "/home";
377 $hdr->{program} = "INSTALLCHECK";
379 $xfer = Amanda::Xfer->new([
380 Amanda::Xfer::Source::Random->new($data_length, 0x5EED5),
385 $scribe->handle_xmsg(@_);
391 dump_cb => $steps->{'dump_cb'});
394 step dump_cb => sub {
397 main::event("dump_cb",
399 [ map { undef_or_str($_) } @{ $params{'device_errors'} } ],
406 sub run_scribe_xfer {
407 my ($data_length, $scribe, %params) = @_;
408 $params{'finished_cb'} = \&Amanda::MainLoop::quit;
409 run_scribe_xfer_async($data_length, $scribe, %params);
410 Amanda::MainLoop::run();
416 my $finished_cb = make_cb(finished_cb => sub {
418 die "$error" if $error;
420 Amanda::MainLoop::quit();
423 $scribe->quit(finished_cb => $finished_cb);
425 Amanda::MainLoop::run();
431 # write less than a tape full
434 $scribe = Amanda::Taper::Scribe->new(
435 taperscan => Mock::Taperscan->new(),
436 feedback => Mock::Feedback->new());
439 run_scribe_xfer(1024*300, $scribe,
440 start_scribe => { dump_timestamp => "20010203040506" });
442 is_deeply([ @events ], [
444 [ 'scan-finished', undef, 'slot: 1' ],
445 [ 'request_volume_permission', 'answer:', undef ],
446 [ 'notif_new_tape', undef, 'FAKELABEL' ],
447 [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
448 [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
449 [ 'notif_part_done', bi(3), bi(3), 1, bi(45056) ],
450 [ 'dump_cb', 'DONE', [], bi(307200) ],
451 ], "correct event sequence for a multipart scribe of less than a whole volume")
452 or diag(Dumper([@events]));
454 # pick up where we left off, writing just a tiny bit more
456 run_scribe_xfer(1024*30, $scribe);
458 is_deeply([ @events ], [
459 [ 'notif_part_done', bi(1), bi(4), 1, bi(30720) ],
460 [ 'dump_cb', 'DONE', [], bi(30720) ],
461 ], "correct event sequence for a subsequent single-part scribe, still on the same volume")
462 or diag(Dumper([@events]));
464 quit_scribe($scribe);
466 # start over again and try a multivolume write
468 # NOTE: the part size and volume size are such that the VFS driver produces
469 # ENOSPC while writing the fourth file header, rather than while writing
470 # data. This is a much less common error path, so it's good to test it.
473 $scribe = Amanda::Taper::Scribe->new(
474 taperscan => Mock::Taperscan->new(),
475 feedback => Mock::Feedback->new());
478 run_scribe_xfer($volume_length + $volume_length / 4, $scribe,
479 start_scribe => { dump_timestamp => "20010203040506" });
481 is_deeply([ @events ], [
483 [ 'scan-finished', undef, 'slot: 1' ],
484 [ 'request_volume_permission', 'answer:', undef ],
485 [ 'notif_new_tape', undef, 'FAKELABEL' ],
487 [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
488 [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
489 [ 'notif_part_done', bi(3), bi(3), 1, bi(131072) ],
490 [ 'notif_part_done', bi(4), bi(0), 0, bi(0) ],
493 [ 'request_volume_permission', 'answer:', undef ],
494 [ 'scan-finished', undef, 'slot: 2' ],
495 [ 'notif_new_tape', undef, 'FAKELABEL' ],
497 [ 'notif_part_done', bi(4), bi(1), 1, bi(131072) ],
498 [ 'notif_part_done', bi(5), bi(2), 1, bi(131072) ],
499 # empty part is written but not notified
501 [ 'dump_cb', 'DONE', [], bi(655360) ],
502 ], "correct event sequence for a multipart scribe of more than a whole volume")
503 or print (Dumper([@events]));
505 quit_scribe($scribe);
507 # now a multivolume write where the second volume gives a changer error
510 $scribe = Amanda::Taper::Scribe->new(
511 taperscan => Mock::Taperscan->new("1", "bogus"),
512 feedback => Mock::Feedback->new());
515 run_scribe_xfer($volume_length + $volume_length / 4, $scribe,
516 start_scribe => { dump_timestamp => "20010203040507" });
518 $experr = 'Slot bogus not found';
519 is_deeply([ @events ], [
521 [ 'scan-finished', undef, 'slot: 1' ],
522 [ 'request_volume_permission', 'answer:', undef ],
523 [ 'notif_new_tape', undef, 'FAKELABEL' ],
525 [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
526 [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
527 [ 'notif_part_done', bi(3), bi(3), 1, bi(131072) ],
528 [ 'notif_part_done', bi(4), bi(0), 0, bi(0) ],
531 [ 'request_volume_permission', 'answer:', undef ],
532 [ 'scan-finished', $experr, 'slot: none' ],
533 [ 'notif_new_tape', $experr, undef ],
535 [ 'dump_cb', 'PARTIAL', [$experr], bi(393216) ],
536 ], "correct event sequence for a multivolume scribe where the second volume isn't found")
537 or print (Dumper([@events]));
539 quit_scribe($scribe);
541 # now a multivolume write where the second volume does not have permission
544 $scribe = Amanda::Taper::Scribe->new(
545 taperscan => Mock::Taperscan->new(),
546 feedback => Mock::Feedback->new(undef, "sorry!"));
549 run_scribe_xfer($volume_length + $volume_length / 4, $scribe,
550 start_scribe => { dump_timestamp => "20010203040507" });
552 is_deeply([ @events ], [
554 [ 'scan-finished', undef, 'slot: 1' ],
555 [ 'request_volume_permission', 'answer:', undef ],
556 [ 'notif_new_tape', undef, 'FAKELABEL' ],
558 [ 'notif_part_done', bi(1), bi(1), 1, bi(131072) ],
559 [ 'notif_part_done', bi(2), bi(2), 1, bi(131072) ],
560 [ 'notif_part_done', bi(3), bi(3), 1, bi(131072) ],
561 [ 'notif_part_done', bi(4), bi(0), 0, bi(0) ],
564 [ 'request_volume_permission', 'answer:', "sorry!" ],
565 [ 'scan-finished', undef, 'slot: 2' ],
567 [ 'dump_cb', 'PARTIAL', ["sorry!"], bi(393216) ],
568 ], "correct event sequence for a multivolume scribe where the second volume isn't permitted")
569 or print (Dumper([@events]));
571 quit_scribe($scribe);
573 # a non-splitting xfer on a single volume
576 $scribe = Amanda::Taper::Scribe->new(
577 taperscan => Mock::Taperscan->new(),
578 feedback => Mock::Feedback->new());
581 run_scribe_xfer(1024*300, $scribe, split_method => 'none',
582 start_scribe => { dump_timestamp => "20010203040506" });
584 is_deeply([ @events ], [
586 [ 'scan-finished', undef, 'slot: 1' ],
587 [ 'request_volume_permission', 'answer:', undef ],
588 [ 'notif_new_tape', undef, 'FAKELABEL' ],
589 [ 'notif_part_done', bi(1), bi(1), 1, bi(307200) ],
590 [ 'dump_cb', 'DONE', [], bi(307200) ],
591 ], "correct event sequence for a non-splitting scribe of less than a whole volume")
592 or diag(Dumper([@events]));
594 quit_scribe($scribe);
596 # DirectTCP support is tested through the taper installcheck