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