if (defined($ENV{"CAULDRON"})) {
@ARGV = (
"-v",
"-d",
"/Users/mtrent/c/cctools_tests/test-cases",
);
}
use Cwd 'realpath';
use FileHandle;
use Getopt::Std;
$MAXCHILDREN = 4;
$CURCHILDREN = 0;
$PASS = 0;
@PLATFORMS = (
"MACOS",
"IOS",
"WATCHOS",
"TVOS",
);
%PLATFORMS = map { $_ => $_ } @PLATFORMS;
exit &main();
sub tests_dir_path {
my ($tests_dir) = "test-cases";
(my $basename = $0) =~ s|.*/||;
(my $path = $0) =~ s/${basename}$/$tests_dir/;
return $path;
}
sub find_tests {
my ($path) = @_;
opendir TESTSDIR, "$path" or die "can't read $path: $!\n";
my (@tests) = grep !/^\./, readdir TESTSDIR;
closedir TESTSDIR;
return sort @tests;
}
sub shellcmd {
my (@args) = @_;
my $rc = 0xffff & system @args;
if ($rc == 0xff00) {
warn "system failed: $!\n";
$rc = -1;
}
elsif ($rc > 0x80) {
$rc >>= 8;
}
elsif ($rc) {
my $w = "ran with ";
if ($rc & 0x80) {
$rc &= ~0x80;
$w .= "coredump from ";
}
$w .= "signal $rc";
warn "$w\n";
}
return $rc;
}
sub print_test {
my ($test, $note) = @_;
printf "%-32s %s\n", $test, $note;
}
sub indent {
my ($message) = @_;
my ($result);
foreach my $line (split '\n', $message) {
$result .= "\t$line\n";
}
return $result;
}
sub run_test {
my ($test, $platformRef, $cctools_root, $verbose) = @_;
my ($test_failed);
my ($logmsg);
my ($pass) = 0;
print "$test starting...\n" if ($verbose);
foreach $platform (@$platformRef) {
print "\tcleaning\n" if ($verbose);
&shellcmd("make clean > /dev/null");
print "\ttesting $platform\n" if ($verbose);
my $outfile = "/tmp/stdout.$$";
my $errfile = "/tmp/stderr.$$";
my $makearg = "PLATFORM=$platform";
$makearg .= " CCTOOLS_ROOT=$cctools_root" if (defined($cctools_root));
my $rc = &shellcmd("make $makearg 1>$outfile 2>$errfile");
chomp(my $errstr = `cat $errfile`);
chomp(my $outstr = `cat $outfile`);
if ($outstr =~ /^(FAIL|XFAIL)/m) {
$logmsg = "FAIL $platform";
$logmsg .= "\n" . &indent($outstr) if ($outstr);
$test_failed = 1;
}
elsif ($rc) {
$logmsg = "FAIL $platform Makefile failure";
$logmsg .= "\n" . &indent($errstr) if ($errstr);
$test_failed = 1;
}
elsif ($errstr ne "") {
$logmsg = "FAIL $platform spurious stderr failure";
$logmsg .= "\n" . &indent($errstr) if ($errstr);
$test_failed = 1;
}
elsif (!($outstr =~ /^(PASS|XPASS)/m)) {
$logmsg = "AMBIGIOUS $platform missing [X]PASS/[X]FAIL";
$test_failed = 1;
}
else {
$logmsg = "PASS" unless(defined($logmsg));
}
unlink $outfile;
unlink $errfile;
next if ($test_failed);
}
print "\tcleaning\n" if ($verbose);
&shellcmd("make clean > /dev/null");
&print_test($test, $logmsg) if ($logmsg);
$pass += 1 unless ($test_failed);
return $pass;
}
sub collect_test {
if ($CURCHILDREN) {
my $pid = wait;
my $status = $?;
die "no children: $!\n" if ($pid == -1);
$CURCHILDREN -= 1;
$path = "/tmp/result.$pid";
my $fh = new FileHandle($path) or
die "can't open $path: $!\n";
while (<$fh>) {
print;
}
$PASS += 1 if ($status == 0);
unlink $path;
}
}
sub schedule_test {
my ($test, $platformRef, $cctools_root, $verbose) = @_;
my $pid = fork;
die "fork: $!\n" unless defined ($pid);
if ($pid == 0) {
open(STDOUT, ">/tmp/result.$$") || die "can't redirect stdout: $!";
open(STDERR, ">&STDOUT") || die "can't dup stderr to stdout: $!";
exit (&run_test($test, $platformRef, $cctools_root, $verbose) == 0);
} else {
$CURCHILDREN += 1;
if ($CURCHILDREN >= $MAXCHILDREN) {
&collect_test();
}
}
}
sub main {
$opt_a = 1;
$opt_c = undef;
$opt_h = undef;
$opt_r = undef;
$opt_t = undef;
$opt_v = undef;
return &usage() unless (getopts('ac:r:t:hv'));
my ($async) = $opt_a;
my ($cctools_root) = $opt_c;
my ($tests_dir) = $opt_r;
my ($one_test) = $opt_t;
my ($verbose) = $opt_v;
if ($opt_h) {
return &usage();
}
$tests_dir = &tests_dir_path() unless(defined($tests_dir));
$tests_dir = realpath $tests_dir;
if ($async) {
my ($ncpu) = int(`sysctl -n hw.ncpu`);
if ($ncpu > $MAXCHILDREN) {
$MAXCHILDREN = $ncpu;
}
}
if (defined($cctools_root)) {
die "can't find $cctools_root\n" unless ( -e $cctools_root );
die "not a directory: $cctools_root\n" unless ( -d $cctools_root );
die "does not appear to be a cctools root: $cctools_root\n"
unless ( -e "$cctools_root/usr/bin/lipo" );
}
my (@tests) = &find_tests($tests_dir);
if ($one_test) {
my %tests = map { $_ => $_ } @tests;
die "test $one_test not found in $tests_dir\n" unless $tests{$one_test};
@tests = ( $one_test );
}
my (%platform_map);
my ($total, $pass);
printf "### running %s in $tests_dir\n",
defined($one_test) ? $one_test : "all tests";
foreach my $test (@tests)
{
$total += 1;
my ($makefile) = "$tests_dir/$test/Makefile";
unless ( -e "$makefile" ) {
&print_test($test, "FAIL missing Makefile");
next;
}
my (@platform_entries) = `grep -E '^#\\s*PLATFORMS?:' '$makefile'`;
unless (@platform_entries) {
&print_test($test, "FAIL Makefile contains no platforms");
next;
}
my (@known_platforms, @unknown_platforms);
foreach my $entry (@platform_entries) {
$entry =~ s/.*PLATFORMS?://;
foreach my $platform (split ' ', $entry) {
if ($PLATFORMS{$platform}) {
push @known_platforms, $platform;
} else {
push @unknown_platforms, $platform;
}
}
}
if (@unknown_platforms) {
my ($unknown) = join ' ', @unknown_platforms;
&print_test($test, "FAIL Makefile contains unknown platforms: $unknown");
next;
}
if (@known_platforms == 0) {
&print_test($test, "FAIL Makefile contains no known platforms");
next;
}
chdir "$tests_dir/$test" or die "can't cd to $tests_dir/$test: $!\n";
if ($async) {
&schedule_test($test, \@known_platforms, $cctools_root, $verbose);
} else {
$pass += &run_test($test, \@known_platforms, $cctools_root, $verbose);
}
}
if ($async) {
while ($CURCHILDREN) {
&collect_test();
}
$pass = $PASS;
}
printf "### %d of %d tests passed (%.1f percent)\n",
$pass, $total, ($pass / $total) * 100;
return $pass == $total ? 0 : 1;
}
sub usage {
(my $basename = $0) =~ s|.*/||;
print <<USAGE;
usage: $basename [-a] [-c cctools_root] [-r tests_dir] [-t test] [-v]
-a - run tests asynchronously, potentially boosting performance.
This value is currently on by default.
-c <dir> - use cctools installed in <dir>. The directory must be a root
filesystem; i.e., one containing ./usr/bin. By default, cctools
will all run through "xcrun", although individual tests have
the final say.
-r <dir> - run all the tests found in the repository <dir>. By default,
the test repository is a directory named "test-cases" in the
same directory as $basename.
-t <test> - run only the test named <test> from the test repository.
-v - verbose, print a line for each phase of the test.
USAGE
return 1;
}