117cf01a48691a5541303fd740a137dcbbfe4b96
[debian/amanda] / installcheck / Installcheck / Run.pm
1 # vim:ft=perl
2 # Copyright (c) 2005-2008 Zmanda Inc.  All Rights Reserved.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
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 Mathlida Ave, Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 package Installcheck::Run;
21
22 =head1 NAME
23
24 Installcheck::Run - utilities to set up and run amanda dumps and restores
25
26 =head1 SYNOPSIS
27
28   use Installcheck::Run;
29
30   my $testconf = Installcheck::Run::setup();
31   # make any modifications you'd like to the configuration
32   $testconf->write();
33
34   ok(Installcheck::Run::run('amdump', 'TESTCONF'), "amdump completes successfully");
35
36   # It's generally polite to clean up your mess, although the test
37   # framework will clean up if your tests crash
38   Installcheck::Run::cleanup();
39
40   SKIP: {
41     skip "Expect.pm not installed", 7
42         unless $Installcheck::Run::have_expect;
43
44     my $exp = Installcheck::Run::run_expect('amflush', 'TESTCONF');
45     $exp->expect(..);
46     # ..
47   }
48
49 =head1 USAGE
50
51 High-level tests generally depend on a full-scale run of Amanda --
52 a fairly messy project.  This module simplifies that process by
53 abstracting away the mess.  It takes care of:
54
55 =over
56 =item Setting up a holding disk;
57 =item Setting up several vtapes; and
58 =item Setting up a DLE pointing to a reasonably-sized subdirectory of the build directory.
59 =back
60
61 Most of this magic is in C<setup()>, which returns a configuration
62 object from C<Installcheck::Config>, allowing the test to
63 modify that configuration before writing it out.  The hostname
64 for the DLE is "localhost", and the disk name is available in
65 C<Installcheck::Run::diskname>.
66
67 This module also provides a convenient Perlish interface for running
68 Amanda commands: C<run($app, $args, ...)>.  This function uses the
69 appropriate path to get to $app, and returns true if the application
70 exited with a status of zero.  The stdout and stderr of the application
71 are left in C<Installcheck::Run::stdout> and C<stderr>, respectively.
72
73 To check that a run is successful, and return its stdout (chomped), use
74 C<run_get($app, $args, ...)>.  This function returns C<''> if the application
75 returns a nonzero exit status.  Similarly, C<run_err> checks that a run returns
76 a nonzero exit status, and then returns its stderr, chomped.  If you need a
77 different output file, use a bare C<run> followed by C<get_stderr> or
78 C<get_stdout> as needed.
79
80 C<run> and friends can be used whether or not this module's C<setup>
81 was invoked.
82
83 Finally, C<cleanup()> cleans up from a run, deleting all backed-up
84 data, holding disks, and configuration.  It's just good-neighborly
85 to call this before your test script exits.
86
87 =head2 VTAPES
88
89 This module sets up a configuration with three 10M vtapes, replete with
90 the proper vtape directories.  These are controlled by C<chg-disk>.
91 The tapes are not labeled, and C<label_new_tapes> is not set by
92 default, although C<labelstr> is set to C<TESTCONF[0-9][0-9]>.
93
94 The vtapes are created in a subdirectory of C<AMANDA_TMPDIR> for ease of later
95 deletion.  The subdirectory is available from C<vtape_dir($slot)>.
96 C<load_vtape($slot)> will "load" the indicated slot just like chg-disk would,
97 and return the resulting path.
98
99 =head2 HOLDING
100
101 The holding disk is also stored under C<AMANDA_TMPDIR>.  It is a 15M
102 holding disk, with a chunksize of 1M (to help exercise the chunker).
103
104 =head2 DISKLIST
105
106 The disklist is empty by default.  Use something like the following
107 to add an entry:
108
109   $testconf->add_dle("localhost $diskname installcheck-test");
110
111 The C<installcheck-test> dumptype specifies
112   auth "local"
113   compress none
114   program "GNUTAR"
115
116 but of course, it can be modified by the test module.
117
118 =head2 INTERACTIVE APPLICATIONS
119
120 This package provides a rudimentary wrapper around C<Expect.pm>, which is not
121 typically included in a perl installation.  Consult C<$have_expect> to see if
122 this module is installed, and skip any Expect-based tests if it is not.
123
124 Otherwise, C<run_expect> takes arguments just like C<run>, but returns an Expect
125 object which you can use as you would like.
126
127 =head2 DIAGNOSTICS
128
129 If your test runs 'amdump', a nonzero exit status may not be very helpful.  The
130 function C<amdump_diag> will attempt to figure out what went wrong and display
131 useful information for the user via diag().  If it is given an argument, then
132 it will C<BAIL_OUT> with that message, causing L<Test::Harness> to stop running
133 tests.  Otherwise, it will simply die(), which will only terminate this
134 particular test script.
135
136 =cut
137
138 use Installcheck::Config;
139 use Amanda::Paths;
140 use File::Path;
141 use IPC::Open3;
142 use Cwd qw(abs_path getcwd);
143 use Carp;
144 use Test::More;
145 use Amanda::Config qw( :init );
146
147 require Exporter;
148
149 @ISA = qw(Exporter);
150 @EXPORT_OK = qw(setup 
151     run run_get run_err
152     cleanup 
153     $diskname $stdout $stderr
154     amdump_diag);
155 @EXPORT = qw(exp_continue exp_continue_timeout);
156
157 # global variables
158 our $stdout = '';
159 our $stderr = '';
160
161 our $have_expect;
162
163 BEGIN {
164     eval "use Expect;";
165     if ($@) {
166         $have_expect = 0;
167         sub ignore() { };
168         *exp_continue = *ignore;
169         *exp_continue_timeout = *ignore;
170     } else {
171         $have_expect = 1;
172     }
173 };
174
175 # diskname is device-src, which, when full of object files, is about 4M in
176 # my environment.  Consider creating a directory full of a configurable amount
177 # of junk and pointing to that, to eliminate a potential point of variation in
178 # tests.
179 our $diskname = abs_path(getcwd() . "/../device-src");
180
181 # common paths
182 my $taperoot = "$AMANDA_TMPDIR/installcheck-vtapes";
183 my $holdingdir ="$AMANDA_TMPDIR/installcheck-holding";
184
185 sub setup {
186     my $testconf = Installcheck::Config->new();
187
188     setup_vtapes($testconf, 3);
189     setup_holding($testconf, 25);
190     setup_disklist($testconf);
191
192     return $testconf;
193 }
194
195 sub setup_vtapes {
196     my ($testconf, $ntapes) = @_;
197     if (-d $taperoot) {
198         rmtree($taperoot);
199     }
200
201     # make each of the tape directories
202     for (my $i = 1; $i < $ntapes+1; $i++) {
203         my $tapepath = "$taperoot/slot$i";
204         mkpath("$tapepath");
205     }
206
207     load_vtape(1);
208
209     # set up the appropriate configuration
210     $testconf->add_param("tapedev", "\"file:$taperoot\"");
211     $testconf->add_param("tpchanger", "\"chg-disk\"");
212     $testconf->add_param("changerfile", "\"$CONFIG_DIR/TESTCONF/ignored-filename\"");
213     $testconf->add_param("labelstr", "\"TESTCONF[0-9][0-9]\"");
214
215     # this overwrites the existing TEST-TAPE tapetype
216     $testconf->add_tapetype('TEST-TAPE', [
217         'length' => '20 mbytes',
218         'filemark' => '4 kbytes',
219     ]);
220 }
221
222 sub setup_holding {
223     my ($testconf, $mbytes) = @_;
224
225     if (-d $holdingdir) {
226         rmtree($holdingdir);
227     }
228     mkpath($holdingdir);
229
230     $testconf->add_holdingdisk("hd1", [
231         'directory' => "\"$holdingdir\"",
232         'use' => "$mbytes mbytes",
233         'chunksize' => "1 mbyte",
234     ]);
235 }
236
237 sub setup_disklist {
238     my ($testconf) = @_;
239     
240     $testconf->add_dumptype("installcheck-test", [
241         'auth' => '"local"',
242         'compress' => 'none',
243         'program' => '"GNUTAR"',
244     ]);
245 }
246
247 sub vtape_dir {
248     my ($slot) = @_;
249     my $tapepath = "$taperoot/slot$slot";
250 }
251
252 sub load_vtape {
253     my ($slot) = @_;
254
255     # make the data/ symlink from our taperoot
256     unlink("$taperoot/data");
257     symlink(vtape_dir($slot), "$taperoot/data")
258         or die("Could not create 'data' symlink: $!");
259
260     return $taperoot;
261 }
262
263 sub run {
264     my $app = shift;
265     my @args = @_;
266     my $errtempfile = "$AMANDA_TMPDIR/stderr$$.out";
267
268     # use a temporary file for error output -- this eliminates synchronization
269     # problems between reading stderr and stdout
270     local (*INFH, *OUTFH, *ERRFH);
271     open(ERRFH, ">", $errtempfile);
272
273     my $pid = IPC::Open3::open3("INFH", "OUTFH", ">&ERRFH",
274         "$sbindir/$app", @args);
275     
276     # immediately close the child's stdin
277     close(INFH);
278
279     # read from stdout until it's closed
280     $stdout = do { local $/; <OUTFH> };
281     close(OUTFH);
282
283     # and wait for the kid to die
284     waitpid $pid, 0 or croak("Error waiting for child process to die: $@");
285     my $status = $?;
286     close(ERRFH);
287
288     # fetch stderr from the temporary file
289     open(ERRFH, "<", "$errtempfile") or croak("Could not open '$errtempfile'");
290     $stderr = do { local $/; <ERRFH> };
291     close(ERRFH);
292     unlink($errtempfile);
293
294     # and return true if the exit status was zero
295     return ($status >> 8) == 0;
296 }
297
298 sub run_get {
299     if (!run @_) {
300         Test::More::diag("run unexpectedly failed; no output to compare");
301         return '';
302     }
303
304     chomp $stdout;
305     return $stdout;
306 }
307
308 sub run_err {
309     if (run @_) {
310         Test::More::diag("run unexpectedly succeeded; no output to compare");
311         return '';
312     }
313
314     chomp $stderr;
315     return $stderr;
316 }
317
318 sub get_stdout {
319     chomp $stdout;
320     return $stdout;
321 }
322
323 sub get_stderr {
324     chomp $stderr;
325     return $stderr;
326 }
327
328 sub cleanup {
329     if (-d $taperoot) {
330         rmtree($taperoot);
331     }
332     if (-d $holdingdir) {
333         rmtree($holdingdir);
334     }
335 }
336
337 sub run_expect {
338     my $app = shift;
339     my @args = @_;
340
341     die "Expect.pm not found" unless $have_expect;
342
343     my $exp = Expect->new("$sbindir/$app", @args);
344
345     return $exp;
346 }
347
348 sub amdump_diag {
349     my ($msg) = @_;
350
351     # try running amreport
352     my $report = "failure-report.txt";
353     unlink($report);
354     my @logfiles = <$CONFIG_DIR/TESTCONF/log/log.*>;
355     if (@logfiles > 0) {
356         run('amreport', 'TESTCONF', '-f', $report, '-l', $logfiles[$#logfiles]);
357         if (-f $report) {
358             open(my $fh, "<", $report) or return;
359             for my $line (<$fh>) {
360                 Test::More::diag($line);
361             }
362             unlink($report);
363             goto bail;
364         }
365     }
366
367     # maybe there was a config error
368     config_init($CONFIG_INIT_EXPLICIT_NAME, "TESTCONF");
369     my ($cfgerr_level, @cfgerr_errors) = config_errors();
370     if ($cfgerr_level >= $CFGERR_WARNINGS) {
371         foreach (@cfgerr_errors) {
372             Test::More::diag($_);
373         }
374         goto bail;
375     }
376
377     # huh.
378     Test::More::diag("no amreport available, and no config errors");
379
380 bail:
381     if ($msg) {
382         Test::More::BAIL_OUT($msg);
383     } else {
384         die("amdump failed; cannot continue");
385     }
386 }
387
388 1;