#!/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.2\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, '', '', 60);
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;
}