Imported Upstream version 3.1.0
[debian/amanda] / installcheck / Amanda_Changer.pl
index d2ce81b5ac8f31b49764c6b5de7012611d223102..a626e7370af878a86b1c6886a519a0ab23e6da67 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2005-2008 Zmanda Inc.  All Rights Reserved.
+# Copyright (c) 2007, 2008, 2009, 2010 Zmanda, Inc.  All Rights Reserved.
 #
 # This program is free software; you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 2 as published
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 #
-# Contact information: Zmanda Inc, 465 S Mathlida Ave, Suite 300
+# Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
 
-use Test::More tests => 35;
+use Test::More tests => 47;
 use File::Path;
+use Data::Dumper;
 use strict;
 
 use lib "@amperldir@";
@@ -31,6 +32,7 @@ use Amanda::Changer;
 
 # set up debugging so debug output doesn't interfere with test results
 Amanda::Debug::dbopen("installcheck");
+Installcheck::log_test_output();
 
 # and disable Debug's die() and warn() overrides
 Amanda::Debug::disable_die_override();
@@ -48,9 +50,10 @@ $INC{'Amanda/Changer/test.pm'} = "Amanda_Changer";
 
 sub new {
     my $class = shift;
-    my ($cc, $tpchanger) = @_;
+    my ($config, $tpchanger) = @_;
 
     my $self = {
+       config => $config,
        curslot => 0,
        slots => [ 'TAPE-00', 'TAPE-01', 'TAPE-02', 'TAPE-03' ],
        reserved_slots => [],
@@ -98,10 +101,17 @@ sub load {
         }
 
        $cb->(undef, Amanda::Changer::test::Reservation->new($self, $slot, $label));
-    } elsif (exists $params{'slot'}) {
+    } elsif (exists $params{'slot'} or exists $params{'relative_slot'}) {
        my $slot = $params{'slot'};
-       $slot = $self->{'curslot'}
-           if ($slot eq "current");
+       if (exists $params{'relative_slot'}) {
+           if ($params{'relative_slot'} eq "current") {
+               $slot = $self->{'curslot'};
+           } elsif ($params{'relative_slot'} eq "next") {
+               $slot = ($self->{'curslot'} + 1) % (scalar @{$self->{'slots'}});
+           } else {
+               die "invalid relative_slot";
+           }
+       }
 
        if (grep { $_ == $slot } @{$self->{'reserved_slots'}}) {
            $cb->("Slot $slot is already in use", undef);
@@ -120,15 +130,33 @@ sub load {
     }
 }
 
+sub info_key {
+    my $self = shift;
+    my ($key, %params) = @_;
+    my %results;
+
+    if ($key eq 'num_slots') {
+       $results{$key} = 13;
+    } elsif ($key eq 'mkerror1') {
+       return $self->make_error("failed", $params{'info_cb'},
+           reason => "unknown",
+           message => "err1");
+    } elsif ($key eq 'mkerror2') {
+       return $self->make_error("failed", $params{'info_cb'},
+           reason => "unknown",
+           message => "err2");
+    }
+
+    $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
+}
+
 sub reset {
     my $self = shift;
     my %params = @_;
 
     $self->{'curslot'} = 0;
 
-    if (exists $params{'finished_cb'}) {
-       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
-    }
+    $params{'finished_cb'}->(undef) if $params{'finished_cb'};
 }
 
 sub clean {
@@ -137,11 +165,24 @@ sub clean {
 
     $self->{'clean'} = 1;
 
-    if (exists $params{'finished_cb'}) {
-       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
-    }
+    $params{'finished_cb'}->(undef) if $params{'finished_cb'};
 }
 
+sub inventory {
+    my $self = shift;
+    my %params = @_;
+
+    Amanda::MainLoop::call_later($params{'inventory_cb'},
+       undef, [ {
+           slot => 1,
+           empty => 0,
+           label => 'TAPE-99',
+           barcode => '09385A',
+           reserved => 0,
+           import_export => 0,
+           loaded_in => undef,
+       }]);
+}
 
 package Amanda::Changer::test::Reservation;
 use vars qw( @ISA );
@@ -156,14 +197,13 @@ sub new {
     $self->{'slot'} = $slot;
     $self->{'label'} = $label;
 
-    $self->{'device_name'} = "test:slot-$slot";
+    $self->{'device'} = Amanda::Device->new("null:slot-$slot");
     $self->{'this_slot'} = $slot;
-    $self->{'next_slot'} = ($slot + 1) % (scalar @{$chg->{'slots'}});
 
     return $self;
 }
 
-sub release {
+sub do_release {
     my $self = shift;
     my %params = @_;
     my $slot = $self->{'slot'};
@@ -171,9 +211,7 @@ sub release {
 
     $chg->{'reserved_slots'} = [ grep { $_ != $slot } @{$chg->{'reserved_slots'}} ];
 
-    if (exists $params{'finished_cb'}) {
-       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
-    }
+    $params{'finished_cb'}->(undef) if $params{'finished_cb'};
 }
 
 sub set_label {
@@ -185,9 +223,7 @@ sub set_label {
     $self->{'chg'}->{'slots'}->[$self->{'slot'}] = $params{'label'};
     $self->{'label'} = $params{'label'};
 
-    if (exists $params{'finished_cb'}) {
-       Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
-    }
+    $params{'finished_cb'}->(undef) if $params{'finished_cb'};
 }
 
 # --------
@@ -201,6 +237,7 @@ my $testconf;
 $testconf = Installcheck::Config->new();
 $testconf->add_changer("mychanger", [
     'tpchanger' => '"chg-test:/foo"',
+    'property' => '"testprop" "testval"',
 ]);
 $testconf->write();
 
@@ -210,55 +247,75 @@ if ($cfg_result != $CFGERR_OK) {
     die(join "\n", @errors);
 }
 
-# test loading by label
-
+# check out the relevant changer properties
 my $chg = Amanda::Changer->new("mychanger");
+is($chg->{'config'}->get_property("testprop"), "testval",
+    "changer properties are correctly represented");
+
+# test loading by label
 {
-    my @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
-    my @reservations = ();
-    my $getres;
+    my @labels;
+    my @reservations;
+    my ($getres, $rq_reserved, $relres);
+
+    $getres = make_cb('getres' => sub {
+       if (!@labels) {
+           return $rq_reserved->();
+       }
 
-    $getres = sub {
        my $label = pop @labels;
 
        $chg->load(label => $label,
                    set_current => ($label eq "TAPE-02"),
                   res_cb => sub {
-           my ($err, $reservation) = @_;
+           my ($err, $res) = @_;
            ok(!$err, "no error loading $label")
                or diag($err);
 
            # keep this reservation
-           if ($reservation) {
-               push @reservations, $reservation;
-           }
+           push @reservations, $res if $res;
 
            # and start on the next
-           if (@labels) {
-               $getres->();
-               return;
-           } else {
-               # try to load an already-reserved volume
-               $chg->load(label => 'TAPE-00',
-                          res_cb => sub {
-                   my ($err, $reservation) = @_;
-                   ok($err, "error when requesting already-reserved volume");
-                   Amanda::MainLoop::quit();
-               });
-           }
+           $getres->();
        });
-    };
+    });
+
+    $rq_reserved = make_cb(rq_reserved => sub {
+       # try to load an already-reserved volume
+       $chg->load(label => 'TAPE-00',
+                  res_cb => sub {
+           my ($err, $res) = @_;
+           ok($err, "error when requesting already-reserved volume");
+           push @reservations, $res if $res;
+
+           $relres->();
+       });
+    });
+
+    $relres = make_cb('relres' => sub {
+       if (!@reservations) {
+           return Amanda::MainLoop::quit();
+       }
+
+       my $res = pop @reservations;
+       $res->release(finished_cb => sub {
+           my ($err) = @_;
+           die $err if $err;
+
+           $relres->();
+       });
+    });
 
     # start the loop
-    Amanda::MainLoop::call_later($getres);
+    @labels = ( 'TAPE-02', 'TAPE-00', 'TAPE-03' );
+    $getres->();
+    Amanda::MainLoop::run();
+
+    $relres->();
     Amanda::MainLoop::run();
 
-    # ditch the reservations and do it all again
-    @reservations = ();
     @labels = ( 'TAPE-00', 'TAPE-01' );
-    is_deeply($chg->{'reserved_slots'}, [],
-       "reservations are released when the Reservation object goes out of scope");
-    Amanda::MainLoop::call_later($getres);
+    $getres->();
     Amanda::MainLoop::run();
 
     # explicitly release the reservations (without using the callback)
@@ -269,29 +326,37 @@ my $chg = Amanda::Changer->new("mychanger");
 
 # test loading by slot
 {
-    my ($start, $first_cb, $second_cb);
+    my ($start, $first_cb, $released, $second_cb, $quit);
+    my $slot;
 
     # reserves the current slot
-    $start = sub {
-        $chg->load(res_cb => $first_cb, slot => "current");
-    };
+    $start = make_cb('start' => sub {
+        $chg->load(res_cb => $first_cb, relative_slot => "current");
+    });
 
     # gets a reservation for the "current" slot
-    $first_cb = sub {
+    $first_cb = make_cb('first_cb' => sub {
         my ($err, $res) = @_;
         die $err if $err;
 
         is($res->{'this_slot'}, 2,
             "'current' slot loads slot 2");
-        is($res->{'device_name'}, "test:slot-2",
+        is($res->{'device'}->device_name, "null:slot-2",
             "..device is correct");
-        is($res->{'next_slot'}, 3,
-            "..and the next slot is slot 3");
-        $chg->load(res_cb => $second_cb, slot => $res->{'next_slot'}, set_current => 1);
-    };
+
+       $slot = $res->{'this_slot'};
+       $res->release(finished_cb => $released);
+    });
+
+    $released = make_cb(released => sub {
+       my ($err) = @_;
+
+        $chg->load(res_cb => $second_cb, relative_slot => 'next',
+                  slot => $slot, set_current => 1);
+    });
 
     # gets a reservation for the "next" slot
-    $second_cb = sub {
+    $second_cb = make_cb('second_cb' => sub {
         my ($err, $res) = @_;
         die $err if $err;
 
@@ -299,86 +364,161 @@ my $chg = Amanda::Changer->new("mychanger");
             "next slot loads slot 3");
         is($chg->{'curslot'}, 3,
             "..which is also now the current slot");
-        is($res->{'next_slot'}, 0,
-            "..and the next slot is slot 0");
+
+       $res->release(finished_cb => $quit);
+    });
+
+    $quit = make_cb(quit => sub {
+       my ($err) = @_;
+       die $err if $err;
 
         Amanda::MainLoop::quit();
-    };
+    });
 
-    Amanda::MainLoop::call_later($start);
+    $start->();
     Amanda::MainLoop::run();
 }
 
 # test set_label
 {
-    my ($start, $load1_cb, $set_cb, $load2_cb, $load3_cb);
+    my ($start, $load1_cb, $set_cb, $released, $load2_cb, $released2, $load3_cb);
+    my $res;
 
     # load TAPE-00
-    $start = sub {
+    $start = make_cb('start' => sub {
         $chg->load(res_cb => $load1_cb, label => "TAPE-00");
-    };
+    });
 
     # rename it to TAPE-99
-    $load1_cb = sub {
-        my ($err, $res) = @_;
+    $load1_cb = make_cb('load1_cb' => sub {
+        (my $err, $res) = @_;
         die $err if $err;
 
         pass("loaded TAPE-00");
         $res->set_label(label => "TAPE-99", finished_cb => $set_cb);
-        $res->release();
-    };
+    });
+
+    $set_cb = make_cb('set_cb' => sub {
+       my ($err) = @_;
+
+       $res->release(finished_cb => $released);
+    });
 
     # try to load TAPE-00
-    $set_cb = sub {
+    $released = make_cb('released' => sub {
         my ($err) = @_;
         die $err if $err;
 
         pass("relabeled TAPE-00 to TAPE-99");
         $chg->load(res_cb => $load2_cb, label => "TAPE-00");
-    };
+    });
 
     # try to load TAPE-99
-    $load2_cb = sub {
-        my ($err, $res) = @_;
-
+    $load2_cb = make_cb('load2_cb' => sub {
+        (my $err, $res) = @_;
         ok($err, "loading TAPE-00 is now an error");
+
         $chg->load(res_cb => $load3_cb, label => "TAPE-99");
-    };
+    });
 
     # check result
-    $load3_cb = sub {
-        my ($err, $res) = @_;
+    $load3_cb = make_cb('load3_cb' => sub {
+        (my $err, $res) = @_;
         die $err if $err;
 
         pass("but loading TAPE-99 is ok");
 
+       $res->release(finished_cb => $released2);
+    });
+
+    $released2 = make_cb(released2 => sub {
+       my ($err) = @_;
+       die $err if $err;
+
         Amanda::MainLoop::quit();
-    };
+    });
 
-    Amanda::MainLoop::call_later($start);
+    $start->();
     Amanda::MainLoop::run();
 }
 
-# test reset and clean
-{
-    my ($do_reset, $do_clean);
+# test reset and clean and inventory
+sub test_simple {
+    my ($finished_cb) = @_;
+
+    my $steps = define_steps
+       cb_ref => \$finished_cb;
 
-    $do_reset = sub {
+    step do_reset => sub {
         $chg->reset(finished_cb => sub {
             is($chg->{'curslot'}, 0,
                 "reset() resets to slot 0");
-            $do_clean->();
+            $steps->{'do_clean'}->();
         });
     };
 
-    $do_clean = sub {
+    step do_clean => sub {
         $chg->clean(finished_cb => sub {
             ok($chg->{'clean'}, "clean 'cleaned' the changer");
-            Amanda::MainLoop::quit();
+           $steps->{'do_inventory'}->();
+        });
+    };
+
+    step do_inventory => sub {
+        $chg->inventory(inventory_cb => sub {
+           is_deeply($_[1], [ {
+                   slot => 1,
+                   empty => 0,
+                   label => 'TAPE-99',
+                   barcode => '09385A',
+                   reserved => 0,
+                   import_export => 0,
+                   loaded_in => undef,
+               }], "inventory returns an inventory");
+           $finished_cb->();
         });
     };
+}
+test_simple(\&Amanda::MainLoop::quit);
+Amanda::MainLoop::run();
 
-    Amanda::MainLoop::call_later($do_reset);
+# test info
+{
+    my ($do_info, $check_info, $do_info_err, $check_info_err);
+
+    $do_info = make_cb('do_info' => sub {
+        $chg->info(info_cb => $check_info,
+           info => [ 'num_slots' ]);
+    });
+
+    $check_info = make_cb('check_info' => sub {
+       my ($err, %results) = @_;
+       die($err) if $err;
+       is_deeply(\%results, { 'num_slots' => 13 },
+           "info() works");
+       $do_info_err->();
+    });
+
+    $do_info_err = make_cb('do_info_err' => sub {
+        $chg->info(info_cb => $check_info_err,
+           info => [ 'mkerror1', 'mkerror2' ]);
+    });
+
+    $check_info_err = make_cb('check_info_err' => sub {
+       my ($err, %results) = @_;
+       is($err,
+         "While getting info key 'mkerror1': err1; While getting info key 'mkerror2': err2",
+         "info errors are handled correctly");
+       is($err->{'type'}, 'failed', "error has type 'failed'");
+       ok($err->failed, "\$err->failed is true");
+       ok(!$err->fatal, "\$err->fatal is false");
+       is($err->{'reason'}, 'unknown', "\$err->{'reason'} is 'unknown'");
+       ok($err->unknown, "\$err->unknown is true");
+       ok(!$err->notimpl, "\$err->notimpl is false");
+       Amanda::MainLoop::quit();
+    });
+
+    $do_info->();
     Amanda::MainLoop::run();
 }
 
@@ -386,13 +526,14 @@ my $chg = Amanda::Changer->new("mychanger");
 # _new_from_uri so we can monitor the result
 sub my_new_from_uri {
     my ($uri, $cc, $name) = @_;
+    return $uri if (ref $uri and $uri->isa("Amanda::Changer::Error"));
     return [ $uri, $cc? "cc" : undef ];
 }
 *saved_new_from_uri = *Amanda::Changer::_new_from_uri;
 *Amanda::Changer::_new_from_uri = *my_new_from_uri;
 
 sub loadconfig {
-    my ($global_tapedev, $global_tpchanger, $defn_tpchanger) = @_;
+    my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn) = @_;
 
     $testconf = Installcheck::Config->new();
 
@@ -410,6 +551,11 @@ sub loadconfig {
        ]);
     }
 
+    if (defined($custom_defn)) {
+       $testconf->add_changer("customchanger", $custom_defn);
+       $testconf->add_param('tpchanger', '"customchanger"');
+    }
+
     $testconf->write();
 
     my $cfg_result = config_init($CONFIG_INIT_EXPLICIT_NAME, 'TESTCONF');
@@ -420,66 +566,150 @@ sub loadconfig {
 }
 
 sub assert_invalid {
-    my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $name, $msg) = @_;
-    loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger);
-    eval { Amanda::Changer->new($name); };
-    ok($@, $msg);
+    my ($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn,
+       $name, $regexp, $msg) = @_;
+    loadconfig($global_tapedev, $global_tpchanger, $defn_tpchanger, $custom_defn);
+    my $err = Amanda::Changer->new($name);
+    if ($err->isa("Amanda::Changer::Error")) {
+       like($err->{'message'}, $regexp, $msg);
+    } else {
+       diag("Amanda::Changer->new did not return an Error object:");
+       diag("".Dumper($err));
+       fail($msg);
+    }
 }
 
-assert_invalid(undef, undef, undef, undef,
+assert_invalid(undef, undef, undef, undef, undef,
+    qr/You must specify one of 'tapedev' or 'tpchanger'/,
     "supplying a nothing is invalid");
 
-loadconfig(undef, "file:/foo", undef);
+loadconfig(undef, "file:/foo", undef, undef);
 is_deeply( Amanda::Changer->new(), [ "chg-single:file:/foo", undef ],
     "default changer with global tpchanger naming a device");
 
-loadconfig(undef, "chg-disk:/foo", undef);
+loadconfig(undef, "chg-disk:/foo", undef, undef);
 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
     "default changer with global tpchanger naming a changer");
 
-loadconfig(undef, "mychanger", "chg-disk:/bar");
+loadconfig(undef, "mychanger", "chg-disk:/bar", undef);
 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
     "default changer with global tpchanger naming a defined changer with a uri");
 
-loadconfig(undef, "mychanger", "chg-zd-mtx");
+loadconfig(undef, "mychanger", "chg-zd-mtx", undef);
 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
     "default changer with global tpchanger naming a defined changer with a compat script");
 
-loadconfig(undef, "chg-zd-mtx", undef);
+loadconfig(undef, "chg-zd-mtx", undef, undef);
 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
     "default changer with global tpchanger naming a compat script");
 
-loadconfig("tape:/dev/foo", undef, undef);
+loadconfig("tape:/dev/foo", undef, undef, undef);
 is_deeply( Amanda::Changer->new(), [ "chg-single:tape:/dev/foo", undef ],
     "default changer with global tapedev naming a device and no tpchanger");
 
-assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef,
+assert_invalid("tape:/dev/foo", "tape:/dev/foo", undef, undef, undef,
+    qr/Cannot specify both 'tapedev' and 'tpchanger'/,
     "supplying a device for both tpchanger and tapedev is invalid");
 
-assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef,
+assert_invalid("tape:/dev/foo", "chg-disk:/foo", undef, undef, undef,
+    qr/Cannot specify both 'tapedev' and 'tpchanger'/,
     "supplying a device for tapedev and a changer for tpchanger is invalid");
 
-loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef);
+loadconfig("tape:/dev/foo", 'chg-zd-mtx', undef, undef);
 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", undef ],
     "default changer with global tapedev naming a device and a global tpchanger naming a compat script");
 
-assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef,
+assert_invalid("chg-disk:/foo", "tape:/dev/foo", undef, undef, undef,
+    qr/Cannot specify both 'tapedev' and 'tpchanger'/,
     "supplying a changer for tapedev and a device for tpchanger is invalid");
 
-loadconfig("chg-disk:/foo", undef, undef);
+loadconfig("chg-disk:/foo", undef, undef, undef);
 is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", undef ],
     "default changer with global tapedev naming a device");
 
