1 # Copyright (c) 2007, 2008, 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 => 47;
24 use lib "@amperldir@";
25 use Installcheck::Config;
30 use Amanda::Config qw( :init :getconf config_dir_relative );
33 # set up debugging so debug output doesn't interfere with test results
34 Amanda::Debug::dbopen("installcheck");
35 Installcheck::log_test_output();
37 # and disable Debug's die() and warn() overrides
38 Amanda::Debug::disable_die_override();
41 # define a "test" changer for purposes of this installcheck
43 package Amanda::Changer::test;
45 @ISA = qw( Amanda::Changer );
47 # monkey-patch our test changer into Amanda::Changer, and indicate that
48 # the module has already been required by adding a key to %INC
49 $INC{'Amanda/Changer/test.pm'} = "Amanda_Changer";
53 my ($config, $tpchanger) = @_;
58 slots => [ 'TAPE-00', 'TAPE-01', 'TAPE-02', 'TAPE-03' ],
62 bless ($self, $class);
70 my $cb = $params{'res_cb'};
72 if (exists $params{'label'}) {
75 my $label = $params{'label'};
77 for my $i (0 .. $#{$self->{'slots'}}) {
78 if ($self->{'slots'}->[$i] eq $label) {
84 $cb->("No such label '$label'", undef);
88 # check that it's not in use
89 for my $used_slot (@{$self->{'reserved_slots'}}) {
90 if ($used_slot == $slot) {
91 $cb->("Volume with label '$label' is already in use", undef);
97 push @{$self->{'reserved_slots'}}, $slot;
99 if (exists $params{'set_current'} && $params{'set_current'}) {
100 $self->{'curslot'} = $slot;
103 $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
104 } elsif (exists $params{'slot'} or exists $params{'relative_slot'}) {
105 my $slot = $params{'slot'};
106 if (exists $params{'relative_slot'}) {
107 if ($params{'relative_slot'} eq "current") {
108 $slot = $self->{'curslot'};
109 } elsif ($params{'relative_slot'} eq "next") {
110 $slot = ($self->{'curslot'} + 1) % (scalar @{$self->{'slots'}});
112 die "invalid relative_slot";
116 if (grep { $_ == $slot } @{$self->{'reserved_slots'}}) {
117 $cb->("Slot $slot is already in use", undef);
120 my $label = $self->{'slots'}->[$slot];
121 push @{$self->{'reserved_slots'}}, $slot;
123 if (exists $params{'set_current'} && $params{'set_current'}) {
124 $self->{'curslot'} = $slot;
127 $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
129 die "No label or slot parameter given";
135 my ($key, %params) = @_;
138 if ($key eq 'num_slots') {
140 } elsif ($key eq 'mkerror1') {
141 return $self->make_error("failed", $params{'info_cb'},
144 } elsif ($key eq 'mkerror2') {
145 return $self->make_error("failed", $params{'info_cb'},
150 $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
157 $self->{'curslot'} = 0;
159 $params{'finished_cb'}->(undef) if $params{'finished_cb'};
166 $self->{'clean'} = 1;
168 $params{'finished_cb'}->(undef) if $params{'finished_cb'};
175 Amanda::MainLoop::call_later($params{'inventory_cb'},
187 package Amanda::Changer::test::Reservation;
189 @ISA = qw( Amanda::Changer::Reservation );
193 my ($chg, $slot, $label) = @_;
194 my $self = Amanda::Changer::Reservation::new($class);
196 $self->{'chg'} = $chg;
197 $self->{'slot'} = $slot;
198 $self->{'label'} = $label;
200 $self->{'device'} = Amanda::Device->new("null:slot-$slot");
201 $self->{'this_slot'} = $slot;
209 my $slot = $self->{'slot'};
210 my $chg = $self->{'chg'};
212 $chg->{'reserved_slots'} = [ grep { $_ != $slot } @{$chg->{'reserved_slots'}} ];
214 $params{'finished_cb'}->(undef) if $params{'finished_cb'};
220 my $slot = $self->{'slot'};
221 my $chg = $self->{'chg'};
223 $self->{'chg'}->{'slots'}->[$self->{'slot'}] = $params{'label'};
224 $self->{'label'} = $params{'label'};
226 $params{'finished_cb'}->(undef) if $params{'finished_cb'};
230 # back to the perl tests..
234 # work against a config specifying our test changer, to work out the kinks
235 # when it opens devices to check their labels
237 $testconf = Installcheck::Config->new();
238 $testconf->add_changer("mychanger", [
239 'tpchanger' => '"chg-test:/foo"',
240 'property' => '"testprop" "testval"',
244 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
245 if ($cfg_result != $CFGERR_OK) {
246 my ($level, @errors) = Amanda::Config::config_errors();
247 die(join "\n", @errors);
250 # check out the relevant changer properties
251 my $chg = Amanda::Changer->new("mychanger");
252 is($chg->{'config'}->get_property("testprop"), "testval",
253 "changer properties are correctly represented");
255 # test loading by label
259 my ($getres, $rq_reserved, $relres);
261 $getres = make_cb('getres' => sub {
263 return $rq_reserved->();
266 my $label = pop @labels;
268 $chg->load(label => $label,
269 set_current => ($label eq "TAPE-02"),
271 my ($err, $res) = @_;
272 ok(!$err, "no error loading $label")
275 # keep this reservation
276 push @reservations, $res if $res;
278 # and start on the next
283 $rq_reserved = make_cb(rq_reserved => sub {
284 # try to load an already-reserved volume
285 $chg->load(label => 'TAPE-00',
287 my ($err, $res) = @_;
288 ok($err, "error when requesting already-reserved volume");
289 push @reservations, $res if $res;
295 $relres = make_cb('relres' => sub {
296 if (!@reservations) {
297 return Amanda::MainLoop::quit();
300 my $res = pop @reservations;
301 $res->release(finished_cb => sub {
310 @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
312 Amanda::MainLoop::run();
315 Amanda::MainLoop::run();
317 @labels = ( 'TAPE-00', 'TAPE-01' );
319 Amanda::MainLoop::run();
321 # explicitly release the reservations (without using the callback)
322 for my $res (@reservations) {
327 # test loading by slot
329 my ($start, $first_cb, $released, $second_cb, $quit);
332 # reserves the current slot
333 $start = make_cb('start' => sub {
334 $chg->load(res_cb => $first_cb, relative_slot => "current");
337 # gets a reservation for the "current" slot
338 $first_cb = make_cb('first_cb' => sub {
339 my ($err, $res) = @_;
342 is($res->{'this_slot'}, 2,
343 "'current' slot loads slot 2");
344 is($res->{'device'}->device_name, "null:slot-2",
345 "..device is correct");
347 $slot = $res->{'this_slot'};
348 $res->release(finished_cb => $released);
351 $released = make_cb(released => sub {
354 $chg->load(res_cb => $second_cb, relative_slot => 'next',
355 slot => $slot, set_current => 1);
358 # gets a reservation for the "next" slot
359 $second_cb = make_cb('second_cb' => sub {
360 my ($err, $res) = @_;
363 is($res->{'this_slot'}, 3,
364 "next slot loads slot 3");
365 is($chg->{'curslot'}, 3,
366 "..which is also now the current slot");
368 $res->release(finished_cb => $quit);
371 $quit = make_cb(quit => sub {
375 Amanda::MainLoop::quit();
379 Amanda::MainLoop::run();
384 my ($start, $load1_cb, $set_cb, $released, $load2_cb, $released2, $load3_cb);
388 $start = make_cb('start' => sub {
389 $chg->load(res_cb => $load1_cb, label => "TAPE-00");
392 # rename it to TAPE-99
393 $load1_cb = make_cb('load1_cb' => sub {
394 (my $err, $res) = @_;
397 pass("loaded TAPE-00");
398 $res->set_label(label => "TAPE-99", finished_cb => $set_cb);
401 $set_cb = make_cb('set_cb' => sub {
404 $res->release(finished_cb => $released);
407 # try to load TAPE-00
408 $released = make_cb('released' => sub {
412 pass("relabeled TAPE-00 to TAPE-99");
413 $chg->load(res_cb => $load2_cb, label => "TAPE-00");
416 # try to load TAPE-99
417 $load2_cb = make_cb('load2_cb' => sub {
418 (my $err, $res) = @_;
419 ok($err, "loading TAPE-00 is now an error");
421 $chg->load(res_cb => $load3_cb, label => "TAPE-99");
425 $load3_cb = make_cb('load3_cb' => sub {
426 (my $err, $res) = @_;
429 pass("but loading TAPE-99 is ok");
431 $res->release(finished_cb => $released2);
434 $released2 = make_cb(released2 => sub {
438 Amanda::MainLoop::quit();
442 Amanda::MainLoop::run();
445 # test reset and clean and inventory
447 my ($finished_cb) = @_;
449 my $steps = define_steps
450 cb_ref => \$finished_cb;
452 step do_reset => sub {
453 $chg->reset(finished_cb => sub {
454 is($chg->{'curslot'}, 0,
455 "reset() resets to slot 0");
456 $steps->{'do_clean'}->();
460 step do_clean => sub {
461 $chg->clean(finished_cb => sub {
462 ok($chg->{'clean'}, "clean 'cleaned' the changer");
463 $steps->{'do_inventory'}->();
467 step do_inventory => sub {
468 $chg->inventory(inventory_cb => sub {
477 }], "inventory returns an inventory");
482 test_simple(\&Amanda::MainLoop::quit);
483 Amanda::MainLoop::run();
487 my ($do_info, $check_info, $do_info_err, $check_info_err);
489 $do_info = make_cb('do_info' => sub {
490 $chg->info(info_cb => $check_info,
491 info => [ 'num_slots' ]);
494 $check_info = make_cb('check_info' => sub {
495 my ($err, %results) = @_;
497 is_deeply(\%results, { 'num_slots' => 13 },
502 $do_info_err = make_cb('do_info_err' => sub {
503 $chg->info(info_cb => $check_info_err,
504 info => [ 'mkerror1', 'mkerror2' ]);
507 $check_info_err = make_cb('check_info_err' => sub {
508 my ($err, %results) = @_;
510 "While getting info key 'mkerror1': err1; While getting info key 'mkerror2': err2",
511 "info errors are handled correctly");
512 is($err->{'type'}, 'failed', "error has type 'failed'");
513 ok($err->failed, "\$err->failed is true");
514 ok(!$err->fatal, "\$err->fatal is false");
515 is($err->{'reason'}, 'unknown', "\$err->{'reason'} is 'unknown'");
516 ok($err->unknown, "\$err->unknown is true");
517 ok(!$err->notimpl, "\$err->notimpl is false");
518 Amanda::MainLoop::quit();
522 Amanda::MainLoop::run();
525 # Test the various permutations of configuration setup, with a patched
526 # _new_from_uri so we can monitor the result
527 sub my_new_from_uri {
528 my ($uri, $cc, $name) = @_;
529 return $uri if (ref $uri and $uri->isa("Amanda::Changer::Error"));
530 return [ $uri, $cc? "cc" : undef ];
532 *saved_new_from_uri = *Amanda::Changer::_new_from_uri;
533 *Amanda::Changer::_new_from_uri = *my_new_from_uri;
536 my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn) = @_;
538 $testconf = Installcheck::Config->new();
540 if (defined($global_tapedev)) {
541 $testconf->add_param('tapedev', "\"$global_tapedev\"")
544 if (defined($global_tpchanger)) {
545 $testconf->add_param('tpchanger', "\"$global_tpchanger\"")
548 if (defined($defn_tpchanger)) {
549 $testconf->add_changer("mychanger", [
550 'tpchanger' => "\"$defn_tpchanger\"",
554 if (defined($custom_defn)) {
555 $testconf->add_changer("customchanger", $custom_defn);
556 $testconf->add_param('tpchanger', '"customchanger"');
561 my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
562 if ($cfg_result != $CFGERR_OK) {
563 my ($level, @errors) = Amanda::Config::config_errors();
564 die(join "\n", @errors);
569 my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn,
570 $name, $regexp, $msg) = @_;
571 loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn);
572 my $err = Amanda::Changer->new($name);
573 if ($err->isa("Amanda::Changer::Error")) {
574 like($err->{'message'}, $regexp, $msg);
576 diag("Amanda::Changer->new did not return an Error object:");
577 diag("".Dumper($err));
582 assert_invalid(undef, undef, undef, undef, undef,
583 qr/You must specify one of 'tapedev' or 'tpchanger'/,
584 "supplying a nothing is invalid");
586 loadconfig(undef, "file:/foo", undef, undef);
587 is_deeply( Amanda::Changer->new(), [ "chg-single:file:/foo", undef ],
588 "default changer with global tpchanger naming a device");
590 loadconfig(undef, "chg-disk:/foo", undef, undef);
591 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
592 "default changer with global tpchanger naming a changer");
594 loadconfig(undef, "mychanger", "chg-disk:/bar", undef);
595 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
596 "default changer with global tpchanger naming a defined changer with a uri");
598 loadconfig(undef, "mychanger", "chg-zd-mtx", undef);
599 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
600 "default changer with global tpchanger naming a defined changer with a compat script");
602 loadconfig(undef, "chg-zd-mtx", undef, undef);
603 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
604 "default changer with global tpchanger naming a compat script");
606 loadconfig("tape:/dev/foo", undef, undef, undef);
607 is_deeply( Amanda::Changer->new(), [ "chg-single:tape:/dev/foo", undef ],
608 "default changer with global tapedev naming a device and no tpchanger");
610 assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef, undef,
611 qr/Cannot specify both 'tapedev' and 'tpchanger'/,
612 "supplying a device for both tpchanger and tapedev is invalid");
614 assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef, undef,
615 qr/Cannot specify both 'tapedev' and 'tpchanger'/,
616 "supplying a device for tapedev and a changer for tpchanger is invalid");
618 loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef, undef);
619 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
620 "default changer with global tapedev naming a device and a global tpchanger naming a compat script");
622 assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef, undef,
623 qr/Cannot specify both 'tapedev' and 'tpchanger'/,
624 "supplying a changer for tapedev and a device for tpchanger is invalid");
626 loadconfig("chg-disk:/foo", undef, undef, undef);
627 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
628 "default changer with global tapedev naming a device");
630 loadconfig("mychanger", undef, "chg-disk:/bar", undef);
631 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
632 "default changer with global tapedev naming a defined changer with a uri");
634 loadconfig("mychanger", undef, "chg-zd-mtx", undef);
635 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
636 "default changer with global tapedev naming a defined changer with a compat script");
638 loadconfig(undef, undef, "chg-disk:/foo", undef);
639 is_deeply( Amanda::Changer->new("mychanger"), [ "chg-disk:/foo", "cc" ],
640 "named changer loads the proper definition");
642 loadconfig(undef, undef, undef, [
643 tapedev => '"chg-disk:/foo"',
645 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", "cc" ],
646 "defined changer with tapedev loads the proper definition");
648 loadconfig(undef, undef, undef, [
649 tpchanger => '"chg-disk:/bar"',
651 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
652 "defined changer with tpchanger loads the proper definition");
654 assert_invalid(undef, undef, undef, [
655 tpchanger => '"chg-disk:/bar"',
656 tapedev => '"file:/bar"',
658 qr/Cannot specify both 'tapedev' and 'tpchanger'/,
659 "supplying both a new tpchanger and tapedev in a definition is invalid");
661 assert_invalid(undef, undef, undef, [
662 property => '"this" "will not work"',
664 qr/You must specify one of 'tapedev' or 'tpchanger'/,
665 "supplying neither a tpchanger nor tapedev in a definition is invalid");
667 *Amanda::Changer::_new_from_uri = *saved_new_from_uri;
669 # test with_locked_state *within* a process
671 sub test_locked_state {
672 my ($finished_cb) = @_;
674 my $stfile = "$Installcheck::TMP/test-statefile";
675 my $num_outstanding = 0;
677 my $steps = define_steps
678 cb_ref => \$finished_cb;
681 $chg = Amanda::Changer->new("chg-null:");
683 for my $num (qw( one two three )) {
685 $chg->with_locked_state($stfile, $steps->{'maybe_done'}, sub {
686 my ($state, $maybe_done) = @_;
688 $state->{$num} = $num;
691 Amanda::MainLoop::call_after(50, $maybe_done, undef, $state);
696 step maybe_done => sub {
697 my ($err, $state) = @_;
700 return if (--$num_outstanding);
707 }, "state is maintained correctly (within a process)");
709 unlink($stfile) if -f $stfile;
714 test_locked_state(\&Amanda::MainLoop::quit);
715 Amanda::MainLoop::run();