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