2 # Copyright (c) 2008-2012 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 package Installcheck::Run;
25 Installcheck::Run - utilities to set up and run amanda dumps and restores
29 use Installcheck::Run;
31 my $testconf = Installcheck::Run::setup();
32 # make any modifications you'd like to the configuration
35 ok(Installcheck::Run::run('amdump', 'TESTCONF'), "amdump completes successfully");
37 # It's generally polite to clean up your mess, although the test
38 # framework will clean up if your tests crash
39 Installcheck::Run::cleanup();
42 skip "Expect.pm not installed", 7
43 unless $Installcheck::Run::have_expect;
45 my $exp = Installcheck::Run::run_expect('amflush', 'TESTCONF');
52 High-level tests generally depend on a full-scale run of Amanda --
53 a fairly messy project. This module simplifies that process by
54 abstracting away the mess. It takes care of:
58 =item Setting up a holding disk;
60 =item Setting up several vtapes; and
62 =item Setting up a DLE pointing to a reasonably-sized subdirectory of the build directory.
66 Most of this magic is in C<setup()>, which returns a configuration object from
67 C<Installcheck::Config>, allowing the test to modify that configuration before
68 writing it out. The hostname for the DLE is "localhost", and the disk name is
69 available in C<$Installcheck::Run::diskname>. This DLE has a subdirectory
70 C<dir> which can be used as a secondary, smaller DLE if needed.
72 This module also provides a convenient Perlish interface for running Amanda
73 commands: C<run($app, $args, ...)>. This function runs $app (from $sbindir if
74 $app is not an absolute path), and returns true if the application exited with
75 a status of zero. The stdout and stderr of the application are left in
76 C<$Installcheck::Run::stdout> and C<stderr>, respectively.
78 To check that a run is successful, and return its stdout (chomped), use
79 C<run_get($app, $args, ...)>. This function returns C<''> if the application
80 returns a nonzero exit status. Since many Amanda applications send normal
81 output to stderr, use C<run_get_err($app, $args, ...)> to check that a run is
82 successful and return its stderr. Similarly, C<run_err> checks that a run
83 returns a nonzero exit status, and then returns its stderr, chomped. If you
84 need both, use a bare C<run> and then check C<$stderr> and C<$stdout> as needed.
86 C<run> and friends can be used whether or not this module's C<setup>
89 Finally, C<cleanup()> cleans up from a run, deleting all backed-up
90 data, holding disks, and configuration. It's just good-neighborly
91 to call this before your test script exits.
95 This module sets up a configuration with three 30M vtapes, replete with
96 the proper vtape directories. These are controlled by C<chg-disk>.
97 The tapes are not labeled, and C<autolabel> is not set by
98 default, although C<labelstr> is set to C<TESTCONF[0-9][0-9]>.
100 The vtapes are created in <$Installcheck::Run::taperoot>, a subdirectory of
101 C<$Installcheck::TMP> for ease of later deletion. The subdirectory for each
102 slot is available from C<vtape_dir($slot)>, while the parent directory is
103 available from C<vtape_dir()>. C<load_vtape($slot)> will "load" the indicated
104 slot just like chg-disk would, and return the resulting path.
108 The holding disk is C<$Installcheck::Run::holdingdir>. It is a 15M holding disk,
109 with a chunksize of 1M (to help exercise the chunker).
113 The disklist is empty by default. Use something like the following
116 $testconf->add_dle("localhost $diskname installcheck-test");
118 The C<installcheck-test> dumptype specifies
123 but of course, it can be modified by the test module.
125 =head2 INTERACTIVE APPLICATIONS
127 This package provides a rudimentary wrapper around C<Expect.pm>, which is not
128 typically included in a perl installation. Consult C<$have_expect> to see if
129 this module is installed, and skip any Expect-based tests if it is not.
131 Otherwise, C<run_expect> takes arguments just like C<run>, but returns an Expect
132 object which you can use as you would like.
136 If your test runs 'amdump', a nonzero exit status may not be very helpful. The
137 function C<amdump_diag> will attempt to figure out what went wrong and display
138 useful information for the user via diag(). If it is given an argument, then
139 it will C<BAIL_OUT> with that message, causing L<Test::Harness> to stop running
140 tests. Otherwise, it will simply die(), which will only terminate this
141 particular test script.
146 use Installcheck::Config;
150 use Cwd qw(abs_path getcwd);
152 use POSIX qw( WIFEXITED );
154 use Amanda::Config qw( :init );
155 use Amanda::Util qw(slurp);
160 @EXPORT_OK = qw(setup
161 run run_get run_get_err run_err
163 $diskname $taperoot $holdingdir
164 $stdout $stderr $exit_code
166 amdump_diag run_expect );
167 @EXPORT = qw(exp_continue exp_continue_timeout);
180 *exp_continue = *ignore;
181 *exp_continue_timeout = *ignore;
187 # common paths (note that Installcheck::Dumpcache assumes these do not change)
188 our $diskname = "$Installcheck::TMP/backmeup";
189 our $taperoot = "$Installcheck::TMP/vtapes";
190 our $holdingdir ="$Installcheck::TMP/holding";
193 my $new_vtapes = shift;
195 my $testconf = Installcheck::Config->new();
197 $nb_slot = 3 if !defined $nb_slot;
198 (-d $diskname) or setup_backmeup();
200 setup_new_vtapes($testconf, $nb_slot);
202 setup_vtapes($testconf, $nb_slot);
204 setup_holding($testconf, 25);
205 setup_disklist($testconf);
210 # create the 'backmeup' data
212 my $dir_structure = {
213 '1megabyte' => 1024*1024,
228 mkpath($diskname) or die("Could not create $name");
230 # pick a file for 'random' data -- /dev/urandom or, failing that,
231 # Amanda's ChangeLog.
232 my $randomfile = "/dev/urandom";
233 if (!-r $randomfile) {
234 $randomfile = "../ChangeLog";
239 my ($parent, $contents) = @_;
240 while (my ($name, $val) = each(%$contents)) {
241 my $name = "$parent/$name";
242 if (ref($val) eq 'HASH') {
243 mkpath($name) or die("Could not create $name");
244 $create->($name, $val);
246 my $bytes_needed = $val+0;
247 open(my $wfd, ">", $name) or die("Could not open $name: $!");
249 # read bytes from a source file as a source of "random" data..
250 while ($bytes_needed) {
252 if (!defined($rfd)) {
253 open($rfd, "<", "$randomfile") or die("Could not open $randomfile");
255 my $to_read = $bytes_needed>10240? 10240:$bytes_needed;
256 my $bytes_read = sysread($rfd, $buf, $to_read);
258 if ($bytes_read < $to_read) {
263 $bytes_needed -= $bytes_read;
269 $create->($diskname, $dir_structure);
273 my ($testconf, $ntapes) = @_;
278 # make each of the tape directories
279 for (my $i = 1; $i < $ntapes+1; $i++) {
280 my $tapepath = "$taperoot/slot$i";
286 # set up the appropriate configuration
287 $testconf->add_param("tapedev", "\"file:$taperoot\"");
288 $testconf->add_param("tpchanger", "\"chg-disk\"");
289 $testconf->add_param("changerfile", "\"$CONFIG_DIR/TESTCONF/ignored-filename\"");
290 $testconf->add_param("labelstr", "\"TESTCONF[0-9][0-9]\"");
291 $testconf->add_param("tapecycle", "$ntapes");
293 # this overwrites the existing TEST-TAPE tapetype
294 $testconf->add_tapetype('TEST-TAPE', [
295 'length' => '30 mbytes',
296 'filemark' => '4 kbytes',
300 sub setup_new_vtapes {
301 my ($testconf, $ntapes) = @_;
306 # make each of the tape directories
307 for (my $i = 1; $i < $ntapes+1; $i++) {
308 my $tapepath = "$taperoot/slot$i";
314 # set up the appropriate configuration
315 $testconf->add_param("tpchanger", "\"chg-disk:$taperoot\"");
316 $testconf->add_param("labelstr", "\"TESTCONF[0-9][0-9]\"");
317 $testconf->add_param("tapecycle", "$ntapes");
319 # this overwrites the existing TEST-TAPE tapetype
320 $testconf->add_tapetype('TEST-TAPE', [
321 'length' => '30 mbytes',
322 'filemark' => '4 kbytes',
327 my ($testconf, $mbytes) = @_;
329 if (-d $holdingdir) {
334 $testconf->add_holdingdisk("hd1", [
335 'directory' => "\"$holdingdir\"",
336 'use' => "$mbytes mbytes",
337 'chunksize' => "1 mbyte",
344 $testconf->add_dumptype("installcheck-test", [
346 'compress' => 'none',
347 'program' => '"GNUTAR"',
353 if (defined($slot)) {
354 return "$taperoot/slot$slot";
363 # make the data/ symlink from our taperoot
364 unlink("$taperoot/data");
365 symlink(vtape_dir($slot), "$taperoot/data")
366 or die("Could not create 'data' symlink: $!");
374 my $errtempfile = "$Installcheck::TMP/stderr$$.out";
376 # use a temporary file for error output -- this eliminates synchronization
377 # problems between reading stderr and stdout
378 local (*INFH, *OUTFH, *ERRFH);
379 open(ERRFH, ">", $errtempfile);
381 $app = "$sbindir/$app" unless ($app =~ qr{/});
382 my $pid = IPC::Open3::open3("INFH", "OUTFH", ">&ERRFH",
385 # immediately close the child's stdin
388 # read from stdout until it's closed
389 $stdout = do { local $/; <OUTFH> };
392 # and wait for the kid to die
393 waitpid $pid, 0 or croak("Error waiting for child process to die: $@");
397 # fetch stderr from the temporary file
398 $stderr = slurp($errtempfile);
399 unlink($errtempfile);
401 # and return true if the exit status was zero
402 $exit_code = $status >> 8;
403 return WIFEXITED($status) && $exit_code == 0;
409 # prefer to put stderr in the output
411 $detail .= "\nstderr is:\n$stderr";
413 if ($stdout and length($stdout) < 1024) {
414 $detail .= "\nstdout is:\n$stdout";
417 Test::More::diag("run unexpectedly failed; no output to compare$detail");
428 my $detail = "\nstderr is:\n$stderr";
429 Test::More::diag("run unexpectedly failed; no output to compare$detail");
440 Test::More::diag("run unexpectedly succeeded; no output to compare");
450 Installcheck::Config::cleanup();
455 if (-d $holdingdir) {
464 die "Expect.pm not found" unless $have_expect;
466 $app = "$sbindir/$app" unless ($app =~ qr{^/});
467 my $exp = Expect->new("$app", @args);
475 # try running amreport
476 my $report = "failure-report.txt";
478 my @logfiles = <$CONFIG_DIR/TESTCONF/log/log.*>;
480 run('amreport', 'TESTCONF', '-f', $report, '-l', $logfiles[$#logfiles]);
482 open(my $fh, "<", $report) or return;
483 for my $line (<$fh>) {
484 Test::More::diag($line);
491 # maybe there was a config error
492 config_init($CONFIG_INIT_EXPLICIT_NAME, "TESTCONF");
493 my ($cfgerr_level, @cfgerr_errors) = config_errors();
494 if ($cfgerr_level >= $CFGERR_WARNINGS) {
495 foreach (@cfgerr_errors) {
496 Test::More::diag($_);
502 Test::More::diag("no amreport available, and no config errors");
506 Test::More::BAIL_OUT($msg);
508 die("amdump failed; cannot continue");