From 9a0b88bf7777c153dc46ace22db74ef24d51583a Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Tue, 29 Apr 2008 12:55:25 +0000 Subject: update release date to actual date git-svn-id: svn+q:///qpdf/trunk@599 71b93d88-0707-0410-a8cf-f5a4172ac649 --- qtest/bin/qtest-driver | 798 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 798 insertions(+) create mode 100755 qtest/bin/qtest-driver (limited to 'qtest/bin/qtest-driver') diff --git a/qtest/bin/qtest-driver b/qtest/bin/qtest-driver new file mode 100755 index 00000000..9ca1ec44 --- /dev/null +++ b/qtest/bin/qtest-driver @@ -0,0 +1,798 @@ +#!/usr/bin/env perl +# +# This file is part of qtest. +# +# Copyright 1993-2007, Jay Berkenbilt +# +# QTest is distributed under the terms of version 2.0 of the Artistic +# license which may be found in the source distribution. +# +require 5.008; +BEGIN { $^W = 1; } +use strict; +use IO::Handle; +use IO::File; +use IO::Socket; +use Cwd 'abs_path'; +use Cwd; +use Config; +use File::Copy; +use File::Basename; +use File::Spec; + +my $whoami = basename($0); +my $dirname = dirname(abs_path($0)); +my $cwd = getcwd(); +my $top = dirname($dirname); +my $module_dir = "$top/module"; +my $qtc_dir = "$top/QTC/perl"; + +unshift(@INC, $module_dir, $qtc_dir); +require QTC; +require TestDriver; + +if ((@ARGV == 1) && ($ARGV[0] eq '--version')) +{ + print "$whoami version 1.1\n"; + exit 0; +} +if ((@ARGV == 1) && ($ARGV[0] eq '--print-path')) +{ + print $top, "\n"; + exit 0; +} + +my @bindirs = (); +my $datadir = undef; +my $covdir = '.'; +my $stdout_tty = (-t STDOUT) ? "1" : "0"; + +while (@ARGV) +{ + my $arg = shift(@ARGV); + if ($arg eq '-bindirs') + { + usage() unless @ARGV; + push(@bindirs, split(':', shift(@ARGV))); + } + elsif ($arg eq '-datadir') + { + usage() unless @ARGV; + $datadir = shift(@ARGV); + } + elsif ($arg eq '-covdir') + { + usage() unless @ARGV; + $covdir = shift(@ARGV); + } + elsif ($arg =~ m/^-stdout-tty=([01])$/) + { + $stdout_tty = $1; + } + else + { + usage(); + } +} +usage() unless defined($datadir); +if (@bindirs) +{ + my @path = (); + foreach my $d (@bindirs) + { + my $abs = abs_path($d) or + fatal("can't canonicalize path to bindir $d: $!"); + push(@path, $abs); + } + my $path = join(':', @path) . ':' . $ENV{'PATH'}; + # Delete and explicitly recreate the PATH environment variable. + # This seems to be more reliable. If we just reassign, in some + # cases, the modified environment is not inherited by the child + # process. (This happens when qtest-driver is invoked from ant + # running from gjc-compat. I have no idea how or why.) + delete $ENV{'PATH'}; + $ENV{'PATH'} = $path; +} + +if ($stdout_tty) +{ + TestDriver::get_tty_features(); +} + +my $pid = undef; +my $pid_cleanup = new TestDriver::PidKiller(\$pid); + +# $in_testsuite is whether the test driver itself is being run from a +# test suite! Check before we set the environment variable. +my $in_testsuite = $ENV{'IN_TESTSUITE'} || 0; + +$ENV{'IN_TESTSUITE'} = 1; + +# Temporary path is intended to be easy to locate so its contents can +# be inspected by impatient test suite runners. It is not intended to +# be a "secure" (unpredictable) path. +my $tempdir = File::Spec->tmpdir() . "/testtemp.$$"; + +my $file_cleanup = new TestDriver::TmpFileDeleter([$tempdir]); + +$| = 1; +$SIG{'PIPE'} = 'IGNORE'; +$SIG{'INT'} = $SIG{'HUP'} = $SIG{'TERM'} = $SIG{'QUIT'} = sub { exit 2 }; + +TestDriver::rmrf($tempdir); +fatal("removal of $tempdir failed") if -e "$tempdir"; + +mkdir($tempdir, 0777) || die "mkdir $tempdir: $!\n"; +$tempdir = abs_path($tempdir) or + fatal("can't canonicalize path to $tempdir: $!"); + +my $errors = 0; + +my $tc_input = undef; +my $tc_scope = undef; +my @testcov = (<$covdir/*.testcov>); +if (@testcov > 1) +{ + fatal("more than one testcov file exists"); +} +elsif (@testcov) +{ + &QTC::TC("testdriver", "coverage directory", + ($covdir eq '.' ? 1 : 0)); + $tc_input = $testcov[0]; + $tc_input =~ s,^\./,,; + $tc_scope = basename($tc_input); + $tc_scope =~ s/\.testcov$// or + fatal("can't get scope from testcov filename"); +} + +my $testlogfile = 'qtest.log'; +my $testxmlfile = 'qtest-results.xml'; +unlink $testlogfile; +unlink $testxmlfile; + +my $totmissing = 0; +my $totextra = 0; +my $tottests = 0; +my $totpasses = 0; +my $totfails = 0; +my $totxpasses = 0; +my $totxfails = 0; + +my $now = ($in_testsuite ? '---timestamp---' : localtime(time)); +my $msg = "STARTING TESTS on $now"; +print "\n"; +print_and_log(('*' x length($msg)) . "\n$msg\n" . + ('*' x length($msg)) . "\n\n"); + +my $tc_log = undef; +my $tc_winlog = undef; +my %tc_cases = (); +my %tc_ignored_scopes = (); +parse_tc_file(); +tc_do_initial_checks(); + +my $tests_to_run; +defined($tests_to_run = $ENV{"TESTS"}) or $tests_to_run = ""; +my @tests = (); +if ($tests_to_run ne "") +{ + @tests = split(/\s+/, $tests_to_run); + for (@tests) + { + &QTC::TC("testdriver", "driver tests specified"); + $_ = "$datadir/$_.test"; + } +} +else +{ + &QTC::TC("testdriver", "driver tests not specified"); + @tests = <$datadir/*.test>; +} + +print_xml("\n" . + "\n"); +foreach my $test (@tests) +{ + print_and_log("\nRunning $test\n"); + print_xml(" \n"); + my @results = run_test($test); + if (scalar(@results) != 5) + { + error("test driver $test returned invalid results"); + } + else + { + my ($ntests, $passes, $fails, $xpasses, $xfails) = @results; + my $actual = $passes + $fails + $xpasses + $xfails; + my $extra = 0; + my $missing = 0; + if ($actual > $ntests) + { + &QTC::TC("testdriver", "driver extra tests"); + my $n = ($actual - $ntests); + print_and_log(sprintf("\n*** WARNING: saw $n extra test%s\n\n", + ($n == 1 ? "" : "s"))); + $extra = $n; + } + elsif ($actual < $ntests) + { + &QTC::TC("testdriver", "driver missing tests"); + my $n = ($ntests - $actual); + print_and_log(sprintf("\n*** WARNING: missing $n test%s\n\n", + ($n == 1 ? "" : "s"))); + $missing = $n; + } + + $totmissing += $missing; + $totextra += $extra; + $totpasses += $passes; + $totfails += $fails; + $totxpasses += $xpasses; + $totxfails += $xfails; + $tottests += ($passes + $fails + $xpasses + $xfails); + + my $passed = (($extra == 0) && ($missing == 0) && + ($fails == 0) && ($xpasses == 0)); + + print_xml(" \n"); + } + print_xml(" \n"); +} + +my $coverage_okay = 1; +tc_do_final_checks(); + +my $okay = ((($totpasses + $totxfails) == $tottests) && + ($errors == 0) && ($totmissing == 0) && ($totextra == 0) && + ($coverage_okay)); + +print "\n"; +print_and_pad("Overall test suite"); +if ($okay) +{ + &QTC::TC("testdriver", "driver overall pass"); + print_results(pass(), pass()); +} +else +{ + &QTC::TC("testdriver", "driver overall fail"); + print_results(fail(), pass()); + print "\nFailure summary may be found in $testlogfile\n"; +} + +my $summary = "\nTESTS COMPLETE. Summary:\n\n"; +$summary .= + sprintf("Total tests: %d\n" . + "Passes: %d\n" . + "Failures: %d\n" . + "Unexpected Passes: %d\n" . + "Expected Failures: %d\n" . + "Missing Tests: %d\n" . + "Extra Tests: %d\n", + $tottests, $totpasses, $totfails, $totxpasses, $totxfails, + $totmissing, $totextra); + +print_and_log($summary); +print "\n"; + +print_xml(" \n" . + "\n"); + +exit ($okay ? 0 : 2); + +sub run_test +{ + my $prog = shift; + my @results = (); + + # Open a socket for communication with subsidiary test drivers. + # Exchange some handshaking information over this socket. When + # the subsidiary test suite exits, it reports its results over the + # socket. + + my $use_socketpair = (defined $Config{d_sockpair}); + if ($Config{'osname'} eq 'cygwin') + { + $use_socketpair = 0; + } + + my $listensock; + my $for_parent; + my $for_child; + + my @comm_args = (); + + if ($use_socketpair) + { + socketpair($for_child, $for_parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC) + or fatal("socketpair: $!"); + my $fd = fileno($for_child); + close($for_child); + close($for_parent); + local $^F = $fd; # prevent control fd from being closed on exec + socketpair($for_child, $for_parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC) + or fatal("socketpair: $!"); + if (fileno($for_child) != $fd) + { + fatal("FOR_CHILD socket has wrong file descriptor number: got " . + fileno($for_child) . "; wanted $fd"); + } + $for_parent->autoflush(1); + $for_child->autoflush(1); + binmode $for_parent; + binmode $for_child; + @comm_args = ('-fd', $fd); + } + else + { + $listensock = IO::Socket::INET->new( + Listen => 1, Proto => 'tcp', LocalPort => 0) or + fatal("listen: $!"); + my ($s_port, $s_addr) = unpack_sockaddr_in($listensock->sockname()); + @comm_args = ('-port', $s_port); + } + + my $pid = fork; + fatal("fork failed: $!") unless defined $pid; + if ($pid == 0) + { + if ($use_socketpair) + { + close($for_parent); + } + chdir($datadir) or fatal("chdir $datadir failed: $!"); + + if (defined $tc_log) + { + # Set these environment variables in the child process so + # that we can actually use the coverage system + # successfully to test the test driver itself. + $ENV{'TC_SCOPE'} = $tc_scope; + $ENV{'TC_FILENAME'} = $tc_log; + if (defined $tc_winlog) + { + $ENV{'TC_WIN_FILENAME'} = $tc_winlog; + } + } + + # Clear this environment variable so that nested test suites + # don't inherit the value from this test suite. Note that as + # of perl 5.8.7 in cygwin, deleting an environment variable + # doesn't work. + $ENV{'TESTS'} = ""; + + exec +('perl', '-I', $module_dir, '-I', $qtc_dir, + basename($prog), + @comm_args, + '-origdir', $cwd, + '-tempdir', $tempdir, + '-testlog', "$cwd/$testlogfile", + '-testxml', "$cwd/$testxmlfile", + "-stdout-tty=$stdout_tty") or + fatal("exec $prog failed: $!"); + } + if ($use_socketpair) + { + close($for_child); + } + else + { + $for_parent = $listensock->accept() or die $!; + $for_parent->autoflush(); + $listensock->close(); + } + + eval + { + # Either CHLD or PIPE here indicates premature exiting of + # subsidiary process which will be detected by either a + # protocol error or a timeout on the select below. + local $SIG{'CHLD'} = local $SIG{'PIPE'} = 'IGNORE'; + print $for_parent "TEST_DRIVER 1\n" + or die "--child--\n"; + my $rin = ''; + vec($rin, fileno($for_parent), 1) = 1; + my $nfound = select($rin, '', '', 5); + if ($nfound == 0) + { + fatal("timed out waiting for input on $for_parent"); + } + # Setting to DEFAULT should be unnecessary because of "local" + # above, but there seems to be a race condition that this + # helps to correct. + $SIG{'CHLD'} = $SIG{'PIPE'} = 'DEFAULT'; + }; + if ($@) + { + if ($@ =~ m/--child--/) + { + error("subsidiary test driver exited"); + } + else + { + die $@; + } + } + else + { + my $line = <$for_parent>; + if (! ((defined $line) && ($line =~ m/^TEST_DRIVER_CLIENT 1$/))) + { + error("invalid protocol with subdiary test driver"); + kill 1, $pid; + } + waitpid $pid, 0; + my $results = <$for_parent>; + close($for_parent); + if (! ((defined $results) && ($results =~ m/^\d+(?: \d+){4}$/))) + { + &QTC::TC("testdriver", "driver test returned invalid results"); + error("invalid results from subsidiary test driver"); + } + else + { + @results = split(/ /, $results); + } + } + @results; +} + +sub parse_tc_file +{ + return unless defined $tc_input; + + my $tc = new IO::File("<$tc_input") or fatal("can't read $tc_input: $!"); + while (<$tc>) + { + s/\r?\n$//s; + next if m/^\#/; + next if m/^\s*$/; + if (m/^ignored-scope: (\S+)$/) + { + $tc_ignored_scopes{$1} = 1; + } + elsif (m/^\s*?(\S.+?)\s+(\d+)$/) + { + my ($case, $n) = ($1, $2); + if (exists $tc_cases{$case}) + { + &QTC::TC("testdriver", "driver duplicate coverage case"); + error("$tc_input:$.: duplicate case"); + } + $tc_cases{$case} = $n; + } + else + { + error("$tc_input:$.: invalid syntax"); + } + } + $tc->close(); +} + +sub tc_do_initial_checks +{ + return unless defined $tc_input; + + if (! exists $ENV{'TC_SRCS'}) + { + fatal("TC_SRCS must be set"); + } + + my @tc_srcs = (grep { m/\S/ } (split(/\s+/, $ENV{'TC_SRCS'}))); + + my %seen_cases = (); + foreach my $src (@tc_srcs) + { + my $s = new IO::File("<$src") or die "$whoami: open $src: $!\n"; + binmode $s; + while (<$s>) + { + # Look for coverage calls in the source subject to certain + # lexical constraints + my ($lscope, $case); + if (m/^\s*\&?QTC(?:::|\.)TC\(\"([^\"]+)\",\s*\"([^\"]+)\"/) + { + # C++, Java, Perl, etc. + ($lscope, $case) = ($1, $2); + } + elsif (m/^[^\#]*\$\(call QTC.TC,([^,]+),([^,\)]+)/) + { + # make + ($lscope, $case) = ($1, $2); + } + if ((defined $lscope) && (defined $case)) + { + if ($lscope eq $tc_scope) + { + push(@{$seen_cases{$case}}, [$src, $.]); + } + elsif (exists $tc_ignored_scopes{$lscope}) + { + &QTC::TC("testdriver", "driver ignored scope"); + } + else + { + &QTC::TC("testdriver", "driver out-of-scope case"); + error("$src:$.: out-of-scope coverage case"); + } + } + } + $s->close(); + } + + my %wanted_cases = %tc_cases; + foreach my $case (sort keys %seen_cases) + { + my $wanted = 1; + my $whybad = undef; + if (exists $wanted_cases{$case}) + { + delete $wanted_cases{$case}; + } + else + { + &QTC::TC("testdriver", "driver unregistered coverage case"); + $wanted = 0; + $whybad = "unregistered"; + } + if (scalar(@{$seen_cases{$case}}) > $wanted) + { + $whybad = $whybad || "duplicate"; + foreach my $d (@{$seen_cases{$case}}) + { + my ($file, $lineno) = @$d; + &QTC::TC("testdriver", "driver coverage error in src", + ($whybad eq 'unregistered' ? 0 : + $whybad eq 'duplicate' ? 1 : + 9999)); + error("$file:$lineno: $whybad coverage case \"$case\""); + } + } + } + foreach my $case (sort keys %wanted_cases) + { + &QTC::TC("testdriver", "driver unseen coverage case"); + error("$whoami: coverage case \"$case\" was not seen"); + } + + fatal("errors detected; exiting") if $errors; + + $tc_log = "$cwd/$tc_scope.cov_out"; + if ($^O eq 'cygwin') + { + chop(my $f = `cygpath --windows $tc_log`); + $tc_winlog = $f; + } + unlink $tc_log; + print_and_log("Test coverage active in scope $tc_scope\n"); +} + +sub tc_do_final_checks +{ + return unless (defined $tc_log); + + my %seen_cases = (); + my $tc = new IO::File("<$tc_log"); + if ($tc) + { + binmode $tc; + while (<$tc>) + { + s/\r?\n$//s; + next if m/^\#/; + next if m/^\s*$/; + if (m/^(.+) (\d+)$/) + { + $seen_cases{$1}{$2} = 1; + } + } + $tc->close(); + } + + my $testlog = open_log(); + + $testlog->print("\nTest coverage results:\n"); + + my @problems = (); + foreach my $c (sort keys %tc_cases) + { + my ($case, $n) = ($c, $tc_cases{$c}); + for (my $i = 0; $i <= $n; ++$i) + { + if (exists $seen_cases{$c}{$i}) + { + delete $seen_cases{$c}{$i}; + } + else + { + &QTC::TC("testdriver", "driver missing coverage case"); + push(@problems, "missing: $c $i"); + } + } + } + foreach my $c (sort keys %seen_cases) + { + foreach my $n (sort { $a <=> $b } (keys %{$seen_cases{$c}})) + { + &QTC::TC("testdriver", "driver extra coverage case"); + push(@problems, "extra: $c $n"); + } + } + + if (@problems) + { + my $testxml = open_xml(); + $testxml->print(" \n"); + foreach my $p (@problems) + { + $testlog->print("$p\n"); + $testxml->print(" \n"); + } + $testxml->print(" \n"); + $testxml->close(); + $testlog->print("coverage errors: " . scalar(@problems) . "\n"); + } + my $passed = (@problems == 0); + $testlog->print("\nCoverage analysis: ", ($passed ? 'PASSED' : 'FAILED'), + "\n"); + $testlog->close(); + + print "\n"; + print_and_pad("Coverage analysis"); + if ($passed) + { + print_results(pass(), pass()); + my $passlog = $tc_log; + $passlog =~ s/(\.[^\.]+)$/-passed$1/; + copy($tc_log, $passlog); + } + else + { + $coverage_okay = 0; + print_results(fail(), pass()); + } +} + +sub open_binary +{ + my $file = shift; + my $fh = new IO::File(">>$file") or fatal("can't open $file: $!"); + binmode $fh; + $fh; +} + +sub open_log +{ + open_binary($testlogfile); +} + +sub open_xml +{ + open_binary($testxmlfile); +} + +sub print_and_log +{ + my $fh = open_log(); + print @_; + print $fh @_; + $fh->close(); +} + +sub print_xml +{ + my $fh = open_xml(); + print $fh @_; + $fh->close(); +} + +sub print_and_pad +{ + TestDriver::print_and_pad(@_); +} + +sub print_results +{ + TestDriver::print_results(@_); +} + +sub pass +{ + TestDriver->PASS; +} + +sub fail +{ + TestDriver->FAIL; +} + +sub error +{ + my $msg = shift; + warn $msg, "\n"; + ++$errors; +} + +sub fatal +{ + my $msg = shift; + warn "$whoami: $msg\n"; + exit 2; +} + +sub usage +{ + warn " +Usage: $whoami --print-path + +Prints full path to ${whoami}'s installation directory and exits. + + - OR - + +Usage: $whoami options + +Options include: + + -datadir datadir + -bindirs bindir[:bindir...] + [ -covdir [coverage-dir] ] + [ -stdout-tty=[01] ] + +Subsidiary test programs are run with the -bindirs argument (a +colon-separated list of directories, which may be relative but will be +internally converted to absolute) prepended to the path and with the +-datadir argument set as the current working directory. + +By default, this program runs datadir/*.test as subsidiary test +suites. If the TESTS environment variable is set, it is taken to be a +space-separated list of test suite names. For each name n, +datadir/n.test is run. + +Test coverage support is built in. If a file whose name matches +*.testcov in the coverage directory (which defaults to \".\") that is +a valid test coverage file, the full path to the file into which test +coverage results are written will be placed in the TC_FILENAME +environment variable. (If running under cygwin, the Windows path will +be in TC_WIN_FILENAME.) The test coverage scope, which is equal to +the part of the testcov file name excluding the extension, is placed +in the TC_SCOPE environment variable. + +If the -stdout-tty option is passed, its value overrides ${whoami}'s +determination of whether standard output is a terminal. This can be +useful for cases in which another program is invoking ${whoami} and +passing its output through a pipe to a terminal. + +"; + exit 2; + +} -- cgit v1.2.3-70-g09d2