7eadaa25e9e3d6462728f1972e55e4890faa7472
[debian/amanda] / installcheck / Amanda_Changer_robot.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 => 324;
20 use File::Path;
21 use Data::Dumper;
22 use strict;
23 use warnings;
24
25 use lib "@amperldir@";
26 use Installcheck;
27 use Installcheck::Config;
28 use Installcheck::Changer;
29 use Installcheck::Mock qw( setup_mock_mtx $mock_mtx_path );
30 use Amanda::Device qw( :constants );
31 use Amanda::Debug;
32 use Amanda::Paths;
33 use Amanda::MainLoop;
34 use Amanda::Config qw( :init :getconf config_dir_relative );
35 use Amanda::Changer;
36
37 # set up debugging so debug output doesn't interfere with test results
38 Amanda::Debug::dbopen("installcheck");
39
40 # and disable Debug's die() and warn() overrides
41 Amanda::Debug::disable_die_override();
42 Installcheck::log_test_output();
43
44 my $chg_state_file = "$Installcheck::TMP/chg-robot-state";
45 unlink($chg_state_file) if -f $chg_state_file;
46
47 my $mtx_state_file = setup_mock_mtx (
48          num_slots => 5,
49          num_ie => 1,
50          barcodes => 1,
51          track_orig => 1,
52          num_drives => 2,
53          loaded_slots => {
54             1 => '11111',
55             2 => '22222',
56             3 => '33333',
57             4 => '44444',
58             # slot 5 is empty
59          },
60          first_slot => 1,
61          first_drive => 0,
62          first_ie => 6,
63        );
64
65 sub check_inventory {
66     my ($chg, $barcodes, $next_step, $expected, $msg) = @_;
67
68     $chg->inventory(inventory_cb => make_cb(sub {
69         my ($err, $inv) = @_;
70         die $err if $err;
71
72         # strip barcodes from both $expected and $inv
73         if (!$barcodes) {
74             for (@$expected, @$inv) {
75                 delete $_->{'barcode'};
76             }
77         }
78
79         is_deeply($inv, $expected, $msg)
80             or diag("Got:\n" . Dumper($inv));
81
82         $next_step->();
83     }));
84 }
85
86 ##
87 # test the "interface" package
88
89 sub test_interface {
90     my ($finished_cb) = @_;
91     my ($interface, $chg);
92
93     my $steps = define_steps
94         cb_ref => \$finished_cb,
95         finalize => sub { $chg->quit() };
96
97     step start => sub {
98         my $testconf = Installcheck::Config->new();
99         $testconf->add_changer('robo', [
100             tpchanger => "\"chg-robot:$mtx_state_file\"",
101             changerfile => "\"$chg_state_file\"",
102
103             # point to the two vtape "drives" that mock/mtx will set up
104             property => "\"tape-device\" \"0=null:drive0\"",
105
106             # an point to the mock mtx
107             property => "\"mtx\" \"$mock_mtx_path\"",
108         ]);
109         $testconf->write();
110
111         my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
112         if ($cfg_result != $CFGERR_OK) {
113             my ($level, @errors) = Amanda::Config::config_errors();
114             die(join "\n", @errors);
115         }
116
117         $chg = Amanda::Changer->new("robo");
118         die "$chg" if $chg->isa("Amanda::Changer::Error");
119         is($chg->have_inventory(), '1', "changer have inventory");
120         $interface = $chg->{'interface'};
121
122         $interface->inquiry($steps->{'inquiry_cb'});
123     };
124
125     step inquiry_cb => sub {
126         my ($error, $info) = @_;
127
128         die $error if $error;
129
130         is_deeply($info, {
131             'revision' => '0416',
132             'product id' => 'SSL2000 Series',
133             'attached changer' => 'No',
134             'vendor id' => 'COMPAQ',
135             'product type' => 'Medium Changer'
136             }, "robot::Interface inquiry() info is correct");
137
138         $steps->{'status1'}->();
139     };
140
141     step status1 => sub {
142         $interface->status(sub {
143             my ($error, $status) = @_;
144
145             die $error if $error;
146
147             is_deeply($status, {
148                 drives => {
149                     0 => undef,
150                     1 => undef,
151                 },
152                 slots => {
153                     1 => { 'barcode' => '11111', ie => 0 },
154                     2 => { 'barcode' => '22222', ie => 0 },
155                     3 => { 'barcode' => '33333', ie => 0 },
156                     4 => { 'barcode' => '44444', ie => 0 },
157                     5 => { empty => 1, ie => 0 },
158                     6 => { empty => 1, ie => 1 },
159                 },
160             }, "robot::Interface status() output is correct (no drives loaded)");
161             $steps->{'load0'}->();
162         });
163     };
164
165     step load0 => sub {
166         $interface->load(2, 0, sub {
167             my ($error) = @_;
168
169             die $error if $error;
170
171             pass("load");
172             $steps->{'status2'}->();
173         });
174     };
175
176     step status2 => sub {
177         $interface->status(sub {
178             my ($error, $status) = @_;
179
180             die $error if $error;
181
182             is_deeply($status, {
183                 drives => {
184                     0 => { barcode => '22222', 'orig_slot' => 2 },
185                     1 => undef,
186                 },
187                 slots => {
188                     1 => { 'barcode' => '11111', ie => 0 },
189                     2 => { empty => 1, ie => 0 },
190                     3 => { 'barcode' => '33333', ie => 0 },
191                     4 => { 'barcode' => '44444', ie => 0 },
192                     5 => { empty => 1, ie => 0 },
193                     6 => { empty => 1, ie => 1 },
194                 },
195             }, "robot::Interface status() output is correct (one drive loaded)");
196
197             $steps->{'load1'}->();
198         });
199     };
200
201     step load1 => sub {
202         $interface->load(4, 1, sub {
203             my ($error) = @_;
204
205             die $error if $error;
206
207             pass("load");
208             $steps->{'status3'}->();
209         });
210     };
211
212     step status3 => sub {
213         $interface->status(sub {
214             my ($error, $status) = @_;
215
216             die $error if $error;
217
218             is_deeply($status, {
219                 drives => {
220                     0 => { barcode => '22222', 'orig_slot' => 2 },
221                     1 => { barcode => '44444', 'orig_slot' => 4 },
222                 },
223                 slots => {
224                     1 => { 'barcode' => '11111', ie => 0 },
225                     2 => { empty => 1, ie => 0 },
226                     3 => { 'barcode' => '33333', ie => 0 },
227                     4 => { empty => 1, ie => 0 },
228                     5 => { empty => 1, ie => 0 },
229                     6 => { empty => 1, ie => 1 },
230                 },
231             }, "robot::Interface status() output is correct (two drives loaded)");
232
233             $steps->{'transfer'}->();
234         });
235     };
236
237     step transfer => sub {
238         $interface->transfer(3, 6, sub {
239             my ($error) = @_;
240
241             die $error if $error;
242
243             pass("transfer");
244             $steps->{'status4'}->();
245         });
246     };
247
248     step status4 => sub {
249         $interface->status(sub {
250             my ($error, $status) = @_;
251
252             die $error if $error;
253
254             is_deeply($status, {
255                 drives => {
256                     0 => { barcode => '22222', 'orig_slot' => 2 },
257                     1 => { barcode => '44444', 'orig_slot' => 4 },
258                 },
259                 slots => {
260                     1 => { 'barcode' => '11111', ie => 0 },
261                     2 => { empty => 1, ie => 0 },
262                     3 => { empty => 1, ie => 0 },
263                     4 => { empty => 1, ie => 0 },
264                     5 => { empty => 1, ie => 0 },
265                     6 => { 'barcode' => '33333', ie => 1 },
266                 },
267             }, "robot::Interface status() output is correct after transfer");
268
269             $finished_cb->();
270         });
271     };
272 }
273 test_interface(\&Amanda::MainLoop::quit);
274 Amanda::MainLoop::run();
275
276 {
277     my $testconf = Installcheck::Config->new();
278     $testconf->add_changer('bum-scsi-dev', [
279         tpchanger => "\"chg-robot:does/not/exist\"",
280         property => "\"tape-device\" \"0=null:foo\"",
281         changerfile => "\"$chg_state_file\"",
282     ]);
283     $testconf->add_changer('no-tape-device', [
284         tpchanger => "\"chg-robot:$mtx_state_file\"",
285         changerfile => "\"$chg_state_file\"",
286     ]);
287     $testconf->add_changer('bad-property', [
288         tpchanger => "\"chg-robot:$mtx_state_file\"",
289         changerfile => "\"$chg_state_file\"",
290         property => "\"fast-search\" \"maybe\"",
291         property => "\"tape-device\" \"0=null:foo\"",
292     ]);
293     $testconf->add_changer('no-fast-search', [
294         tpchanger => "\"chg-robot:$mtx_state_file\"",
295         changerfile => "\"$chg_state_file\"",
296         property => "\"use-slots\" \"1-3,9\"",
297         property => "append \"use-slots\" \"8,5-6\"",
298         property => "\"fast-search\" \"no\"",
299         property => "\"tape-device\" \"0=null:foo\"",
300     ]);
301     $testconf->add_changer('delays', [
302         tpchanger => "\"chg-robot:$mtx_state_file\"",
303         # no changerfile property
304         property => "\"tape-device\" \"0=null:foo\"",
305         property => "\"status-interval\" \"1m\"",
306         property => "\"eject-delay\" \"1s\"",
307         property => "\"unload-delay\" \"2M\"",
308         property => "\"load-poll\" \"2s POLl 3s uNtil 1m\"",
309     ]);
310     $testconf->write();
311
312     config_uninit();
313     my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
314     if ($cfg_result != $CFGERR_OK) {
315         my ($level, @errors) = Amanda::Config::config_errors();
316         die(join "\n", @errors);
317     }
318
319     # test the changer constructor and properties
320     my $err = Amanda::Changer->new("bum-scsi-dev");
321     chg_err_like($err,
322         { message => "'does/not/exist' not found",
323           type => 'fatal' },
324         "check for device existence works");
325
326     $err = Amanda::Changer->new("no-tape-device");
327     chg_err_like($err,
328         { message => "no 'tape-device' property specified",
329           type => 'fatal' },
330         "tape-device property is required");
331
332     $err = Amanda::Changer->new("bad-property");
333     chg_err_like($err,
334         { message => "invalid 'fast-search' value",
335           type => 'fatal' },
336         "invalid boolean value handled correctly");
337
338     my $chg = Amanda::Changer->new("delays");
339     die "$chg" if $chg->isa("Amanda::Changer::Error");
340     is($chg->have_inventory(), '1', "changer have inventory");
341     is($chg->{'status_interval'}, 60, "status-interval parsed");
342     is($chg->{'eject_delay'}, 1, "eject-delay parsed");
343     is($chg->{'unload_delay'}, 120, "unload-delay parsed");
344     is_deeply($chg->{'load_poll'}, [ 2, 3, 60 ], "load-poll parsed");
345
346     # check out the statefile filename generation
347     my $dashed_mtx_state_file = $mtx_state_file;
348     $dashed_mtx_state_file =~ tr/a-zA-Z0-9/-/cs;
349     $dashed_mtx_state_file =~ s/^-*//;
350     is($chg->{'statefile'}, "$localstatedir/amanda/chg-robot-$dashed_mtx_state_file",
351         "statefile calculated correctly");
352     $chg->quit();
353
354     # test no-fast-search
355     $chg = Amanda::Changer->new("no-fast-search");
356     die "$chg" if $chg->isa("Amanda::Changer::Error");
357     is($chg->have_inventory(), '1', "changer have inventory");
358     $chg->info(
359             info => ['fast_search'],
360             info_cb => make_cb(info_cb => sub {
361         my ($err, %info) = @_;
362         ok(!$info{'fast_search'}, "fast-search property works");
363         Amanda::MainLoop::quit();
364     }));
365     Amanda::MainLoop::run();
366
367     # test use-slots
368     my @allowed = map { $chg->_is_slot_allowed($_) } (0 .. 10);
369     is_deeply([ @allowed ], [ 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0 ],
370         "_is_slot_allowed parses multiple properties and behaves as expected");
371     $chg->quit();
372 }
373
374 ##
375 # Test the real deal
376
377 sub test_changer {
378     my ($mtx_config, $finished_cb) = @_;
379     my $chg;
380     my ($res1, $res2, $mtx_state_file);
381     my $pfx = "BC=$mtx_config->{barcodes}; TORIG=$mtx_config->{track_orig}";
382     my $vtape_root = "$Installcheck::TMP/chg-robot-vtapes";
383
384     my $steps = define_steps
385         cb_ref => \$finished_cb,
386         finalize => sub { $chg->quit() };
387
388     step setup => sub {
389         # clean up
390         unlink($chg_state_file) if -f $chg_state_file;
391
392         # set up some vtapes
393         rmtree($vtape_root);
394         mkpath($vtape_root);
395
396         # reset the mock mtx
397         $mtx_state_file = setup_mock_mtx (
398                  %$mtx_config,
399                  num_slots => 6,
400                  num_ie => 1,
401                  num_drives => 2,
402                  loaded_slots => {
403                     1 => '11111',
404                     2 => '22222',
405                     3 => '33333',
406                     4 => '44444',
407                     # slot 5 is empty
408                     6 => '66666', # slot 6 is full, but not in use-slots
409                  },
410                  first_slot => 1,
411                  first_drive => 0,
412                  first_ie => 6,
413                  vtape_root => $vtape_root,
414                );
415
416         my @ignore_barcodes = ( property => "\"ignore-barcodes\" \"y\"")
417             if ($mtx_config->{'barcodes'} == -1);
418
419         my $testconf = Installcheck::Config->new();
420         $testconf->add_changer('robo', [
421             tpchanger => "\"chg-robot:$mtx_state_file\"",
422             changerfile => "\"$chg_state_file\"",
423
424             # point to the two vtape "drives" that mock/mtx will set up
425             property => "\"tape-device\" \"0=file:$vtape_root/drive0\"",
426             property => "append \"tape-device\" \"1=file:$vtape_root/drive1\"",
427             property => "\"use-slots\" \"1-5\"",
428             property => "\"mtx\" \"$mock_mtx_path\"",
429             @ignore_barcodes,
430         ]);
431         $testconf->write();
432
433         config_uninit();
434         my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
435         if ($cfg_result != $CFGERR_OK) {
436             my ($level, @errors) = Amanda::Config::config_errors();
437             die(join "\n", @errors);
438         }
439
440         $steps->{'start'}->();
441     };
442
443     step start => sub {
444         $chg = Amanda::Changer->new("robo");
445         ok(!$chg->isa("Amanda::Changer::Error"),
446             "$pfx: Create working chg-robot instance")
447             or die("no sense going on: $chg");
448
449         $chg->info(info => [qw(vendor_string num_slots fast_search)], info_cb => $steps->{'info_cb'});
450     };
451
452     step info_cb => sub {
453         my ($err, %info) = @_;
454         die $err if $err;
455
456         is_deeply({ %info }, {
457             num_slots => 5,
458             fast_search => 1,
459             vendor_string => "COMPAQ SSL2000 Series",
460         }, "$pfx: info keys num_slots, fast_search, vendor_string are correct");
461
462         $steps->{'inventory1'}->();
463     };
464
465     step inventory1 => sub {
466         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_1'}, [
467             { slot => 1, state => Amanda::Changer::SLOT_FULL,
468               barcode => '11111', current => 1,
469               device_status => undef, device_error => undef,
470               f_type => undef, label => undef },
471             { slot => 2, state => Amanda::Changer::SLOT_FULL,
472               barcode => '22222',
473               device_status => undef, device_error => undef,
474               f_type => undef, label => undef },
475             { slot => 3, state => Amanda::Changer::SLOT_FULL,
476               barcode => '33333',
477               device_status => undef, device_error => undef,
478               f_type => undef, label => undef },
479             { slot => 4, state => Amanda::Changer::SLOT_FULL,
480               barcode => '44444',
481               device_status => undef, device_error => undef,
482               f_type => undef, label => undef },
483             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
484               device_status => undef, device_error => undef,
485               f_type => undef, label => undef },
486         ], "$pfx: inventory is correct on start-up");
487     };
488
489     step load_slot_1 => sub {
490         $chg->load(slot => 1, res_cb => $steps->{'loaded_slot_1'});
491     };
492
493     step loaded_slot_1 => sub {
494         (my $err, $res1) = @_;
495         die $err if $err;
496
497         is($res1->{'device'}->device_name, "file:$vtape_root/drive0",
498             "$pfx: first load returns drive-0 device");
499
500         is_deeply({
501                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
502                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
503             }, {
504                 loaded_in => 0,
505                 orig_slot => 1,
506             }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are correct");
507
508         $steps->{'load_slot_2'}->();
509     };
510
511     step load_slot_2 => sub {
512         $chg->load(slot => 2, res_cb => $steps->{'loaded_slot_2'});
513     };
514
515     step loaded_slot_2 => sub {
516         (my $err, $res2) = @_;
517         die $err if $err;
518
519         is($res2->{'device'}->device_name, "file:$vtape_root/drive1",
520             "$pfx: second load returns drive-1 device");
521
522         is_deeply({
523                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
524                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
525             }, {
526                 loaded_in => 0,
527                 orig_slot => 1,
528             }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are still correct");
529
530         is_deeply({
531                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
532                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
533             }, {
534                 loaded_in => 1,
535                 orig_slot => 2,
536             }, "$pfx: slot 2 'loaded_in' and drive 1 'orig_slot' are correct");
537
538         $steps->{'check_loads'}->();
539     };
540
541     step check_loads => sub {
542         # peek into the interface to check that things are loaded correctly
543         $chg->{'interface'}->status(sub {
544             my ($error, $status) = @_;
545
546             die $error if $error;
547
548             # only perform these checks when barcodes are enabled
549             if ($mtx_config->{'barcodes'} > 0) {
550                 is_deeply($status->{'drives'}, {
551                     0 => { barcode => '11111', 'orig_slot' => 1 },
552                     1 => { barcode => '22222', 'orig_slot' => 2 },
553                 }, "$pfx: double-check: loading drives with the changer gets the right drives loaded");
554             }
555
556             $steps->{'inventory2'}->();
557         });
558     };
559
560     step inventory2 => sub {
561         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'load_slot_3'}, [
562             { slot => 1, state => Amanda::Changer::SLOT_FULL,
563               barcode => '11111', reserved => 1, loaded_in => 0, current => 1,
564               device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
565               device_error => undef,
566               f_type => undef, label => undef },
567             { slot => 2, state => Amanda::Changer::SLOT_FULL,
568               barcode => '22222', reserved => 1, loaded_in => 1,
569               device_status => $DEVICE_STATUS_VOLUME_UNLABELED,
570               device_error => undef,
571               f_type => undef, label => undef },
572             { slot => 3, state => Amanda::Changer::SLOT_FULL,
573               barcode => '33333',
574               device_status => undef, device_error => undef,
575               f_type => undef, label => undef },
576             { slot => 4, state => Amanda::Changer::SLOT_FULL,
577               barcode => '44444',
578               device_status => undef, device_error => undef,
579               f_type => undef, label => undef },
580             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
581               device_status => undef, device_error => undef,
582               f_type => undef, label => undef },
583         ], "$pfx: inventory is updated when slots are loaded");
584     };
585
586     step load_slot_3 => sub {
587         $chg->load(slot => 3, res_cb => $steps->{'loaded_slot_3'});
588     };
589
590     step loaded_slot_3 => sub {
591         my ($err, $no_res) = @_;
592
593         chg_err_like($err,
594             { message => "no drives available",
595               reason => 'driveinuse',
596               type => 'failed' },
597             "$pfx: trying to load a third slot fails with 'no drives available'");
598
599         $steps->{'label_tape_1'}->();
600     };
601
602     step label_tape_1 => sub {
603         $res1->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-1", undef);
604         $res1->{'device'}->finish();
605
606         $res1->set_label(label => "TAPE-1", finished_cb => $steps->{'label_tape_2'});
607     };
608
609     step label_tape_2 => sub {
610         my ($err) = @_;
611         die $err if $err;
612
613         pass("$pfx: labeled TAPE-1 in drive 0");
614
615         is_deeply({
616                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
617                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
618                 slot_label => $chg->{'__last_state'}->{'slots'}->{1}->{'label'},
619                 drive_label => $chg->{'__last_state'}->{'drives'}->{0}->{'label'},
620             }, {
621                 loaded_in => 0,
622                 orig_slot => 1,
623                 slot_label => 'TAPE-1',
624                 drive_label => 'TAPE-1',
625             }, "$pfx: label is correctly reflected in changer state");
626
627         is_deeply({
628                 slot_2_loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
629                 slot_1_loaded_in => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
630             }, {
631                 slot_2_loaded_in => 1,
632                 slot_1_loaded_in => 2,
633             },
634             "$pfx: slot 2 'loaded_in' and drive 1 'orig_slot' are correct");
635
636         $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-2", undef);
637         $res2->{'device'}->finish();
638
639         $res2->set_label(label => "TAPE-2", finished_cb => $steps->{'release1'});
640     };
641
642     step release1 => sub {
643         my ($err) = @_;
644         die $err if $err;
645
646         pass("$pfx: labeled TAPE-2 in drive 1");
647
648         is_deeply({
649                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
650                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
651                 slot_label => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
652                 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
653             }, {
654                 loaded_in => 1,
655                 orig_slot => 2,
656                 slot_label => 'TAPE-2',
657                 drive_label => 'TAPE-2',
658             }, "$pfx: label is correctly reflected in changer state");
659
660         $res2->release(finished_cb => $steps->{'inventory3'});
661     };
662
663     step inventory3 => sub {
664         my ($err) = @_;
665         die "$err" if $err;
666         pass("$pfx: slot 2/drive 1 released");
667
668         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'check_state_after_release1'}, [
669             { slot => 1, state => Amanda::Changer::SLOT_FULL,
670               barcode => '11111', reserved => 1, loaded_in => 0, current => 1,
671               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
672               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
673             { slot => 2, state => Amanda::Changer::SLOT_FULL,
674               barcode => '22222', loaded_in => 1,
675               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
676               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
677             { slot => 3, state => Amanda::Changer::SLOT_FULL,
678               barcode => '33333',
679               device_status => undef, device_error => undef,
680               f_type => undef, label => undef },
681             { slot => 4, state => Amanda::Changer::SLOT_FULL,
682               barcode => '44444',
683               device_status => undef, device_error => undef,
684               f_type => undef, label => undef },
685             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
686               device_status => undef, device_error => undef,
687               f_type => undef, label => undef },
688         ], "$pfx: inventory is still up to date");
689     };
690
691     step check_state_after_release1 => sub {
692         is($chg->{'__last_state'}->{'drives'}->{1}->{'res_info'}, undef,
693                 "$pfx: drive is not reserved");
694         is($chg->{'__last_state'}->{'drives'}->{1}->{'label'}, 'TAPE-2',
695                 "$pfx: tape is still in drive");
696
697         $steps->{'load_current_1'}->();
698     };
699
700     step load_current_1 => sub {
701         $chg->load(relative_slot => "current", res_cb => $steps->{'loaded_current_1'});
702     };
703
704     step loaded_current_1 => sub {
705         my ($err, $res) = @_;
706
707         chg_err_like($err,
708             { message => "the requested volume is in use (drive 0)",
709               reason => 'volinuse',
710               type => 'failed' },
711             "$pfx: loading 'current' when set_current hasn't been used yet gets slot 1 (which is in use)");
712
713         $steps->{'load_slot_4'}->();
714     };
715
716     # this should unload what's in drive 1 and load the empty volume in slot 4
717     step load_slot_4 => sub {
718         $chg->load(slot => 4, set_current => 1, res_cb => $steps->{'loaded_slot_4'});
719     };
720
721     step loaded_slot_4 => sub {
722         (my $err, $res2) = @_;
723         die "$err" if $err;
724
725         is($res2->{'device'}->device_name, "file:$vtape_root/drive1",
726             "$pfx: loaded slot 4 into drive 1 (and set current to slot 4)");
727
728         is_deeply({
729                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
730                 slot_label => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
731             }, {
732                 loaded_in => undef,
733                 slot_label => 'TAPE-2',
734             }, "$pfx: slot 2 (which was just unloaded) still tracked correctly");
735
736         is_deeply({
737                 loaded_in => $chg->{'__last_state'}->{'slots'}->{1}->{'loaded_in'},
738                 orig_slot => $chg->{'__last_state'}->{'drives'}->{0}->{'orig_slot'},
739             }, {
740                 loaded_in => 0,
741                 orig_slot => 1,
742             }, "$pfx: slot 1 'loaded_in' and drive 0 'orig_slot' are *still* correct");
743
744         is_deeply({
745                 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
746                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
747             }, {
748                 loaded_in => 1,
749                 orig_slot => 4,
750             }, "$pfx: slot 4 'loaded_in' and drive 1 'orig_slot' are correct");
751
752         $steps->{'label_tape_4'}->();
753     };
754
755     step label_tape_4 => sub {
756         $res2->{'device'}->start($Amanda::Device::ACCESS_WRITE, "TAPE-4", undef);
757         $res2->{'device'}->finish();
758
759         $res2->set_label(label => "TAPE-4", finished_cb => $steps->{'inventory4'});
760     };
761
762     step inventory4 => sub {
763         my ($err) = @_;
764         die "$err" if $err;
765         pass("$pfx: labeled TAPE-4 in drive 1");
766
767         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'release2'}, [
768             { slot => 1, state => Amanda::Changer::SLOT_FULL,
769               barcode => '11111',
770               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
771               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1',
772               reserved => 1, loaded_in => 0 },
773             { slot => 2, state => Amanda::Changer::SLOT_FULL,
774               barcode => '22222',
775               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
776               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
777             { slot => 3, state => Amanda::Changer::SLOT_FULL,
778               barcode => '33333',
779               device_status => undef, device_error => undef,
780               f_type => undef, label => undef },
781             { slot => 4, state => Amanda::Changer::SLOT_FULL,
782               barcode => '44444', reserved => 1, loaded_in => 1, current => 1,
783               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
784               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
785             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
786               device_status => undef, device_error => undef,
787               f_type => undef, label => undef },
788         ], "$pfx: inventory is up to date after more labelings");
789     };
790
791     step release2 => sub {
792         is_deeply({
793                 loaded_in => $chg->{'__last_state'}->{'slots'}->{4}->{'loaded_in'},
794                 orig_slot => $chg->{'__last_state'}->{'drives'}->{1}->{'orig_slot'},
795                 slot_label => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
796                 drive_label => $chg->{'__last_state'}->{'drives'}->{1}->{'label'},
797             }, {
798                 loaded_in => 1,
799                 orig_slot => 4,
800                 slot_label => 'TAPE-4',
801                 drive_label => 'TAPE-4',
802             }, "$pfx: label is correctly reflected in changer state");
803
804         $res1->release(finished_cb => $steps->{'release2_done'});
805     };
806
807     step release2_done => sub {
808         my ($err) = @_;
809         die $err if $err;
810
811         pass("$pfx: slot 1/drive 0 released");
812
813         is($chg->{'__last_state'}->{'drives'}->{0}->{'label'}, 'TAPE-1',
814                 "$pfx: tape is still in drive");
815
816         $steps->{'release3'}->();
817     };
818
819     step release3 => sub {
820         my ($err) = @_;
821         die $err if $err;
822
823         $res2->release(finished_cb => $steps->{'release3_done'});
824     };
825
826     step release3_done => sub {
827         my ($err) = @_;
828         die $err if $err;
829
830         pass("$pfx: slot 4/drive 0 released");
831
832         is($chg->{'__last_state'}->{'drives'}->{1}->{'label'},
833                 'TAPE-4', "$pfx: tape is still in drive");
834
835         $steps->{'load_preloaded_by_slot'}->();
836     };
837
838     # try loading a slot that's already in a drive
839     step load_preloaded_by_slot => sub {
840         $chg->load(slot => 1, res_cb => $steps->{'loaded_preloaded_by_slot'});
841     };
842
843     step loaded_preloaded_by_slot => sub {
844         (my $err, $res1) = @_;
845         die $err if $err;
846
847         is($res1->{'device'}->device_name, "file:$vtape_root/drive0",
848             "$pfx: loading a tape (by slot) that's already in a drive returns that drive");
849
850         $res1->release(finished_cb => $steps->{'load_preloaded_by_label'});
851     };
852
853     # try again, this time by label
854     step load_preloaded_by_label => sub {
855         pass("$pfx: slot 1/drive 0 released");
856
857         $chg->load(label => 'TAPE-4', res_cb => $steps->{'loaded_preloaded_by_label'});
858     };
859
860     step loaded_preloaded_by_label => sub {
861         (my $err, $res1) = @_;
862         die $err if $err;
863
864         is($res1->{'device'}->device_name, "file:$vtape_root/drive1",
865             "$pfx: loading a tape (by label) that's already in a drive returns that drive");
866
867         $res1->release(finished_cb => $steps->{'load_unloaded_by_label'});
868     };
869
870     # test out searching by label
871     step load_unloaded_by_label => sub {
872         my ($err) = @_;
873         die $err if $err;
874
875         pass("$pfx: slot 4/drive 1 released");
876
877         $chg->load(label => 'TAPE-2', res_cb => $steps->{'loaded_unloaded_by_label'});
878     };
879
880     step loaded_unloaded_by_label => sub {
881         (my $err, $res1) = @_;
882         die $err if $err;
883
884         $res1->{'device'}->read_label();
885         is($res1->{'device'}->volume_label, "TAPE-2",
886             "$pfx: loading a tape (by label) that's *not* already in a drive returns " .
887             "the correct device");
888
889         $steps->{'release4'}->();
890     };
891
892     step release4 => sub {
893         $res1->release(finished_cb => $steps->{'release4_done'}, eject => 1);
894     };
895
896     step release4_done => sub {
897         my ($err) = @_;
898         die $err if $err;
899
900         pass("$pfx: slot 2/drive 0 released");
901
902         is_deeply({
903                 loaded_in => $chg->{'__last_state'}->{'slots'}->{2}->{'loaded_in'},
904                 slot_label => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
905                 drive_label => $chg->{'__last_state'}->{'drives'}->{0}->{'label'},
906             }, {
907                 loaded_in => undef,
908                 slot_label => 'TAPE-2',
909                 drive_label => undef,
910             }, "$pfx: and TAPE-2 ejected");
911
912         $steps->{'load_current_2'}->();
913     };
914
915     step load_current_2 => sub {
916         $chg->load(relative_slot => "current", res_cb => $steps->{'loaded_current_2'});
917     };
918
919     step loaded_current_2 => sub {
920         (my $err, $res1) = @_;
921         die $err if $err;
922
923         $res1->{'device'}->read_label();
924         is($res1->{'device'}->volume_label, "TAPE-4",
925             "$pfx: loading 'current' returns the correct device");
926
927         $steps->{'release5'}->();
928     };
929
930     step release5 => sub {
931         $res1->release(finished_cb => $steps->{'load_slot_next'});
932     };
933
934     step load_slot_next => sub {
935         my ($err) = @_;
936         die $err if $err;
937
938         pass("$pfx: slot 4/drive 1 released");
939
940         $chg->load(relative_slot => "next", res_cb => $steps->{'loaded_slot_next'});
941     };
942
943     step loaded_slot_next => sub {
944         (my $err, $res1) = @_;
945         die $err if $err;
946
947         $res1->{'device'}->read_label();
948         is($res1->{'device'}->volume_label, "TAPE-1",
949             "$pfx: loading 'next' returns the correct slot, skipping slot 5 and " .
950                     "looping around to the beginning");
951
952         $steps->{'load_res1_next_slot'}->();
953     };
954
955     step load_res1_next_slot => sub {
956         $chg->load(relative_slot => "next", slot => $res1->{'this_slot'},
957                    res_cb => $steps->{'loaded_res1_next_slot'});
958     };
959
960     step loaded_res1_next_slot => sub {
961         (my $err, $res2) = @_;
962         die $err if $err;
963
964         $res2->{'device'}->read_label();
965         is($res2->{'device'}->volume_label, "TAPE-2",
966             "$pfx: \$res->{this_slot} + 'next' returns the correct slot, too");
967         if ($mtx_config->{'barcodes'} == 1) {
968             is($res2->{'barcode'}, '22222',
969                 "$pfx: result has a barcode");
970         }
971
972         $steps->{'release6'}->();
973     };
974
975     step release6 => sub {
976         $res1->release(finished_cb => $steps->{'release7'});
977     };
978
979     step release7 => sub {
980         my ($err) = @_;
981         die "$err" if $err;
982
983         pass("$pfx: slot 1 released");
984
985         $res2->release(finished_cb => $steps->{'load_disallowed_slot'});
986     };
987
988     step load_disallowed_slot => sub {
989         my ($err) = @_;
990         die $err if $err;
991
992         pass("$pfx: slot 2 released");
993
994         $chg->load(slot => 6, res_cb => $steps->{'loaded_disallowed_slot'});
995     };
996
997     step loaded_disallowed_slot => sub {
998         (my $err, $res1) = @_;
999
1000         chg_err_like($err,
1001             { message => "slot 6 not in use-slots (1-5)",
1002               reason => 'invalid',
1003               type => 'failed' },
1004             "$pfx: loading a disallowed slot fails propertly");
1005
1006         $steps->{'inventory5'}->();
1007     };
1008
1009     step inventory5 => sub {
1010         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'try_update'}, [
1011             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1012               barcode => '11111', loaded_in => 1,
1013               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
1014               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1015             { slot => 2, state => Amanda::Changer::SLOT_FULL,
1016               barcode => '22222', loaded_in => 0,
1017               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
1018               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
1019             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1020               barcode => '33333',
1021               device_status => undef, device_error => undef,
1022               f_type => undef, label => undef },
1023             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1024               barcode => '44444', current => 1,
1025               device_status => $DEVICE_STATUS_SUCCESS, device_error => undef,
1026               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1027             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
1028               device_status => undef, device_error => undef,
1029               f_type => undef, label => undef },
1030         ], "$pfx: inventory still accurate");
1031     };
1032
1033     step try_update => sub {
1034         # first, add a label in slot 3, which hasn't been written
1035         # to yet
1036         my $dev = Amanda::Device->new("file:$vtape_root/slot3");
1037         die $dev->error_or_status()
1038             unless $dev->status == 0;
1039         die "error writing label"
1040             unless $dev->start($Amanda::Device::ACCESS_WRITE, "TAPE-3", undef);
1041         $dev->finish();
1042
1043         # now update that slot
1044         $chg->update(changed => "2-4", finished_cb => $steps->{'update_finished'});
1045     };
1046
1047     step update_finished => sub {
1048         my ($err) = @_;
1049         die "$err" if $err;
1050
1051         # verify that slots 2, 3, and 4 have correct info now
1052         is_deeply({
1053                 slot_2 => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
1054                 slot_3 => $chg->{'__last_state'}->{'slots'}->{3}->{'label'},
1055                 slot_4 => $chg->{'__last_state'}->{'slots'}->{4}->{'label'},
1056             }, {
1057                 slot_2 => 'TAPE-2',
1058                 slot_3 => 'TAPE-3',
1059                 slot_4 => 'TAPE-4',
1060             }, "$pfx: update correctly finds new label in slot 3");
1061
1062         # and check barcodes otherwise
1063         if ($mtx_config->{'barcodes'} > 0) {
1064             is_deeply({
1065                     barcode_2 => $chg->{'__last_state'}->{'bc2lb'}->{'22222'},
1066                     barcode_3 => $chg->{'__last_state'}->{'bc2lb'}->{'33333'},
1067                     barcode_4 => $chg->{'__last_state'}->{'bc2lb'}->{'44444'},
1068                 }, {
1069                     barcode_2 => 'TAPE-2',
1070                     barcode_3 => 'TAPE-3',
1071                     barcode_4 => 'TAPE-4',
1072                 }, "$pfx: bc2lb is correct, too");
1073         }
1074
1075         $steps->{'try_update2'}->();
1076     };
1077
1078     step try_update2 => sub {
1079         # lie about slot 2
1080         $chg->update(changed => "2=SURPRISE!", finished_cb => $steps->{'update_finished2'});
1081     };
1082
1083     step update_finished2 => sub {
1084         my ($err) = @_;
1085         die "$err" if $err;
1086
1087         # verify the new slot info
1088         is_deeply({
1089                 slot_2 => $chg->{'__last_state'}->{'slots'}->{2}->{'label'},
1090             }, {
1091                 slot_2 => 'SURPRISE!',
1092             }, "$pfx: assignment-style update correctly sets new label in slot 2");
1093
1094         # and check barcodes otherwise
1095         if ($mtx_config->{'barcodes'} > 0) {
1096             is_deeply({
1097                     barcode_2 => $chg->{'__last_state'}->{'bc2lb'}->{'22222'},
1098                 }, {
1099                     barcode_2 => 'SURPRISE!',
1100                 }, "$pfx: bc2lb is correct, too");
1101         }
1102
1103         $steps->{'try_update3'}->();
1104     };
1105
1106     step try_update3 => sub {
1107         # lie about slot 2
1108         $chg->update(changed => "5=NO!", finished_cb => $steps->{'update_finished3'});
1109     };
1110
1111     step update_finished3 => sub {
1112         my ($err) = @_;
1113         chg_err_like($err,
1114             { message => "slot 5 is empty",
1115               reason => 'unknown',
1116               type => 'failed' },
1117             "$pfx: assignment-style update of an empty slot gives error");
1118
1119         $steps->{'inventory6'}->();
1120     };
1121
1122     step inventory6 => sub {
1123         # note that the loading behavior of update() is not required, so the loaded_in
1124         # keys here may change if update() gets smarter
1125         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'move1'}, [
1126             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1127               barcode => '11111',
1128               device_status => $DEVICE_STATUS_SUCCESS,
1129               device_error => undef,
1130               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1131             { slot => 2, state => Amanda::Changer::SLOT_FULL,
1132               barcode => '22222',
1133               device_status => $DEVICE_STATUS_SUCCESS,
1134               device_error => undef,
1135               f_type => $Amanda::Header::F_TAPESTART, label => 'SURPRISE!' },
1136             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1137               barcode => '33333', loaded_in => 1,
1138               device_status => $DEVICE_STATUS_SUCCESS, 
1139               device_error => undef,
1140               f_type => undef, label => 'TAPE-3' },
1141             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1142               barcode => '44444', loaded_in => 0, current => 1,
1143               device_status => $DEVICE_STATUS_SUCCESS,
1144               device_error => undef,
1145               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1146             { slot => 5, state => Amanda::Changer::SLOT_EMPTY,
1147               device_status => undef, device_error => undef,
1148               f_type => undef, label => undef },
1149         ], "$pfx: inventory reflects updates");
1150     };
1151
1152     step move1 => sub {
1153         # move to a full slot
1154         $chg->move(from_slot => 2, to_slot => 1, finished_cb => $steps->{'moved1'});
1155     };
1156
1157     step moved1 => sub {
1158         my ($err) = @_;
1159
1160         chg_err_like($err,
1161             { message => "slot 1 is not empty",
1162               reason => 'invalid',
1163               type => 'failed' },
1164             "$pfx: moving to a full slot is an error");
1165
1166         $steps->{'move2'}->();
1167     };
1168
1169     step move2 => sub {
1170         # move to a full slot that's loaded (so there's not *actually* a tape
1171         # in the slot)
1172         $chg->move(from_slot => 2, to_slot => 3, finished_cb => $steps->{'moved2'});
1173     };
1174
1175     step moved2 => sub {
1176         my ($err) = @_;
1177
1178         chg_err_like($err,
1179             { message => "slot 3 is not empty",
1180               reason => 'invalid',
1181               type => 'failed' },
1182             "$pfx: moving to a full slot is an error even if that slot is loaded");
1183
1184         $steps->{'move3'}->();
1185     };
1186
1187     step move3 => sub {
1188         # move from an empty slot
1189         $chg->move(from_slot => 5, to_slot => 3, finished_cb => $steps->{'moved3'});
1190     };
1191
1192     step moved3 => sub {
1193         my ($err) = @_;
1194
1195         chg_err_like($err,
1196             { message => "slot 5 is empty", # note that this depends on the order of checks..
1197               reason => 'invalid',
1198               type => 'failed' },
1199             "$pfx: moving from an empty slot is an error");
1200
1201         $steps->{'move4'}->();
1202     };
1203
1204     step move4 => sub {
1205         # move from a loaded slot to an empty slot
1206         $chg->move(from_slot => 4, to_slot => 5, finished_cb => $steps->{'moved4'});
1207     };
1208
1209     step moved4 => sub {
1210         my ($err) = @_;
1211         die "$err" if $err;
1212
1213         pass("$pfx: move of a loaded volume succeeds");
1214
1215         $steps->{'move5'}->();
1216     };
1217
1218     step move5 => sub {
1219         $chg->move(from_slot => 2, to_slot => 4, finished_cb => $steps->{'inventory7'});
1220     };
1221
1222
1223     step inventory7 => sub {
1224         my ($err) = @_;
1225         die $err if $err;
1226
1227         pass("$pfx: move succeeds");
1228
1229         # note that the loading behavior of update() is not required, so the loaded_in
1230         # keys here may change if update() gets smarter
1231         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'start_scan'}, [
1232             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1233               barcode => '11111',
1234               device_status => $DEVICE_STATUS_SUCCESS,
1235               device_error => undef,
1236               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1237             { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1238               device_status => undef, device_error => undef,
1239               f_type => undef, label => undef },
1240             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1241               barcode => '33333', loaded_in => 1,
1242               device_status => $DEVICE_STATUS_SUCCESS,
1243               device_error => undef,
1244               f_type => undef, label => 'TAPE-3' },
1245             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1246               barcode => '22222', current => 1,
1247               device_status => $DEVICE_STATUS_SUCCESS,
1248               device_error => undef,
1249               f_type => $Amanda::Header::F_TAPESTART, label => 'SURPRISE!' },
1250             { slot => 5, state => Amanda::Changer::SLOT_FULL,
1251               barcode => '44444',
1252               device_status => $DEVICE_STATUS_SUCCESS,
1253               device_error => undef,
1254               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1255         ], "$pfx: inventory reflects the move");
1256     };
1257
1258     # test a scan, using except_slots
1259     my %except_slots;
1260
1261     step start_scan => sub {
1262         $chg->load(relative_slot => "current", except_slots => { %except_slots },
1263                    res_cb => $steps->{'loaded_for_scan'});
1264     };
1265
1266     step loaded_for_scan => sub {
1267         (my $err, $res1) = @_;
1268         my $slot;
1269         if ($err) {
1270             if ($err->notfound) {
1271                 return $steps->{'scan_done'}->();
1272             } elsif ($err->volinuse and defined $err->{'slot'}) {
1273                 $slot = $err->{'slot'};
1274             } else {
1275                 die $err;
1276             }
1277         } else {
1278             $slot = $res1->{'this_slot'};
1279         }
1280
1281         $except_slots{$slot} = 1;
1282
1283         $res1->release(finished_cb => $steps->{'released_for_scan'});
1284     };
1285
1286     step released_for_scan => sub {
1287         my ($err) = @_;
1288         die $err if $err;
1289
1290         $chg->load(relative_slot => 'next', slot => $res1->{'this_slot'},
1291                    except_slots => { %except_slots },
1292                    res_cb => $steps->{'loaded_for_scan'});
1293     };
1294
1295     step scan_done => sub {
1296         is_deeply({ %except_slots }, { 4=>1, 5=>1, 1=>1, 3=>1 },
1297                 "$pfx: scanning with except_slots works");
1298         check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'update_unknown'}, [
1299             { slot => 1, state => Amanda::Changer::SLOT_FULL,
1300               barcode => '11111', loaded_in => 1,
1301               device_status => $DEVICE_STATUS_SUCCESS,
1302               device_error => undef,
1303               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1304             { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1305               device_status => undef, device_error => undef,
1306               f_type => undef, label => undef },
1307             { slot => 3, state => Amanda::Changer::SLOT_FULL,
1308               barcode => '33333', loaded_in => 0,
1309               device_status => $DEVICE_STATUS_SUCCESS,
1310               device_error => undef,
1311               f_type => undef, label => 'TAPE-3' },
1312             { slot => 4, state => Amanda::Changer::SLOT_FULL,
1313               barcode => '22222', current => 1,
1314               device_status => $DEVICE_STATUS_SUCCESS,
1315               device_error => undef,
1316               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
1317             { slot => 5, state => Amanda::Changer::SLOT_FULL,
1318               barcode => '44444',
1319               device_status => $DEVICE_STATUS_SUCCESS,
1320               device_error => undef,
1321               f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1322         ], "$pfx: inventory before updates with unknown state");
1323     };
1324
1325     step update_unknown => sub {
1326         $chg->update(changed => "3-4=", finished_cb => $steps->{'update_unknown_finished'});
1327     };
1328
1329     step update_unknown_finished => sub {
1330         my ($err) = @_;
1331         die "$err" if $err;
1332
1333         if ($mtx_config->{'barcodes'} > 0) {
1334             check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'quit'}, [
1335                 { slot => 1, state => Amanda::Changer::SLOT_FULL,
1336                   barcode => '11111', loaded_in => 1,
1337                   device_status => $DEVICE_STATUS_SUCCESS,
1338                   device_error => undef,
1339                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1340                 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1341                   device_status => undef, device_error => undef,
1342                   f_type => undef, label => undef },
1343                 { slot => 3, state => Amanda::Changer::SLOT_FULL,
1344                   barcode => '33333', loaded_in => 0,
1345                   device_status => $DEVICE_STATUS_SUCCESS,
1346                   device_error => undef,
1347                   f_type => undef, label => 'TAPE-3' },
1348                 { slot => 4, state => Amanda::Changer::SLOT_FULL,
1349                   barcode => '22222', current => 1,
1350                   device_status => $DEVICE_STATUS_SUCCESS,
1351                   device_error => undef,
1352                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-2' },
1353                 { slot => 5, state => Amanda::Changer::SLOT_FULL,
1354                   barcode => '44444',
1355                   device_status => $DEVICE_STATUS_SUCCESS,
1356                   device_error => undef,
1357                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1358             ], "$pfx: inventory reflects updates with unknown state with barcodes");
1359         } else {
1360             check_inventory($chg, $mtx_config->{'barcodes'} > 0, $steps->{'quit'}, [
1361                 { slot => 1, state => Amanda::Changer::SLOT_FULL,
1362                   barcode => '11111', loaded_in => 1,
1363                   device_status => $DEVICE_STATUS_SUCCESS,
1364                   device_error => undef,
1365                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-1' },
1366                 { slot => 2, state => Amanda::Changer::SLOT_EMPTY,
1367                   device_status => undef, device_error => undef,
1368                   f_type => undef, label => undef },
1369                 { slot => 3, state => Amanda::Changer::SLOT_FULL,
1370                   barcode => '33333', loaded_in => 0,
1371                   device_status => undef, device_error => undef,
1372                   f_type => undef, label => undef },
1373                 { slot => 4, state => Amanda::Changer::SLOT_FULL,
1374                   barcode => '22222', current => 1,
1375                   device_status => undef, device_error => undef,
1376                   f_type => undef, label => undef },
1377                 { slot => 5, state => Amanda::Changer::SLOT_FULL,
1378                   barcode => '44444',
1379                   device_status => $DEVICE_STATUS_SUCCESS,
1380                   device_error => undef,
1381                   f_type => $Amanda::Header::F_TAPESTART, label => 'TAPE-4' },
1382             ], "$pfx: inventory reflects updates with unknown state without barcodes");
1383         }
1384     };
1385
1386     step quit => sub {
1387         unlink($chg_state_file) if -f $chg_state_file;
1388         unlink($mtx_state_file) if -f $mtx_state_file;
1389         rmtree($vtape_root);
1390
1391         $finished_cb->();
1392     };
1393 }
1394
1395 # These tests are run over a number of different mtx configurations, to ensure
1396 # that the behavior is identical regardless of the changer/mtx characteristics
1397 for my $mtx_config (
1398     { barcodes => 1, track_orig => 1, },
1399     { barcodes => 0, track_orig => 1, },
1400     { barcodes => 1, track_orig => -1, },
1401     { barcodes => 0, track_orig => 0, },
1402     { barcodes => -1, track_orig => 0, },
1403     ) {
1404     test_changer($mtx_config, \&Amanda::MainLoop::quit);
1405     Amanda::MainLoop::run();
1406 }