-loadconfig("mychanger", undef, "chg-disk:/bar");
+loadconfig("mychanger", undef, "chg-disk:/bar", undef);
 is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
     "default changer with global tapedev naming a defined changer with a uri");
 
-loadconfig("mychanger", undef, "chg-zd-mtx");
+loadconfig("mychanger", undef, "chg-zd-mtx", undef);
 is_deeply( Amanda::Changer->new(), [ "chg-compat:chg-zd-mtx", "cc" ],
     "default changer with global tapedev naming a defined changer with a compat script");
 
-loadconfig(undef, undef, "chg-disk:/foo");
+loadconfig(undef, undef, "chg-disk:/foo", undef);
 is_deeply( Amanda::Changer->new("mychanger"), [ "chg-disk:/foo", "cc" ],
     "named changer loads the proper definition");
 
+loadconfig(undef, undef, undef, [
+    tapedev => '"chg-disk:/foo"',
+]);
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/foo", "cc" ],
+    "defined changer with tapedev loads the proper definition");
+
+loadconfig(undef, undef, undef, [
+    tpchanger => '"chg-disk:/bar"',
+]);
+is_deeply( Amanda::Changer->new(), [ "chg-disk:/bar", "cc" ],
+    "defined changer with tpchanger loads the proper definition");
+
+assert_invalid(undef, undef, undef, [
+       tpchanger => '"chg-disk:/bar"',
+       tapedev => '"file:/bar"',
+    ], undef,
+    qr/Cannot specify both 'tapedev' and 'tpchanger'/,
+    "supplying both a new tpchanger and tapedev in a definition is invalid");
+
+assert_invalid(undef, undef, undef, [
+       property => '"this" "will not work"',
+    ], undef,
+    qr/You must specify one of 'tapedev' or 'tpchanger'/,
+    "supplying neither a tpchanger nor tapedev in a definition is invalid");
+
 *Amanda::Changer::_new_from_uri = *saved_new_from_uri;
