2 # This is a testing framework.
4 # Copyright (C) 1998, 2000-2002, 2004-2008 Free Software Foundation, Inc.
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 use vars qw($VERSION @ISA @EXPORT);
23 use File::Compare qw(compare);
26 ($VERSION = '$Revision: 1.5 $ ') =~ tr/[0-9].//cd;
27 @EXPORT = qw (run_tests triple_test);
29 my $debug = $ENV{DEBUG};
31 my @Types = qw (IN IN_PIPE OUT ERR AUX CMP EXIT PRE POST OUT_SUBST
32 ERR_SUBST ENV ENV_DEL);
33 my %Types = map {$_ => 1} @Types;
34 my %Zero_one_type = map {$_ => 1}
35 qw (OUT ERR EXIT PRE POST OUT_SUBST ERR_SUBST ENV);
36 my $srcdir = $ENV{srcdir};
39 # When running in a DJGPP environment, make $ENV{SHELL} point to bash.
40 # Otherwise, a bad shell might be used (e.g. command.com) and many
43 and $ENV{SHELL} = "$ENV{DJDIR}/bin/bash.exe";
45 # A file spec: a scalar or a reference to a single-keyed hash
47 # 'contents' contents only (file name is derived from test name)
48 # {filename => 'contents'} filename and contents
49 # {filename => undef} filename only -- $(srcdir)/filename must exist
51 # FIXME: If there is more than one input file, then you can't specify `REDIR'.
54 # I/O spec: a hash ref with the following properties
56 # - one key/value pair
57 # - the key must be one of these strings: IN, OUT, ERR, AUX, CMP, EXIT
58 # - the value must be a file spec
59 # {OUT => 'data'} put data in a temp file and compare it to stdout from cmd
60 # {OUT => {'filename'=>undef}} compare contents of existing filename to
62 # {OUT => {'filename'=>[$CTOR, $DTOR]}} $CTOR and $DTOR are references to
63 # functions, each which is passed the single argument `filename'.
64 # $CTOR must create `filename'.
65 # DTOR may be omitted in which case `sub{unlink @_[0]}' is used.
66 # FIXME: implement this
68 # Same as for OUT, but compare with stderr, not stdout.
69 # {OUT_SUBST => 's/variable_output/expected_output/'}
70 # Transform actual standard output before comparing it against expected output.
71 # This is useful e.g. for programs like du that produce output that
72 # varies a lot from system. E.g., an empty file may consume zero file
73 # blocks, or more, depending on the OS and on the file system type.
74 # {ERR_SUBST => 's/variable_output/expected_output/'}
75 # Transform actual stderr output before comparing it against expected.
76 # This is useful when verifying that we get a meaningful diagnostic.
77 # For example, in rm/fail-2eperm, we have to account for three different
78 # diagnostics: Operation not permitted, Not owner, and Permission denied.
79 # {EXIT => N} expect exit status of cmd to be N
80 # {ENV => 'VAR=val ...'}
81 # Prepend 'VAR=val ...' to the command that we execute via `system'.
83 # Remove VAR from the environment just before running the corresponding
84 # command, and restore any value just afterwards.
86 # There may be many input file specs. File names from the input specs
87 # are concatenated in order on the command line.
88 # There may be at most one of the OUT-, ERR-, and EXIT-keyed specs.
89 # If the OUT-(or ERR)-keyed hash ref is omitted, then expect no output
90 # on stdout (or stderr).
91 # If the EXIT-keyed one is omitted, then expect the exit status to be zero.
93 # FIXME: Make sure that no junkfile is also listed as a
94 # non-junkfile (i.e. with undef for contents)
99 $string =~ s/\'/\'\\\'\'/g;
103 sub _create_file ($$$$)
105 my ($program_name, $test_name, $file_name, $data) = @_;
107 if (defined $file_name)
113 $file = "$test_name.$Global_count";
117 warn "creating file `$file' with contents `$data'\n" if $debug;
119 # The test spec gave a string.
120 # Write it to a temp file and return tempfile name.
121 my $fh = new FileHandle "> $file";
122 die "$program_name: $file: $!\n" if ! $fh;
124 $fh->close || die "$program_name: $file: $!\n";
129 sub _compare_files ($$$$$)
131 my ($program_name, $test_name, $in_or_out, $actual, $expected) = @_;
133 my $differ = compare ($expected, $actual);
136 my $info = (defined $in_or_out ? "std$in_or_out " : '');
137 warn "$program_name: test $test_name: ${info}mismatch, comparing "
138 . "$actual (actual) and $expected (expected)\n";
139 # Ignore any failure, discard stderr.
140 system "diff -c $actual $expected 2>/dev/null";
146 sub _process_file_spec ($$$$$)
148 my ($program_name, $test_name, $file_spec, $type, $junk_files) = @_;
150 my ($file_name, $contents);
153 ($file_name, $contents) = (undef, $file_spec);
155 elsif (ref $file_spec eq 'HASH')
157 my $n = keys %$file_spec;
158 die "$program_name: $test_name: $type spec has $n elements --"
161 ($file_name, $contents) = each %$file_spec;
163 # This happens for the AUX hash in an io_spec like this:
164 # {CMP=> ['zy123utsrqponmlkji', {'@AUX@'=> undef}]},
166 or return $file_name;
170 die "$program_name: $test_name: invalid RHS in $type-spec\n"
173 my $is_junk_file = (! defined $file_name
174 || (($type eq 'IN' || $type eq 'AUX' || $type eq 'CMP')
175 && defined $contents));
176 my $file = _create_file ($program_name, $test_name,
177 $file_name, $contents);
181 push @$junk_files, $file
185 # FIXME: put $srcdir in here somewhere
186 warn "$program_name: $test_name: specified file `$file' does"
188 if ! -f "$srcdir/$file";
197 foreach my $eo (qw (AUX OUT ERR))
202 and $s =~ s/\@$eo\@/$f/g;
207 # FIXME: cleanup on interrupt
208 # FIXME: extract `do_1_test' function
210 # FIXME: having to include $program_name here is an expedient kludge.
211 # Library code doesn't `die'.
212 sub run_tests ($$$$$)
214 my ($program_name, $prog, $t_spec, $save_temps, $verbose) = @_;
216 # To indicate that $prog is a shell built-in, you'd make it a string 'ref'.
217 # E.g., call run_tests ($prog, \$prog, \@Tests, $save_temps, $verbose);
218 # If it's a ref, invoke it via "env":
219 my @prog = ref $prog ? (qw(env --), $$prog) : $prog;
221 # Warn about empty t_spec.
224 # Remove all temp files upon interrupt.
227 # Verify that test names are distinct.
228 my $bad_test_name = 0;
232 foreach $t (@$t_spec)
234 my $test_name = $t->[0];
235 if ($seen{$test_name})
237 warn "$program_name: $test_name: duplicate test name\n";
240 $seen{$test_name} = 1;
244 my $t8 = lc substr $test_name, 0, 8;
245 if ($seen_8dot3{$t8})
247 warn "$program_name: 8.3 test name conflict: "
248 . "$test_name, $seen_8dot3{$t8}\n";
251 $seen_8dot3{$t8} = $test_name;
254 # The test name may be no longer than 30 bytes.
255 # Yes, this is an arbitrary limit. If it causes trouble,
256 # consider removing it.
258 if ($max < length $test_name)
260 warn "$program_name: $test_name: test name is too long (> $max)\n";
264 return 1 if $bad_test_name;
266 # FIXME check exit status
267 system (@prog, '--version') if $verbose;
271 foreach my $tt (@$t_spec)
276 my $test_name = shift @$t;
280 # FIXME: maybe don't reset this.
288 foreach $io_spec (@$t)
292 push @args, $io_spec;
296 if (ref $io_spec ne 'HASH')
298 eval 'use Data::Dumper';
299 die "$program_name: $test_name: invalid entry in test spec; "
300 . "expected HASH-ref,\nbut got this:\n"
301 . Data::Dumper->Dump ([\$io_spec], ['$io_spec']) . "\n";
304 my $n = keys %$io_spec;
305 die "$program_name: $test_name: spec has $n elements --"
308 my ($type, $val) = each %$io_spec;
309 die "$program_name: $test_name: invalid key `$type' in test spec\n"
312 # Make sure there's no more than one of OUT, ERR, EXIT, etc.
313 die "$program_name: $test_name: more than one $type spec\n"
314 if $Zero_one_type{$type} and $seen_type{$type}++;
316 if ($type eq 'PRE' or $type eq 'POST')
318 $expect->{$type} = $val;
326 or die "$program_name: $test_name: invalid CMP spec\n";
328 or die "$program_name: $test_name: invalid CMP list; must have"
329 . " exactly 2 elements\n";
331 foreach my $e (@$val)
335 and die "$program_name: $test_name: invalid element ($r)"
336 . " in CMP list; only scalars and hash references "
338 if ($r && $r eq 'HASH')
342 or die "$program_name: $test_name: CMP spec has $n "
343 . "elements -- expected 1\n";
345 # Replace any `@AUX@' in the key of %$e.
346 my ($ff, $val) = each %$e;
347 my $new_ff = _at_replace $expect, $ff;
350 $e->{$new_ff} = $val;
354 my $cmp_file = _process_file_spec ($program_name, $test_name,
355 $e, $type, \@junk_files);
356 push @cmp_files, $cmp_file;
358 push @post_compare, [@cmp_files];
360 $expect->{$type} = $val;
366 die "$program_name: $test_name: invalid EXIT code\n"
368 # FIXME: make sure $data is numeric
369 $expect->{EXIT} = $val;
373 if ($type =~ /^(OUT|ERR)_SUBST$/)
375 $expect->{RESULT_SUBST} ||= {};
376 $expect->{RESULT_SUBST}->{$1} = $val;
382 $env_prefix = "$val ";
386 if ($type eq 'ENV_DEL')
388 push @env_delete, $val;
392 my $file = _process_file_spec ($program_name, $test_name, $val,
393 $type, \@junk_files);
395 if ($type eq 'IN' || $type eq 'IN_PIPE')
397 my $quoted_file = _shell_quote $file;
398 if ($type eq 'IN_PIPE')
400 defined $input_pipe_cmd
401 and die "$program_name: $test_name: only one input"
402 . " may be specified with IN_PIPE\n";
403 $input_pipe_cmd = "cat $quoted_file |";
407 push @args, $quoted_file;
410 elsif ($type eq 'AUX' || $type eq 'OUT' || $type eq 'ERR')
412 $expect->{$type} = $file;
416 die "$program_name: $test_name: invalid type: $type\n"
420 # Expect an exit status of zero if it's not specified.
421 $expect->{EXIT} ||= 0;
423 # Allow ERR to be omitted -- in that case, expect no error output.
424 foreach my $eo (qw (OUT ERR))
426 if (!exists $expect->{$eo})
428 $expect->{$eo} = _create_file ($program_name, $test_name,
430 push @junk_files, $expect->{$eo};
434 # FIXME: Does it ever make sense to specify a filename *and* contents
435 # in OUT or ERR spec?
437 # FIXME: this is really suboptimal...
439 foreach my $a (@args)
441 $a = _at_replace $expect, $a;
446 warn "$test_name...\n" if $verbose;
447 &{$expect->{PRE}} if $expect->{PRE};
449 $actual{OUT} = "$test_name.O";
450 $actual{ERR} = "$test_name.E";
451 push @junk_files, $actual{OUT}, $actual{ERR};
452 my @cmd = (@prog, @args, "> $actual{OUT}", "2> $actual{ERR}");
454 and unshift @cmd, $env_prefix;
455 defined $input_pipe_cmd
456 and unshift @cmd, $input_pipe_cmd;
457 my $cmd_str = join (' ', @cmd);
459 # Delete from the environment any symbols specified by syntax
460 # like this: {ENV_DEL => 'TZ'}.
462 foreach my $env_sym (@env_delete)
464 my $val = delete $ENV{$env_sym};
466 and $pushed_env{$env_sym} = $val;
469 warn "Running command: `$cmd_str'\n" if $debug;
470 my $rc = 0xffff & system $cmd_str;
472 # Restore any environment setting we changed via a deletion.
473 foreach my $env_sym (keys %pushed_env)
475 $ENV{$env_sym} = $pushed_env{$env_sym};
480 warn "$program_name: test $test_name failed: command failed:\n"
481 . " `$cmd_str': $!\n";
485 $rc >>= 8 if $rc > 0x80;
486 if ($expect->{EXIT} != $rc)
488 warn "$program_name: test $test_name failed: exit status mismatch:"
489 . " expected $expect->{EXIT}, got $rc\n";
495 # Record actual stdout and stderr contents, if POST may need them.
498 foreach my $eo (qw (OUT ERR))
500 my $out_file = $actual{$eo};
502 or (warn "$program_name: cannot open $out_file for reading: $!\n"),
504 $actual_data{$eo} = <IN>;
506 or (warn "$program_name: failed to read $out_file: $!\n"),
511 foreach my $eo (qw (OUT ERR))
513 my $subst_expr = $expect->{RESULT_SUBST}->{$eo};
514 if (defined $subst_expr)
516 my $out = $actual{$eo};
517 my $orig = "$out.orig";
519 # Move $out aside (to $orig), then recreate $out
520 # by transforming each line of $orig via $subst_expr.
522 or (warn "$program_name: cannot rename $out to $orig: $!\n"),
525 or (warn "$program_name: cannot open $orig for reading: $!\n"),
526 $fail = 1, (unlink $orig), next;
528 or (warn "$program_name: cannot unlink $orig: $!\n"),
531 or (warn "$program_name: cannot open $out for writing: $!\n"),
533 while (defined (my $line = <IN>))
535 eval "\$_ = \$line; $subst_expr; \$line = \$_";
540 or (warn "$program_name: failed to write $out: $!\n"),
544 my $eo_lower = lc $eo;
545 _compare_files ($program_name, $test_name, $eo_lower,
546 $actual{$eo}, $expect->{$eo})
550 foreach my $pair (@post_compare)
552 my ($expected, $actual) = @$pair;
553 _compare_files $program_name, $test_name, undef, $actual, $expected
559 and &{$expect->{POST}} ($actual_data{OUT}, $actual_data{ERR});
563 # FIXME: maybe unlink files inside the big foreach loop?
564 unlink @junk_files if ! $save_temps;
569 # For each test in @$TESTS, generate two additional tests,
570 # one using stdin, the other using a pipe. I.e., given this one
571 # ['idem-0', {IN=>''}, {OUT=>''}],
573 # ['idem-0.r', '<', {IN=>''}, {OUT=>''}],
574 # ['idem-0.p', {IN_PIPE=>''}, {OUT=>''}],
575 # Generate new tests only if there is exactly one input spec.
576 # The returned list of tests contains each input test, followed
577 # by zero or two derived tests.
582 foreach my $t (@$tests)
592 and push (@args, $e), next;
594 ref $e && ref $e eq 'HASH'
595 or (warn "$0: $t->[0]: unexpected entry type\n"), next;
597 and (push @in, $e->{IN}), next;
598 push @list_of_hash, $e;
600 # Add variants IFF there is exactly one input file.
603 shift @args; # discard test name
604 push @new, ["$t->[0].r", @args, '<', {IN => $in[0]}, @list_of_hash];
605 push @new, ["$t->[0].p", @args, {IN_PIPE => $in[0]}, @list_of_hash];