#!/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; }