+
+# test with_locked_state *within* a process
+
+sub test_locked_state {
+    my ($finished_cb) = @_;
+    my $chg;
+    my $stfile = "$Installcheck::TMP/test-statefile";
+    my $num_outstanding = 0;
+
+    my $steps = define_steps
+       cb_ref => \$finished_cb;
+
+    step start => sub {
+       $chg = Amanda::Changer->new("chg-null:");
+
+       for my $num (qw( one two three )) {
+           ++$num_outstanding;
+           $chg->with_locked_state($stfile, $steps->{'maybe_done'}, sub {
+               my ($state, $maybe_done) = @_;
+
+               $state->{$num} = $num;
+               $state->{'count'}++;
+
+               Amanda::MainLoop::call_after(50, $maybe_done, undef, $state);
+           });
+       }
+    };
+
+    step maybe_done => sub {
+       my ($err, $state) = @_;
+       die $err if $err;
+
+       return if (--$num_outstanding);
+
+       is_deeply($state, {
+           one => "one",
+           two => "two",
+           three => "three",
+           count => 3,
+       }, "state is maintained correctly (within a process)");
+
+       unlink($stfile) if -f $stfile;
+
+       $finished_cb->();
+    };
+}
+test_locked_state(\&Amanda::MainLoop::quit);
+Amanda::MainLoop::run();