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