1 # Copyright (c) 2009, 2010 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19 use Test::More tests => 201;
22 use POSIX qw( WIFEXITED );
26 use lib "@amperldir@";
29 use Amanda::Constants;
30 use Amanda::Util qw( slurp );
32 ## this is an unusual installcheck, because it does not test anything about
33 ## Amanda itself. However, it validates the accuracy of our understanding of
34 ## GNU Tar's behavior, as recorded at
35 ## http://wiki.zmanda.com/index.php/GNU_Tar_Include_and_Exclude_Behavior
37 my $gnutar = $Amanda::Constants::GNUTAR;
38 $gnutar = $ENV{'GNUTAR'} if exists $ENV{'GNUTAR'};
42 my @filenames = (qw{A*A AxA B?B BxB C[C CC D]D E\E F'F G"G}, 'H H');
44 my $tarfile = "$Installcheck::TMP/gnutar-tests.tar";
45 my $datadir = "$Installcheck::TMP/gnutar-tests";
51 rmtree($datadir) if -e $datadir;
54 for my $fn (@filenames) {
55 open(my $fh, ">", "$datadir/$fn");
60 system($gnutar, "-C", $datadir, "-cf", $tarfile, @extra_args, '.');
61 die "could not run gnutar" unless $? == 0;
63 rmtree($datadir) if -e $datadir;
68 my ($v, $numeric_version);
70 my $verstring = `$gnutar --version`;
71 die "could not run gnutar" unless $? == 0;
72 ($v) = ($verstring =~ /tar \(GNU tar\) *([0-9.]+)/);
73 my ($maj, $min, $mic) = ($v =~ /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/);
76 $numeric_version += $maj * 10000 if $maj;
77 $numeric_version += $min * 100 if $min;
78 $numeric_version += $mic if $mic;
81 # see if the default for --wildcards during inclusion has been changed
82 my $wc_default_changed = 0;
84 my $help_output = `$gnutar --help`;
85 # redhatty patches helpfully change the help message
86 if ($help_output =~ /--wildcards\s*use wildcards \(default\)$/m) {
87 $wc_default_changed = 1;
91 my %version_classes = (
92 '<1.16' => $numeric_version < 11591,
93 '>=1.16' => $numeric_version >= 11591,
94 '>=1.16-no-wc' => $numeric_version >= 11591 && !$wc_default_changed, # normal
95 '>=1.16-wc' => $numeric_version >= 11591 && $wc_default_changed, # stupid distros screw things up!
96 '1.16..<1.25' => $numeric_version >= 11591 && $numeric_version < 12500,
98 '<1.23' => $numeric_version < 12300,
99 '>=1.23' => $numeric_version >= 12300,
101 '1.23' => ($numeric_version >= 12290 and $numeric_version <= 12300),
102 '!1.23' => ($numeric_version < 12290 || ($numeric_version > 12300 && $numeric_version < 12500)),
103 '>=1.25' => $numeric_version >= 12500.
106 # include and exclude all use the same set of patterns and filenames
124 './E\\\\E' => 'E\\E',
125 './F\\\'F' => 'F\'F',
130 my $named_expectations = [
139 # al be ga de ep ze et empty
140 [ 1, 1, 1, 1, 1, 1, 1, 1, ], # './A*A' => 'A*A',
141 [ 1, 1, 1, 1, 0, 1, 1, 0, ], # './A*A' => 'AxA',
142 [ 1, 1, 1, 1, 1, 1, 1, 1, ], # './B?B' => 'B?B',
143 [ 1, 1, 1, 1, 0, 1, 1, 0, ], # './B?B' => 'BxB',
144 [ 0, 0, 0, 0, 1, 1, 1, 1, ], # './C[C' => 'C[C',
145 [ 1, 1, 1, 1, 1, 1, 1, 1, ], # './D]D' => 'D]D',
146 [ 1, 0, 0, 1, 1, 0, 0, 1, ], # './E\\E' => 'E\\E',
147 [ 1, 1, 1, 1, 1, 1, 1, 1, ], # './F\'F' => 'F\'F',
148 [ 1, 1, 1, 1, 1, 1, 1, 1, ], # './G"G' => 'G"G',
149 [ 1, 1, 1, 1, 1, 1, 1, 1, ], # './H H' => 'H H',
150 [ 1, 1, 1, 0, 0, 1, 1, 0, ], # './A\\*A' => 'A*A',
151 [ 0, 0, 0, 0, 0, 0, 0, 0, ], # './A\\*A' => 'AxA',
152 [ 0, 0, 1, 0, 0, 0, 1, 0, ], # './B\\?B' => 'B?B',
153 [ 0, 0, 0, 0, 0, 0, 0, 0, ], # './B\\?B' => 'BxB',
154 [ 1, 1, 1, 0, 0, 1, 1, 0, ], # './C\\[C' => 'C[C',
155 [ 0, 1, 1, 0, 0, 1, 1, 0, ], # './D\\]D' => 'D]D',
156 [ 1, 0, 1, 0, 1, 0, 1, 0, ], # './E\\\\E' => 'E\\E',
157 [ 0, 1, 1, 0, 0, 1, 1, 0, ], # './F\\\'F' => 'F\'F',
158 [ 0, 1, 1, 0, 0, 1, 1, 0, ], # './G\\"G' => 'G"G',
159 [ 0, 1, 1, 0, 0, 1, 1, 0, ], # './H\\ H' => 'H H',
162 sub get_expectation {
164 my @names = @{$named_expectations->[0]};
166 # get the index for that greek letter
169 if ($names[$_] eq $name) {
175 # then assemble the result
177 my @exps = @$named_expectations;
186 sub get_matching_type {
187 my ($expectations) = @_;
189 # find the type for the first matching version
190 for (keys %$expectations) {
191 if ($version_classes{$_}) {
192 return $expectations->{$_};
198 sub get_version_index {
199 my @versions = @{$_[0]};
202 for (0 .. $#versions) {
203 if ($version_classes{$versions[$_]}) {
212 my ($stderr, $stdout, $exit_code);
215 my @args = @{ $params{'args'} };
217 my $errtempfile = "$Installcheck::TMP/stderr$$.out";
219 # use a temporary file for error output -- this eliminates synchronization
220 # problems between reading stderr and stdout
221 local (*INFH, *OUTFH, *ERRFH);
222 open(ERRFH, ">", $errtempfile);
225 if ($params{'env'}) {
226 my %env = %{$params{'env'}};
232 my $pid = IPC::Open3::open3("INFH", "OUTFH", ">&ERRFH", $gnutar, @args);
233 my $cmdline = "$gnutar " . join(' ', @args);
235 # immediately close the child's stdin
238 # read from stdout until it's closed
239 $stdout = do { local $/; <OUTFH> };
242 # and wait for the kid to die
243 waitpid $pid, 0 or croak("Error waiting for gnutar die: $@");
247 # fetch stderr from the temporary file
248 $stderr = slurp($errtempfile);
249 unlink($errtempfile);
251 # get the exit status
252 $exit_code = WIFEXITED($status)? ($status >> 8) : 0xffff;
254 if ($exit_code != 0) {
261 ## inclusion tests (using -x and filenames on the command line)
263 sub test_gnutar_inclusion {
266 my $matching_type = get_matching_type($params{'expectations'});
268 # skip these tests if there's no matching version
269 if (!defined $matching_type) {
271 my $msg = (join " ", @{$params{'extra_args'}}) .
272 " not supported in version $v";
273 my $count = @$patterns / 2;
280 my @patterns = @$patterns;
281 my @expectations = get_expectation($matching_type);
283 my $pat = shift @patterns;
284 my $file = shift @patterns;
285 my $exp = shift @expectations;
288 $eargs = ', ' . join(' ', @{$params{'extra_args'}}) if @{$params{'extra_args'}};
289 my $match = $exp? "matches" : "does not match";
290 my $msg = "inclusion$eargs, pattern $pat $match file $file";
292 rmtree($datadir) if -e $datadir;
295 my $ok = run_gnutar(args => [ '-C', $datadir, '-x', '-f', $tarfile, @{$params{'extra_args'}}, $pat ]);
296 $ok = 0 unless -f "$datadir/$file";
299 diag(" unexpected success with version $v");
300 } elsif (!$ok and $exp) {
302 diag(" unexpected failure with version $v:\n$stderr");
307 rmtree($datadir) if -e $datadir;
310 # We'll trust that the following logic is implemented correctly in GNU Tar
311 # --no-wildcards is the default (same as no args) (but not everywhere!!)
312 # --unquote is the default (same as no args) (this seems true universally)
314 test_gnutar_inclusion(
318 '>=1.16-no-wc' => 'epsilon',
319 '>=1.16-wc' => 'beta', # acts like --wildcards
323 test_gnutar_inclusion(
324 extra_args => [ '--no-wildcards' ],
327 '>=1.16' => 'epsilon',
331 test_gnutar_inclusion(
332 extra_args => [ '--no-unquote' ],
335 '>=1.16-no-wc' => 'empty',
336 '>=1.16-wc' => 'gamma', # acts like --wildcards --no-unquote
340 test_gnutar_inclusion(
341 extra_args => [ '--no-wildcards', '--no-unquote' ],
348 test_gnutar_inclusion(
349 extra_args => [ '--wildcards' ],
352 '1.16..<1.25' => 'beta',
357 test_gnutar_inclusion(
358 extra_args => [ '--wildcards', '--no-unquote' ],
361 '1.16..<1.25' => 'gamma',
366 ## exclusion tests (using -t and filenames on the command line)
368 sub test_gnutar_exclusion {
371 my $matching_type = get_matching_type($params{'expectations'});
373 # skip these tests if there's no matching version
374 if (!defined $matching_type) {
376 my $msg = (join " ", @{$params{'extra_args'}}) .
377 " not supported in version $v";
378 my $count = @$patterns; # two elements per test, but we run each one twice
385 my @patterns = @$patterns;
386 my @expectations = get_expectation($matching_type);
388 my $pat = shift @patterns;
389 my $file = shift @patterns;
390 my $exp = shift @expectations;
393 $eargs = ', ' . join(' ', @{$params{'extra_args'}}) if @{$params{'extra_args'}};
394 my $match = $exp? "matches" : "does not match";
395 my $msg = "exclusion$eargs, extract, pattern $pat $match $file";
397 rmtree($datadir) if -e $datadir;
400 my $ok = run_gnutar(args => [ '-C', $datadir, '-x', '-f', $tarfile, @{$params{'extra_args'}}, "--exclude=$pat" ]);
402 # fail if the excluded file was extracted anyway..
404 my $excluded_ok = ! -f "$datadir/$file";
405 if ($excluded_ok and !$exp) {
407 diag(" exclusion unexpectedly worked with version $v");
408 } elsif (!$excluded_ok and $exp) {
410 diag(" exclusion unexpectedly failed with version $v");
416 diag(" unexpected error exit with version $v:\n$stderr");
420 # test again, but this time during a 'c'reate operation
421 @patterns = @$patterns;
422 @expectations = get_expectation($matching_type);
424 my $pat = shift @patterns;
425 my $file = shift @patterns;
426 my $exp = shift @expectations;
429 $eargs = ', ' . join(' ', @{$params{'extra_args'}}) if @{$params{'extra_args'}};
430 my $match = $exp? "matches" : "does not match";
431 my $msg = "exclusion$eargs, create, pattern $pat $match $file";
433 # this time around, we create the tarball with the exclude, then extract the whole
434 # thing. We extract rather than using 't' because 't' has a funny habit of backslashing
435 # its output that we don't want to deal with here.
436 make_tarfile(@{$params{'extra_args'}}, "--exclude=$pat");
438 rmtree($datadir) if -e $datadir;
440 my $ok = run_gnutar(args => [ '-C', $datadir, '-x', '-f', $tarfile]);
442 # fail if the excluded file was extracted anyway..
444 my $excluded_ok = ! -f "$datadir/$file";
445 if ($excluded_ok and !$exp) {
447 diag(" exclusion unexpectedly worked with version $v");
448 } elsif (!$excluded_ok and $exp) {
450 diag(" exclusion unexpectedly failed with version $v");
456 diag(" unexpected error exit with version $v:\n$stderr");
460 rmtree($datadir) if -e $datadir;
463 # We'll trust that the following logic is implemented correctly in GNU Tar
464 # --wildcards is the default (same as no args)
465 # --no-unquote / --unquote has no effect
468 test_gnutar_exclusion(
478 test_gnutar_exclusion(
479 extra_args => [ '--no-wildcards' ],
487 sub test_gnutar_toc {
490 my $vi = get_version_index($params{'versions'});
492 my @patterns = @{ $params{'patterns'} };
496 my $file = shift @patterns;
497 my $exp = shift @patterns;
500 push @filenames, $file;
501 push @expectations, $exp;
505 $eargs = ', ' . join(' ', @{$params{'extra_args'}}) if @{$params{'extra_args'}};
506 my $msg = "list$eargs, with lots of funny characters";
508 # make a tarfile containing the filenames, then run -t over it
509 rmtree($datadir) if -e $datadir;
512 for my $fn (@filenames) {
513 open(my $fh, ">", "$datadir/$fn")
514 or die("opening $datadir/$fn: $!");
519 system($gnutar, "-C", $datadir, "-cf", $tarfile, '.');
520 die "could not run gnutar" unless $? == 0;
522 rmtree($datadir) if -e $datadir;
524 if ($params{'env'}) {
525 %env = %{$params{'env'}};
527 my $ok = run_gnutar(args => [ '-t', '-f', $tarfile, @{$params{'extra_args'}}],
531 diag("gnutar exited with nonzero status for version $v");
534 my @toc_members = sort split(/\n/, $stdout);
535 shift @toc_members; # strip off './'
536 is_deeply([ @toc_members ], [ @expectations ], $msg);
539 # there are no extra_args that seem to affect this behavior
542 env => { LC_CTYPE => 'C' }, # avoid any funniness with ctypes
545 "A\007", [ './A\a' ],
546 "B\010", [ './B\b' ],
547 "C\011", [ './C\t' ],
548 "D\012", [ './D\n' ],
549 "E\013", [ './E\v' ],
550 "F\014", [ './F\f' ],
551 "G\015", [ './G\r' ],
552 "H\\", [ './H\\\\' ], # H\ -> H\\
553 "I\177", [ './I\\177' ],
554 "J\317\264", [ './J\\317\\264' ], # use legitimate utf-8, for mac os fs
555 "K\\x", [ './K\\\\x' ],
556 "L\\\\", [ './L\\\\\\\\' ],