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