#!/usr/bin/perl # test.pl # Run unit tests. use strict; use File::Basename; # We use encode_json() to write BATS plist files. # JSON::PP does not exist on iOS devices, but we need not write plists there. # So we simply load JSON:PP if it exists. if (eval { require JSON::PP; 1; }) { JSON::PP->import(); } chdir dirname $0; chomp (my $DIR = `pwd`); if (scalar(@ARGV) == 1) { my $arg = $ARGV[0]; if ($arg eq "-h" || $arg eq "-H" || $arg eq "-help" || $arg eq "help") { print(< OS=[sdk version][-[-]] ROOT=/path/to/project.roots/ CC= LANGUAGE=c,c++,objective-c,objective-c++,swift MEM=mrc,arc GUARDMALLOC=0|1|before|after BUILD=0|1 (build the tests?) RUN=0|1 (run the tests?) VERBOSE=0|1|2 (0=quieter 1=print commands executed 2=full test output) BATS=0|1 (build for and/or run in BATS?) examples: test installed library, x86_64 $0 test buildit-built root, i386 and x86_64, MRC and ARC, clang compiler $0 ARCH=i386,x86_64 ROOT=/tmp/objc4.roots MEM=mrc,arc CC=clang test buildit-built root with iOS simulator, deploy to iOS 7, run on iOS 8 $0 ARCH=x86_64 ROOT=/tmp/objc4.roots OS=iphonesimulator-7.0-8.0 test buildit-built root on attached iOS device $0 ARCH=arm64 ROOT=/tmp/objc4.roots OS=iphoneos END exit 0; } } ######################################################################### ## Tests # Maps test name => test's filename extension. # ex: "msgSend" => "m" # `keys %ALL_TESTS` is also used as the list of all tests found on disk. my %ALL_TESTS; ######################################################################### ## Variables for use in complex build and run rules # variable # example value # things you can multiplex on the command line # ARCH=i386,x86_64,armv6,armv7 # OS=macosx,iphoneos,iphonesimulator (plus sdk/deployment/run versions) # LANGUAGE=c,c++,objective-c,objective-c++,swift # CC=clang # MEM=mrc,arc # GUARDMALLOC=0,1,before,after # things you can set once on the command line # ROOT=/path/to/project.roots # BUILD=0|1 # RUN=0|1 # VERBOSE=0|1|2 # BATS=0|1 # environment variables from the command line # DSTROOT # OBJROOT # (SRCROOT is ignored; test sources are assumed to # be in the same directory as the test script itself.) # fixme SYMROOT for dsymutil output? # Some arguments as read from the command line. my %args; my $BUILD; my $RUN; my $VERBOSE; my $BATS; my $HOST; my $PORT; my @TESTLIBNAMES = ("libobjc.A.dylib", "libobjc-trampolines.dylib"); my $TESTLIBDIR = "/usr/lib"; # Top level directory for intermediate and final build products. # Intermediate files must be kept separate for XBS BATS builds. my $OBJROOT = $ENV{OBJROOT} || ""; my $DSTROOT = $ENV{DSTROOT} || ""; # Build product directory inside DSTROOT and OBJROOT. # Each test config gets its own build directory inside this. my $BUILDDIR; # Local top-level directory. # This is the default value for $BUILDDIR. my $LOCALBASE = "/tmp/test-$TESTLIBNAMES[0]-build"; # Device-side top-level directory. # This replaces $DSTROOT$BUILDDIR/ for on-device execution. my $REMOTEBASE = "/AppleInternal/objctest"; # BATS top-level directory. # This replaces $DSTROOT$BUILDDIR/ for BATS execution. my $BATSBASE = "/AppleInternal/CoreOS/tests/objc4"; my $crashcatch = <<'END'; // interpose-able code to catch crashes, print, and exit cleanly #include #include #include // from dyld-interposing.h #define DYLD_INTERPOSE(_replacement,_replacee) __attribute__((used)) static struct{ const void* replacement; const void* replacee; } _interpose_##_replacee __attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacement, (const void*)(unsigned long)&_replacee }; static void catchcrash(int sig) { const char *msg; switch (sig) { case SIGILL: msg = "CRASHED: SIGILL"; break; case SIGBUS: msg = "CRASHED: SIGBUS"; break; case SIGSYS: msg = "CRASHED: SIGSYS"; break; case SIGSEGV: msg = "CRASHED: SIGSEGV"; break; case SIGTRAP: msg = "CRASHED: SIGTRAP"; break; case SIGABRT: msg = "CRASHED: SIGABRT"; break; default: msg = "unknown signal"; break; } write(STDERR_FILENO, msg, strlen(msg)); // avoid backslash-n newline due to escaping differences somewhere // in BATS versus local execution (perhaps different perl versions?) char newline = 0xa; write(STDERR_FILENO, &newline, 1); _exit(1); } static void setupcrash(void) __attribute__((constructor)); static void setupcrash(void) { signal(SIGILL, &catchcrash); signal(SIGBUS, &catchcrash); signal(SIGSYS, &catchcrash); signal(SIGSEGV, &catchcrash); signal(SIGTRAP, &catchcrash); signal(SIGABRT, &catchcrash); } static int hacked = 0; ssize_t hacked_write(int fildes, const void *buf, size_t nbyte) { if (!hacked) { setupcrash(); hacked = 1; } return write(fildes, buf, nbyte); } DYLD_INTERPOSE(hacked_write, write); END ######################################################################### ## Harness # map language to buildable extensions for that language my %extensions_for_language = ( "c" => ["c"], "objective-c" => ["c", "m"], "c++" => ["c", "cc", "cp", "cpp", "cxx", "c++"], "objective-c++" => ["c", "m", "cc", "cp", "cpp", "cxx", "c++", "mm"], "swift" => ["swift"], "any" => ["c", "m", "cc", "cp", "cpp", "cxx", "c++", "mm", "swift"], ); # map extension to languages my %languages_for_extension = ( "c" => ["c", "objective-c", "c++", "objective-c++"], "m" => ["objective-c", "objective-c++"], "mm" => ["objective-c++"], "cc" => ["c++", "objective-c++"], "cp" => ["c++", "objective-c++"], "cpp" => ["c++", "objective-c++"], "cxx" => ["c++", "objective-c++"], "c++" => ["c++", "objective-c++"], "swift" => ["swift"], ); # Run some newline-separated commands like `make` would, stopping if any fail # run("cmd1 \n cmd2 \n cmd3") sub make { my $output = ""; my @cmds = split("\n", $_[0]); die if scalar(@cmds) == 0; $? = 0; foreach my $cmd (@cmds) { chomp $cmd; next if $cmd =~ /^\s*$/; $cmd .= " 2>&1"; print "$cmd\n" if $VERBOSE; $output .= `$cmd`; last if $?; } print "$output\n" if $VERBOSE; return $output; } sub chdir_verbose { my $dir = shift || die; print "cd $dir\n" if $VERBOSE; chdir $dir || die "couldn't cd $dir"; } sub rm_rf_verbose { my $dir = shift || die; print "mkdir -p $dir\n" if $VERBOSE; `rm -rf '$dir'`; die "couldn't rm -rf $dir" if $?; } sub mkdir_verbose { my $dir = shift || die; print "mkdir -p $dir\n" if $VERBOSE; `mkdir -p '$dir'`; die "couldn't mkdir $dir" if $?; } # xterm colors my $red = "\e[41;37m"; my $yellow = "\e[43;30m"; my $nocolor = "\e[0m"; if (! -t STDIN) { # Not isatty. Don't use colors. $red = ""; $yellow = ""; $nocolor = ""; } # print text with a colored prefix on each line # fixme some callers pass an array of lines and some don't sub colorprefix { my $color = shift; while (defined(my $lines = shift)) { $lines = "\n" if ($lines eq ""); for my $line (split(/^/, $lines)) { chomp $line; print "$color $nocolor$line\n"; } } } # print text colored # fixme some callers pass an array of lines and some don't sub colorprint { my $color = shift; while (defined(my $lines = shift)) { $lines = "\n" if ($lines eq ""); for my $line (split(/^/, $lines)) { chomp $line; print "$color$line$nocolor\n"; } } } # Return test names from the command line. # Returns all tests if no tests were named. sub gettests { my @tests; foreach my $arg (@ARGV) { push @tests, $arg if ($arg !~ /=/ && $arg !~ /^-/); } opendir(my $dir, $DIR) || die; while (my $file = readdir($dir)) { my ($name, $ext) = ($file =~ /^([^.]+)\.([^.]+)$/); next if ! $languages_for_extension{$ext}; open(my $in, "< $file") || die "$file"; my $contents = join "", <$in>; if (defined $ALL_TESTS{$name}) { colorprint $yellow, "SKIP: multiple tests named '$name'; skipping file '$file'."; } else { $ALL_TESTS{$name} = $ext if ($contents =~ m#^[/*\s]*TEST_#m); } close($in); } closedir($dir); if (scalar(@tests) == 0) { @tests = keys %ALL_TESTS; } @tests = sort @tests; return @tests; } # Turn a C compiler name into a C++ compiler name. sub cplusplus { my ($c) = @_; if ($c =~ /cc/) { $c =~ s/cc/\+\+/; return $c; } return $c . "++"; # e.g. clang => clang++ } # Turn a C compiler name into a Swift compiler name sub swift { my ($c) = @_; $c =~ s#[^/]*$#swift#; return $c; } # Returns an array of all sdks from `xcodebuild -showsdks` my @sdks_memo; sub getsdks { if (!@sdks_memo) { @sdks_memo = (`xcodebuild -showsdks` =~ /-sdk (.+)$/mg); } return @sdks_memo; } my %sdk_path_memo = {}; sub getsdkpath { my ($sdk) = @_; if (!defined $sdk_path_memo{$sdk}) { ($sdk_path_memo{$sdk}) = (`xcodebuild -version -sdk '$sdk' Path` =~ /^\s*(.+?)\s*$/); } return $sdk_path_memo{$sdk}; } # Extract a version number from a string. # Ignore trailing "internal". sub versionsuffix { my ($str) = @_; my ($vers) = ($str =~ /([0-9]+\.[0-9]+)(?:\.?internal)?$/); return $vers; } sub majorversionsuffix { my ($str) = @_; my ($vers) = ($str =~ /([0-9]+)\.[0-9]+(?:\.?internal)?$/); return $vers; } sub minorversionsuffix { my ($str) = @_; my ($vers) = ($str =~ /[0-9]+\.([0-9]+)(?:\.?internal)?$/); return $vers; } # Compares two SDK names and returns the newer one. # Assumes the two SDKs are the same OS. sub newersdk { my ($lhs, $rhs) = @_; # Major version wins. my $lhsMajor = majorversionsuffix($lhs); my $rhsMajor = majorversionsuffix($rhs); if ($lhsMajor > $rhsMajor) { return $lhs; } if ($lhsMajor < $rhsMajor) { return $rhs; } # Minor version wins. my $lhsMinor = minorversionsuffix($lhs); my $rhsMinor = minorversionsuffix($rhs); if ($lhsMinor > $rhsMinor) { return $lhs; } if ($lhsMinor < $rhsMinor) { return $rhs; } # Lexically-last wins (i.e. internal is better than not internal) if ($lhs gt $rhs) { return $lhs; } return $rhs; } sub rewind { seek($_[0], 0, 0); } # parse name=value,value pairs sub readconditions { my ($conditionstring) = @_; my %results; my @conditions = ($conditionstring =~ /\w+=(?:[^\s,]+,?)+/g); for my $condition (@conditions) { my ($name, $values) = ($condition =~ /(\w+)=(.+)/); $results{$name} = [split ',', $values]; } return %results; } sub check_output { my %C = %{shift()}; my $name = shift; my @output = @_; my %T = %{$C{"TEST_$name"}}; # Quietly strip MallocScribble before saving the "original" output # because it is distracting. filter_malloc(\@output); my @original_output = @output; # Run result-checking passes, reducing @output each time my $xit = 1; my $bad = ""; my $warn = ""; my $runerror = $T{TEST_RUN_OUTPUT}; filter_hax(\@output); filter_verbose(\@output); filter_simulator(\@output); $warn = filter_warn(\@output); $bad |= filter_guardmalloc(\@output) if ($C{GUARDMALLOC}); $bad |= filter_valgrind(\@output) if ($C{VALGRIND}); $bad = filter_expected(\@output, \%C, $name) if ($bad eq ""); $bad = filter_bad(\@output) if ($bad eq ""); # OK line should be the only one left $bad = "(output not 'OK: $name')" if ($bad eq "" && (scalar(@output) != 1 || $output[0] !~ /^OK: $name/)); if ($bad ne "") { colorprint $red, "FAIL: /// test '$name' \\\\\\"; colorprefix $red, @original_output; colorprint $red, "FAIL: \\\\\\ test '$name' ///"; colorprint $red, "FAIL: $name: $bad"; $xit = 0; } elsif ($warn ne "") { colorprint $yellow, "PASS: /// test '$name' \\\\\\"; colorprefix $yellow, @original_output; colorprint $yellow, "PASS: \\\\\\ test '$name' ///"; print "PASS: $name (with warnings)\n"; } else { print "PASS: $name\n"; } return $xit; } sub filter_expected { my $outputref = shift; my %C = %{shift()}; my $name = shift; my %T = %{$C{"TEST_$name"}}; my $runerror = $T{TEST_RUN_OUTPUT} || return ""; my $bad = ""; my $output = join("\n", @$outputref) . "\n"; if ($output !~ /$runerror/) { $bad = "(run output does not match TEST_RUN_OUTPUT)"; @$outputref = ("FAIL: $name"); } else { @$outputref = ("OK: $name"); # pacify later filter } return $bad; } sub filter_bad { my $outputref = shift; my $bad = ""; my @new_output; for my $line (@$outputref) { if ($line =~ /^BAD: (.*)/) { $bad = "(failed)"; } else { push @new_output, $line; } } @$outputref = @new_output; return $bad; } sub filter_warn { my $outputref = shift; my $warn = ""; my @new_output; for my $line (@$outputref) { if ($line !~ /^WARN: (.*)/) { push @new_output, $line; } else { $warn = "(warned)"; } } @$outputref = @new_output; return $warn; } sub filter_verbose { my $outputref = shift; my @new_output; for my $line (@$outputref) { if ($line !~ /^VERBOSE: (.*)/) { push @new_output, $line; } } @$outputref = @new_output; } sub filter_simulator { my $outputref = shift; my @new_output; for my $line (@$outputref) { if (($line !~ /No simulator devices appear to be running/) && ($line !~ /CoreSimulator is attempting to unload a stale CoreSimulatorService job/) && ($line !~ /Failed to locate a valid instance of CoreSimulatorService/)) { push @new_output, $line; } } @$outputref = @new_output; } sub filter_hax { my $outputref = shift; my @new_output; for my $line (@$outputref) { if ($line !~ /Class OS_tcp_/) { push @new_output, $line; } } @$outputref = @new_output; } sub filter_valgrind { my $outputref = shift; my $errors = 0; my $leaks = 0; my @new_output; for my $line (@$outputref) { if ($line =~ /^Approx: do_origins_Dirty\([RW]\): missed \d bytes$/) { # --track-origins warning (harmless) next; } if ($line =~ /^UNKNOWN __disable_threadsignal is unsupported. This warning will not be repeated.$/) { # signals unsupported (harmless) next; } if ($line =~ /^UNKNOWN __pthread_sigmask is unsupported. This warning will not be repeated.$/) { # signals unsupported (harmless) next; } if ($line !~ /^^\.*==\d+==/) { # not valgrind output push @new_output, $line; next; } my ($errcount) = ($line =~ /==\d+== ERROR SUMMARY: (\d+) errors/); if (defined $errcount && $errcount > 0) { $errors = 1; } (my $leakcount) = ($line =~ /==\d+==\s+(?:definitely|possibly) lost:\s+([0-9,]+)/); if (defined $leakcount && $leakcount > 0) { $leaks = 1; } } @$outputref = @new_output; my $bad = ""; $bad .= "(valgrind errors)" if ($errors); $bad .= "(valgrind leaks)" if ($leaks); return $bad; } sub filter_malloc { my $outputref = shift; my $errors = 0; my @new_output; my $count = 0; for my $line (@$outputref) { # Ignore MallocScribble prologue. # Ignore MallocStackLogging prologue. if ($line =~ /malloc: enabling scribbling to detect mods to free/ || $line =~ /Deleted objects will be dirtied by the collector/ || $line =~ /malloc: stack logs being written into/ || $line =~ /malloc: stack logs deleted from/ || $line =~ /malloc: process \d+ no longer exists/ || $line =~ /malloc: recording malloc and VM allocation stacks/) { next; } # not malloc output push @new_output, $line; } @$outputref = @new_output; } sub filter_guardmalloc { my $outputref = shift; my $errors = 0; my @new_output; my $count = 0; for my $line (@$outputref) { if ($line !~ /^GuardMalloc\[[^\]]+\]: /) { # not guardmalloc output push @new_output, $line; next; } # Ignore 4 lines of guardmalloc prologue. # Anything further is a guardmalloc error. if (++$count > 4) { $errors = 1; } } @$outputref = @new_output; my $bad = ""; $bad .= "(guardmalloc errors)" if ($errors); return $bad; } # TEST_SOMETHING # text # text # END sub extract_multiline { my ($flag, $contents, $name) = @_; if ($contents =~ /$flag\n/) { my ($output) = ($contents =~ /$flag\n(.*?\n)END[ *\/]*\n/s); die "$name used $flag without END\n" if !defined($output); return $output; } return undef; } # TEST_SOMETHING # text # OR # text # END sub extract_multiple_multiline { my ($flag, $contents, $name) = @_; if ($contents =~ /$flag\n/) { my ($output) = ($contents =~ /$flag\n(.*?\n)END[ *\/]*\n/s); die "$name used $flag without END\n" if !defined($output); $output =~ s/\nOR\n/\n|/sg; $output = "^(" . $output . ")\$"; return $output; } return undef; } sub gather_simple { my $CREF = shift; my %C = %{$CREF}; my $name = shift; chdir_verbose $DIR; my $ext = $ALL_TESTS{$name}; my $file = "$name.$ext"; return 0 if !$file; # search file for 'TEST_CONFIG' or '#include "test.h"' # also collect other values: # TEST_DISABLED disable test with an optional message # TEST_CRASHES test is expected to crash # TEST_CONFIG test conditions # TEST_ENV environment prefix # TEST_CFLAGS compile flags # TEST_BUILD build instructions # TEST_BUILD_OUTPUT expected build stdout/stderr # TEST_RUN_OUTPUT expected run stdout/stderr open(my $in, "< $file") || die; my $contents = join "", <$in>; my $test_h = ($contents =~ /^\s*#\s*(include|import)\s*"test\.h"/m); my ($disabled) = ($contents =~ /\b(TEST_DISABLED\b.*)$/m); my $crashes = ($contents =~ /\bTEST_CRASHES\b/m); my ($conditionstring) = ($contents =~ /\bTEST_CONFIG\b(.*)$/m); my ($envstring) = ($contents =~ /\bTEST_ENV\b(.*)$/m); my ($cflags) = ($contents =~ /\bTEST_CFLAGS\b(.*)$/m); my ($buildcmd) = extract_multiline("TEST_BUILD", $contents, $name); my ($builderror) = extract_multiple_multiline("TEST_BUILD_OUTPUT", $contents, $name); my ($runerror) = extract_multiple_multiline("TEST_RUN_OUTPUT", $contents, $name); return 0 if !$test_h && !$disabled && !$crashes && !defined($conditionstring) && !defined($envstring) && !defined($cflags) && !defined($buildcmd) && !defined($builderror) && !defined($runerror); if ($disabled) { colorprint $yellow, "SKIP: $name (disabled by $disabled)"; return 0; } # check test conditions my $run = 1; my %conditions = readconditions($conditionstring); if (! $conditions{LANGUAGE}) { # implicit language restriction from file extension $conditions{LANGUAGE} = $languages_for_extension{$ext}; } for my $condkey (keys %conditions) { my @condvalues = @{$conditions{$condkey}}; # special case: RUN=0 does not affect build if ($condkey eq "RUN" && @condvalues == 1 && $condvalues[0] == 0) { $run = 0; next; } my $testvalue = $C{$condkey}; next if !defined($testvalue); # testvalue is the configuration being run now # condvalues are the allowed values for this test my $ok = 0; for my $condvalue (@condvalues) { # special case: objc and objc++ if ($condkey eq "LANGUAGE") { $condvalue = "objective-c" if $condvalue eq "objc"; $condvalue = "objective-c++" if $condvalue eq "objc++"; } $ok = 1 if ($testvalue eq $condvalue); # special case: CC and CXX allow substring matches if ($condkey eq "CC" || $condkey eq "CXX") { $ok = 1 if ($testvalue =~ /$condvalue/); } last if $ok; } if (!$ok) { my $plural = (@condvalues > 1) ? "one of: " : ""; print "SKIP: $name ($condkey=$testvalue, but test requires $plural", join(' ', @condvalues), ")\n"; return 0; } } # save some results for build and run phases $$CREF{"TEST_$name"} = { TEST_BUILD => $buildcmd, TEST_BUILD_OUTPUT => $builderror, TEST_CRASHES => $crashes, TEST_RUN_OUTPUT => $runerror, TEST_CFLAGS => $cflags, TEST_ENV => $envstring, TEST_RUN => $run, DSTDIR => "$C{DSTDIR}/$name.build", OBJDIR => "$C{OBJDIR}/$name.build", }; return 1; } # Test description plist to write when building for BATS execution. my %bats_plist; $bats_plist{'Project'} = "objc4"; $bats_plist{'Tests'} = []; # populated by append_bats_test() # Saves run instructions for a single test in all configurations as a BATS test. sub append_bats_test { my $name = shift; my $arch = join(',', @{$args{ARCH}}); my $os = join(',', @{$args{OSVERSION}}); my $mem = join(',', @{$args{MEM}}); my $language = join(',', @{$args{LANGUAGE}}); push @{$bats_plist{'Tests'}}, { "TestName" => "$name", "Command" => [ "/usr/bin/perl", "$BATSBASE/test/test.pl", $name, "ARCH=$arch", "OS=$os", "MEM=$mem", "LANGUAGE=$language", "BUILD=0", "RUN=1", "VERBOSE=1", "BATS=1", ] }; } # Builds a simple test sub build_simple { my %C = %{shift()}; my $name = shift; my %T = %{$C{"TEST_$name"}}; mkdir_verbose $T{DSTDIR}; chdir_verbose $T{DSTDIR}; # we don't mkdir $T{OBJDIR} because most tests don't use it my $ext = $ALL_TESTS{$name}; my $file = "$DIR/$name.$ext"; if ($T{TEST_CRASHES}) { `echo '$crashcatch' > crashcatch.c`; make("$C{COMPILE_C} -dynamiclib -o libcrashcatch.dylib -x c crashcatch.c"); die "$?" if $?; } my $cmd = $T{TEST_BUILD} ? eval "return \"$T{TEST_BUILD}\"" : "$C{COMPILE} $T{TEST_CFLAGS} $file -o $name.exe"; my $output = make($cmd); # ignore out-of-date text-based stubs (caused by ditto into SDK) $output =~ s/ld: warning: text-based stub file.*\n//g; # rdar://10163155 $output =~ s/ld: warning: could not create compact unwind for [^\n]+: does not use standard frame\n//g; # rdar://37937122 $output =~ s/^warning: Cannot lower [^\n]+\n//g; $output =~ s/^warning: key: [^\n]+\n//g; $output =~ s/^warning: discriminator: [^\n]+\n//g; $output =~ s/^warning: callee: [^\n]+\n//g; # rdar://38710948 $output =~ s/ld: warning: ignoring file [^\n]*libclang_rt\.bridgeos\.a[^\n]*\n//g; # ignore compiler logging of CCC_OVERRIDE_OPTIONS effects if (defined $ENV{CCC_OVERRIDE_OPTIONS}) { $output =~ s/### (CCC_OVERRIDE_OPTIONS:|Adding argument|Deleting argument|Replacing) [^\n]*\n//g; } my $ok; if (my $builderror = $T{TEST_BUILD_OUTPUT}) { # check for expected output and ignore $? if ($output =~ /$builderror/) { $ok = 1; } elsif (defined $ENV{CCC_OVERRIDE_OPTIONS} && $builderror =~ /warning:/) { # CCC_OVERRIDE_OPTIONS manipulates compiler diagnostics. # Don't enforce any TEST_BUILD_OUTPUT that looks for warnings. colorprint $yellow, "WARN: /// test '$name' \\\\\\"; colorprefix $yellow, $output; colorprint $yellow, "WARN: \\\\\\ test '$name' ///"; colorprint $yellow, "WARN: $name (build output does not match TEST_BUILD_OUTPUT; not fatal because CCC_OVERRIDE_OPTIONS is set)"; $ok = 1; } else { colorprint $red, "FAIL: /// test '$name' \\\\\\"; colorprefix $red, $output; colorprint $red, "FAIL: \\\\\\ test '$name' ///"; colorprint $red, "FAIL: $name (build output does not match TEST_BUILD_OUTPUT)"; $ok = 0; } } elsif ($?) { colorprint $red, "FAIL: /// test '$name' \\\\\\"; colorprefix $red, $output; colorprint $red, "FAIL: \\\\\\ test '$name' ///"; colorprint $red, "FAIL: $name (build failed)"; $ok = 0; } elsif ($output ne "") { colorprint $red, "FAIL: /// test '$name' \\\\\\"; colorprefix $red, $output; colorprint $red, "FAIL: \\\\\\ test '$name' ///"; colorprint $red, "FAIL: $name (unexpected build output)"; $ok = 0; } else { $ok = 1; } if ($ok) { foreach my $file (glob("*.exe *.dylib *.bundle")) { if (!$BATS) { # not for BATS to save space and build time # fixme use SYMROOT? make("xcrun dsymutil $file"); } if ($C{OS} eq "macosx" || $C{OS} =~ /simulator/) { # setting any entitlements disables dyld environment variables } else { # get-task-allow entitlement is required # to enable dyld environment variables make("xcrun codesign -s - --entitlements $DIR/get_task_allow_entitlement.plist $file"); die "$?" if $?; } } } return $ok; } # Run a simple test (testname.exe, with error checking of stdout and stderr) sub run_simple { my %C = %{shift()}; my $name = shift; my %T = %{$C{"TEST_$name"}}; if (! $T{TEST_RUN}) { print "PASS: $name (build only)\n"; return 1; } my $testdir = $T{DSTDIR}; chdir_verbose $testdir; my $env = "$C{ENV} $T{TEST_ENV}"; if ($T{TEST_CRASHES}) { $env .= " OBJC_DEBUG_DONT_CRASH=YES"; } my $output; if ($C{ARCH} =~ /^arm/ && `uname -p` !~ /^arm/) { # run on iOS or watchos or tvos device # fixme device selection and verification my $remotedir = "$REMOTEBASE/" . basename($C{DSTDIR}) . "/$name.build"; # Add test dir and libobjc's dir to DYLD_LIBRARY_PATH. # Insert libcrashcatch.dylib if necessary. $env .= " DYLD_LIBRARY_PATH=$remotedir"; $env .= ":$REMOTEBASE" if ($C{TESTLIBDIR} ne $TESTLIBDIR); if ($T{TEST_CRASHES}) { $env .= " DYLD_INSERT_LIBRARIES=$remotedir/libcrashcatch.dylib"; } my $cmd = "ssh -p $PORT $HOST 'cd $remotedir && env $env ./$name.exe'"; $output = make("$cmd"); } elsif ($C{OS} =~ /simulator/) { # run locally in a simulator # fixme selection of simulated OS version my $simdevice; if ($C{OS} =~ /iphonesimulator/) { $simdevice = 'iPhone X'; } elsif ($C{OS} =~ /watchsimulator/) { $simdevice = 'Apple Watch Series 4 - 40mm'; } elsif ($C{OS} =~ /tvsimulator/) { $simdevice = 'Apple TV 1080p'; } else { die "unknown simulator $C{OS}\n"; } my $sim = "xcrun -sdk iphonesimulator simctl spawn '$simdevice'"; # Add test dir and libobjc's dir to DYLD_LIBRARY_PATH. # Insert libcrashcatch.dylib if necessary. $env .= " DYLD_LIBRARY_PATH=$testdir"; $env .= ":" . $C{TESTLIBDIR} if ($C{TESTLIBDIR} ne $TESTLIBDIR); if ($T{TEST_CRASHES}) { $env .= " DYLD_INSERT_LIBRARIES=$testdir/libcrashcatch.dylib"; } my $simenv = ""; foreach my $keyvalue (split(' ', $env)) { $simenv .= "SIMCTL_CHILD_$keyvalue "; } # Use the full path here so hack_cwd in test.h works. $output = make("env $simenv $sim $testdir/$name.exe"); } else { # run locally # Add test dir and libobjc's dir to DYLD_LIBRARY_PATH. # Insert libcrashcatch.dylib if necessary. $env .= " DYLD_LIBRARY_PATH=$testdir"; $env .= ":" . $C{TESTLIBDIR} if ($C{TESTLIBDIR} ne $TESTLIBDIR); if ($T{TEST_CRASHES}) { $env .= " DYLD_INSERT_LIBRARIES=$testdir/libcrashcatch.dylib"; } $output = make("sh -c '$env ./$name.exe'"); } return check_output(\%C, $name, split("\n", $output)); } my %compiler_memo; sub find_compiler { my ($cc, $toolchain, $sdk_path) = @_; # memoize my $key = $cc . ':' . $toolchain; my $result = $compiler_memo{$key}; return $result if defined $result; $result = make("xcrun -toolchain $toolchain -find $cc 2>/dev/null"); chomp $result; $compiler_memo{$key} = $result; return $result; } sub dirContainsAllTestLibs { my $dir = shift; foreach my $testlib (@TESTLIBNAMES) { my $found = (-e "$dir/$testlib"); my $foundstr = ($found ? "found" : "didn't find"); print "note: $foundstr $testlib in $dir\n" if ($VERBOSE); return 0 if (!$found); } return 1; } sub make_one_config { my $configref = shift; my $root = shift; my %C = %{$configref}; # Aliases $C{LANGUAGE} = "objective-c" if $C{LANGUAGE} eq "objc"; $C{LANGUAGE} = "objective-c++" if $C{LANGUAGE} eq "objc++"; # Interpret OS version string from command line. my ($sdk_arg, $deployment_arg, $run_arg, undef) = split('-', $C{OSVERSION}); delete $C{OSVERSION}; my ($os_arg) = ($sdk_arg =~ /^([^\.0-9]+)/); $deployment_arg = "default" if !defined($deployment_arg); $run_arg = "default" if !defined($run_arg); my %allowed_os_args = ( "macosx" => "macosx", "osx" => "macosx", "macos" => "macosx", "iphoneos" => "iphoneos", "ios" => "iphoneos", "iphonesimulator" => "iphonesimulator", "iossimulator" => "iphonesimulator", "watchos" => "watchos", "watchsimulator" => "watchsimulator", "watchossimulator" => "watchsimulator", "appletvos" => "appletvos", "tvos" => "appletvos", "appletvsimulator" => "appletvsimulator", "tvsimulator" => "appletvsimulator", "bridgeos" => "bridgeos", ); $C{OS} = $allowed_os_args{$os_arg} || die "unknown OS '$os_arg' (expected " . join(', ', sort keys %allowed_os_args) . ")\n"; # set the config name now, after massaging the language and OS versions, # but before adding other settings my $configname = config_name(%C); die if ($configname =~ /'/); die if ($configname =~ / /); ($C{NAME} = $configname) =~ s/~/ /g; (my $configdir = $configname) =~ s#/##g; $C{DSTDIR} = "$DSTROOT$BUILDDIR/$configdir"; $C{OBJDIR} = "$OBJROOT$BUILDDIR/$configdir"; # Allow tests to see BATS-edness in TEST_CONFIG. $C{BATS} = $BATS; if ($C{OS} eq "iphoneos" || $C{OS} eq "iphonesimulator") { $C{TOOLCHAIN} = "ios"; } elsif ($C{OS} eq "watchos" || $C{OS} eq "watchsimulator") { $C{TOOLCHAIN} = "watchos"; } elsif ($C{OS} eq "appletvos" || $C{OS} eq "appletvsimulator") { $C{TOOLCHAIN} = "appletvos"; } elsif ($C{OS} eq "bridgeos") { $C{TOOLCHAIN} = "bridgeos"; } elsif ($C{OS} eq "macosx") { $C{TOOLCHAIN} = "osx"; } else { colorprint $yellow, "WARN: don't know toolchain for OS $C{OS}"; $C{TOOLCHAIN} = "default"; } if ($BUILD) { # Look up SDK. # Try exact match first. # Then try lexically-last prefix match # (so "macosx" => "macosx10.7internal") $sdk_arg =~ s/$os_arg/$C{OS}/; my @sdks = getsdks(); if ($VERBOSE) { print "note: Installed SDKs: @sdks\n"; } my $exactsdk = undef; my $prefixsdk = undef; foreach my $sdk (@sdks) { $exactsdk = $sdk if ($sdk eq $sdk_arg); $prefixsdk = newersdk($sdk, $prefixsdk) if ($sdk =~ /^$sdk_arg/); } my $sdk; if ($exactsdk) { $sdk = $exactsdk; } elsif ($prefixsdk) { $sdk = $prefixsdk; } else { die "unknown SDK '$sdk_arg'\nInstalled SDKs: @sdks\n"; } # Set deployment target. # fixme can't enforce version when run_arg eq "default" # because we don't know it yet $deployment_arg = versionsuffix($sdk) if $deployment_arg eq "default"; if ($run_arg ne "default") { die "Deployment target '$deployment_arg' is newer than run target '$run_arg'\n" if $deployment_arg > $run_arg; } $C{DEPLOYMENT_TARGET} = $deployment_arg; $C{SDK_PATH} = getsdkpath($sdk); } else { # not $BUILD $C{DEPLOYMENT_TARGET} = "unknown_deployment_target"; $C{SDK_PATH} = "/unknown/sdk"; } # Set run target. $C{RUN_TARGET} = $run_arg; # Look up test library (possible in root or SDK_PATH) my $rootarg = $root; my $symroot; my @sympaths = ( (glob "$root/*~sym")[0], (glob "$root/BuildRecords/*_install/Symbols")[0], "$root/Symbols" ); my @dstpaths = ( (glob "$root/*~dst")[0], (glob "$root/BuildRecords/*_install/Root")[0], "$root/Root" ); for(my $i = 0; $i < scalar(@sympaths); $i++) { if (-e $sympaths[$i] && -e $dstpaths[$i]) { $symroot = $sympaths[$i]; $root = $dstpaths[$i]; last; } } if ($root ne "") { # Root specified. Require that it contain our dylibs. if (dirContainsAllTestLibs("$root$C{SDK_PATH}$TESTLIBDIR")) { $C{TESTLIBDIR} = "$root$C{SDK_PATH}$TESTLIBDIR"; } elsif (dirContainsAllTestLibs("$root$TESTLIBDIR")) { $C{TESTLIBDIR} = "$root$TESTLIBDIR"; } elsif (dirContainsAllTestLibs($root)) { $C{TESTLIBDIR} = "$root"; } else { die "Didn't find some libs in root '$rootarg' for sdk '$C{SDK_PATH}'\n"; } } else { # No root specified. Use the SDK or / for our dylibs. if (dirContainsAllTestLibs("$C{SDK_PATH}$TESTLIBDIR")) { $C{TESTLIBDIR} = "$C{SDK_PATH}$TESTLIBDIR"; } else { # We don't actually check in / because on devices # there are no dylib files there. $C{TESTLIBDIR} = $TESTLIBDIR; } } @{$C{TESTLIBS}} = map { "$C{TESTLIBDIR}/$_" } @TESTLIBNAMES; # convenience for tests that want libobjc.dylib's path $C{TESTLIB} = @{$C{TESTLIBS}}[0]; foreach my $testlibname (@TESTLIBNAMES) { if (-e "$symroot/$testlibname.dSYM") { push(@{$C{TESTDSYMS}}, "$symroot/$testlibname.dSYM"); } } if ($VERBOSE) { foreach my $testlib (@{$C{TESTLIBS}}) { my @uuids = `/usr/bin/dwarfdump -u '$testlib'`; while (my $uuid = shift @uuids) { print "note: $uuid"; } } } # Look up compilers my $cc = $C{CC}; my $cxx = cplusplus($C{CC}); my $swift = swift($C{CC}); if (! $BUILD) { $C{CC} = $cc; $C{CXX} = $cxx; $C{SWIFT} = $swift } else { $C{CC} = find_compiler($cc, $C{TOOLCHAIN}, $C{SDK_PATH}); $C{CXX} = find_compiler($cxx, $C{TOOLCHAIN}, $C{SDK_PATH}); $C{SWIFT} = find_compiler($swift, $C{TOOLCHAIN}, $C{SDK_PATH}); die "No C compiler '$cc' ('$C{CC}') in toolchain '$C{TOOLCHAIN}'\n" if !-e $C{CC}; die "No C++ compiler '$cxx' ('$C{CXX}') in toolchain '$C{TOOLCHAIN}'\n" if !-e $C{CXX}; die "No Swift compiler '$swift' ('$C{SWIFT}') in toolchain '$C{TOOLCHAIN}'\n" if !-e $C{SWIFT}; } if ($C{ARCH} eq "i386" && $C{OS} eq "macosx") { # libarclite no longer available on i386 # fixme need an archived copy for bincompat testing $C{FORCE_LOAD_ARCLITE} = ""; } elsif ($C{OS} eq "bridgeos") { # no libarclite on bridgeOS $C{FORCE_LOAD_ARCLITE} = ""; } else { $C{FORCE_LOAD_ARCLITE} = "-Xlinker -force_load -Xlinker " . dirname($C{CC}) . "/../lib/arc/libarclite_$C{OS}.a"; } # Populate cflags my $cflags = "-I$DIR -W -Wall -Wno-objc-weak-compat -Wno-arc-bridge-casts-disallowed-in-nonarc -Wshorten-64-to-32 -Qunused-arguments -fno-caret-diagnostics -Os -arch $C{ARCH} "; if (!$BATS) { # save-temps so dsymutil works so debug info works. # Disabled in BATS to save disk space. # rdar://45656803 -save-temps causes bad -Wstdlibcxx-not-found warnings $cflags .= "-g -save-temps -Wno-stdlibcxx-not-found"; } my $objcflags = ""; my $swiftflags = "-g "; $cflags .= " -isysroot '$C{SDK_PATH}'"; $cflags .= " '-Wl,-syslibroot,$C{SDK_PATH}'"; $swiftflags .= " -sdk '$C{SDK_PATH}'"; # Set deployment target cflags my $target = undef; die "No deployment target" if $C{DEPLOYMENT_TARGET} eq ""; if ($C{OS} eq "iphoneos") { $cflags .= " -mios-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-ios$C{DEPLOYMENT_TARGET}"; } elsif ($C{OS} eq "iphonesimulator") { $cflags .= " -mios-simulator-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-ios$C{DEPLOYMENT_TARGET}"; } elsif ($C{OS} eq "watchos") { $cflags .= " -mwatchos-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-watchos$C{DEPLOYMENT_TARGET}"; } elsif ($C{OS} eq "watchsimulator") { $cflags .= " -mwatchos-simulator-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-watchos$C{DEPLOYMENT_TARGET}"; } elsif ($C{OS} eq "appletvos") { $cflags .= " -mtvos-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-tvos$C{DEPLOYMENT_TARGET}"; } elsif ($C{OS} eq "appletvsimulator") { $cflags .= " -mtvos-simulator-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-tvos$C{DEPLOYMENT_TARGET}"; } elsif ($C{OS} eq "bridgeos") { $cflags .= " -mbridgeos-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-bridgeos$C{DEPLOYMENT_TARGET}"; } else { $cflags .= " -mmacosx-version-min=$C{DEPLOYMENT_TARGET}"; $target = "$C{ARCH}-apple-macosx$C{DEPLOYMENT_TARGET}"; } $swiftflags .= " -target $target"; $C{TESTINCLUDEDIR} = "$C{SDK_PATH}/usr/include"; $C{TESTLOCALINCLUDEDIR} = "$C{SDK_PATH}/usr/local/include"; if ($root ne "") { if ($C{SDK_PATH} ne "/") { $cflags .= " -isystem '$root$C{SDK_PATH}/usr/include'"; $cflags .= " -isystem '$root$C{SDK_PATH}/usr/local/include'"; } my $library_path = $C{TESTLIBDIR}; $cflags .= " -L$library_path"; # fixme Root vs SDKContentRoot $C{TESTINCLUDEDIR} = "$root/../SDKContentRoot/usr/include"; $C{TESTLOCALINCLUDEDIR} = "$root/../SDKContentRoot/usr/local/include"; $cflags .= " -isystem '$C{TESTINCLUDEDIR}'"; $cflags .= " -isystem '$C{TESTLOCALINCLUDEDIR}'"; } # Populate objcflags $objcflags .= " -lobjc"; if ($C{MEM} eq "arc") { $objcflags .= " -fobjc-arc"; } elsif ($C{MEM} eq "mrc") { # nothing } else { die "unrecognized MEM '$C{MEM}'\n"; } # Populate ENV_PREFIX $C{ENV} = "LANG=C MallocScribble=1"; $C{ENV} .= " VERBOSE=$VERBOSE" if $VERBOSE; if ($root ne "") { die "no spaces allowed in root" if $C{TESTLIBDIR} =~ /\s+/; } if ($C{GUARDMALLOC}) { $C{ENV} .= " GUARDMALLOC=1"; # checked by tests and errcheck.pl $C{ENV} .= " DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib"; if ($C{GUARDMALLOC} eq "before") { $C{ENV} .= " MALLOC_PROTECT_BEFORE=1"; } elsif ($C{GUARDMALLOC} eq "after") { # protect after is the default } else { die "Unknown guard malloc mode '$C{GUARDMALLOC}'\n"; } } # Populate compiler commands $C{XCRUN} = "env LANG=C /usr/bin/xcrun -toolchain '$C{TOOLCHAIN}'"; $C{COMPILE_C} = "$C{XCRUN} '$C{CC}' $cflags -x c -std=gnu99"; $C{COMPILE_CXX} = "$C{XCRUN} '$C{CXX}' $cflags -x c++"; $C{COMPILE_M} = "$C{XCRUN} '$C{CC}' $cflags $objcflags -x objective-c -std=gnu99"; $C{COMPILE_MM} = "$C{XCRUN} '$C{CXX}' $cflags $objcflags -x objective-c++"; $C{COMPILE_SWIFT} = "$C{XCRUN} '$C{SWIFT}' $swiftflags"; $C{COMPILE} = $C{COMPILE_C} if $C{LANGUAGE} eq "c"; $C{COMPILE} = $C{COMPILE_CXX} if $C{LANGUAGE} eq "c++"; $C{COMPILE} = $C{COMPILE_M} if $C{LANGUAGE} eq "objective-c"; $C{COMPILE} = $C{COMPILE_MM} if $C{LANGUAGE} eq "objective-c++"; $C{COMPILE} = $C{COMPILE_SWIFT} if $C{LANGUAGE} eq "swift"; die "unknown language '$C{LANGUAGE}'\n" if !defined $C{COMPILE}; ($C{COMPILE_NOMEM} = $C{COMPILE}) =~ s/ -fobjc-arc\S*//g; ($C{COMPILE_NOLINK} = $C{COMPILE}) =~ s/ '?-(?:Wl,|l)\S*//g; ($C{COMPILE_NOLINK_NOMEM} = $C{COMPILE_NOMEM}) =~ s/ '?-(?:Wl,|l)\S*//g; # Reject some self-inconsistent and disallowed configurations if ($C{MEM} !~ /^(mrc|arc)$/) { die "unknown MEM=$C{MEM} (expected one of mrc arc)\n"; } if ($C{MEM} eq "arc" && $C{CC} !~ /clang/) { print "note: skipping configuration $C{NAME}\n"; print "note: because CC=$C{CC} does not support MEM=$C{MEM}\n"; return 0; } if ($C{ARCH} eq "i386" && $C{OS} eq "macosx") { colorprint $yellow, "WARN: skipping configuration $C{NAME}\n"; colorprint $yellow, "WARN: because 32-bit Mac is dead\n"; return 0; } # fixme if ($C{LANGUAGE} eq "swift" && $C{ARCH} =~ /^arm/) { print "note: skipping configuration $C{NAME}\n"; print "note: because ARCH=$C{ARCH} does not support LANGUAGE=SWIFT\n"; return 0; } # fixme unimplemented run targets if ($C{RUN_TARGET} ne "default" && $C{OS} !~ /simulator/) { colorprint $yellow, "WARN: skipping configuration $C{NAME}"; colorprint $yellow, "WARN: because OS=$C{OS} does not yet implement RUN_TARGET=$C{RUN_TARGET}"; } %$configref = %C; } sub make_configs { my ($root, %args) = @_; my @results = ({}); # start with one empty config for my $key (keys %args) { my @newresults; my @values = @{$args{$key}}; for my $configref (@results) { my %config = %{$configref}; for my $value (@values) { my %newconfig = %config; $newconfig{$key} = $value; push @newresults, \%newconfig; } } @results = @newresults; } my @newresults; for my $configref(@results) { if (make_one_config($configref, $root)) { push @newresults, $configref; } } return @newresults; } sub config_name { my %config = @_; my $name = ""; for my $key (sort keys %config) { $name .= '~' if $name ne ""; $name .= "$key=$config{$key}"; } return $name; } sub rsync_ios { my ($src, $timeout) = @_; for (my $i = 0; $i < 10; $i++) { make("$DIR/timeout.pl $timeout rsync -e 'ssh -p $PORT' -av $src $HOST:/$REMOTEBASE/"); return if $? == 0; colorprint $yellow, "WARN: RETRY\n" if $VERBOSE; } die "Couldn't rsync tests to device. Check: device is connected; tcprelay is running; device trusts your Mac; device is unlocked; filesystem is mounted r/w\n"; } sub build_and_run_one_config { my %C = %{shift()}; my @tests = @_; # Build and run my $testcount = 0; my $failcount = 0; my $skipconfig = 0; my @gathertests; foreach my $test (@tests) { if ($VERBOSE) { print "\nGATHER $test\n"; } if ($ALL_TESTS{$test}) { gather_simple(\%C, $test) || next; # not pass, not fail push @gathertests, $test; } else { die "No test named '$test'\n"; } } my @builttests; if (!$BUILD) { @builttests = @gathertests; $testcount = scalar(@gathertests); } else { foreach my $test (@gathertests) { if ($VERBOSE) { print "\nBUILD $test\n"; } if ($ALL_TESTS{$test}) { $testcount++; if (!build_simple(\%C, $test)) { $failcount++; } else { push @builttests, $test; } } else { die "No test named '$test'\n"; } } } if (!$RUN || !scalar(@builttests)) { # nothing to do } else { if ($C{ARCH} =~ /^arm/ && `uname -p` !~ /^arm/) { # upload timeout - longer for slow watch devices my $timeout = ($C{OS} =~ /watch/) ? 120 : 20; # upload all tests to iOS device rsync_ios($C{DSTDIR}, $timeout); # upload library to iOS device if ($C{TESTLIBDIR} ne $TESTLIBDIR) { foreach my $thing (@{$C{TESTLIBS}}, @{$C{TESTDSYMS}}) { rsync_ios($thing, $timeout); } } } elsif ($C{OS} =~ /simulator/) { # run locally in a simulator } else { # run locally if ($BATS) { # BATS execution tries to run architectures that # aren't supported by the device. Skip those configs here. my $machine = `machine`; chomp $machine; # unsupported: # running arm64e on non-arm64e device # running arm64 on non-arm64* device # running armv7k on non-armv7k device # running arm64_32 on armv7k device # We don't need to handle all mismatches here, # only mismatches that arise within a single OS. $skipconfig = (($C{ARCH} eq "arm64e" && $machine ne "arm64e") || ($C{ARCH} eq "arm64" && $machine !~ /^arm64/) || ($C{ARCH} eq "armv7k" && $machine ne "armv7k") || ($C{ARCH} eq "arm64_32" && $machine eq "armv7k")); if ($skipconfig) { print "note: skipping configuration $C{NAME}\n"; print "note: because test arch $C{ARCH} is not " . "supported on device arch $machine\n"; $testcount = 0; } } } if (!$skipconfig) { foreach my $test (@builttests) { print "\nRUN $test\n" if ($VERBOSE); if ($ALL_TESTS{$test}) { if (!run_simple(\%C, $test)) { $failcount++; } } else { die "No test named '$test'\n"; } } } } return ($testcount, $failcount, $skipconfig); } # Return value if set by "$argname=value" on the command line # Return $default if not set. sub getargs { my ($argname, $default) = @_; foreach my $arg (@ARGV) { my ($value) = ($arg =~ /^$argname=(.+)$/); return [split ',', $value] if defined $value; } return [split ',', $default]; } # Return 1 or 0 if set by "$argname=1" or "$argname=0" on the # command line. Return $default if not set. sub getbools { my ($argname, $default) = @_; my @values = @{getargs($argname, $default)}; return [( map { ($_ eq "0") ? 0 : 1 } @values )]; } # Return an integer if set by "$argname=value" on the # command line. Return $default if not set. sub getints { my ($argname, $default) = @_; my @values = @{getargs($argname, $default)}; return [( map { int($_) } @values )]; } sub getarg { my ($argname, $default) = @_; my @values = @{getargs($argname, $default)}; die "Only one value allowed for $argname\n" if @values > 1; return $values[0]; } sub getbool { my ($argname, $default) = @_; my @values = @{getbools($argname, $default)}; die "Only one value allowed for $argname\n" if @values > 1; return $values[0]; } sub getint { my ($argname, $default) = @_; my @values = @{getints($argname, $default)}; die "Only one value allowed for $argname\n" if @values > 1; return $values[0]; } my $default_arch = "x86_64"; $args{ARCH} = getargs("ARCH", 0); $args{ARCH} = getargs("ARCHS", $default_arch) if !@{$args{ARCH}}[0]; $args{OSVERSION} = getargs("OS", "macosx-default-default"); $args{MEM} = getargs("MEM", "mrc,arc"); $args{LANGUAGE} = [ map { lc($_) } @{getargs("LANGUAGE", "c,objective-c,c++,objective-c++")} ]; $args{CC} = getargs("CC", "clang"); $HOST = getarg("HOST", "iphone"); $PORT = getarg("PORT", "10022"); { my $guardmalloc = getargs("GUARDMALLOC", 0); # GUARDMALLOC=1 is the same as GUARDMALLOC=before,after my @guardmalloc2 = (); for my $arg (@$guardmalloc) { if ($arg == 1) { push @guardmalloc2, "before"; push @guardmalloc2, "after"; } else { push @guardmalloc2, $arg } } $args{GUARDMALLOC} = \@guardmalloc2; } $BUILD = getbool("BUILD", 1); $RUN = getbool("RUN", 1); $VERBOSE = getint("VERBOSE", 0); $BATS = getbool("BATS", 0); $BUILDDIR = getarg("BUILDDIR", $BATS ? $BATSBASE : $LOCALBASE); my $root = getarg("ROOT", ""); $root =~ s#/*$##; my @tests = gettests(); if ($BUILD) { rm_rf_verbose "$DSTROOT$BUILDDIR"; rm_rf_verbose "$OBJROOT$BUILDDIR"; } print "note: -----\n"; print "note: testing root '$root'\n"; my @configs = make_configs($root, %args); print "note: -----\n"; print "note: testing ", scalar(@configs), " configurations:\n"; for my $configref (@configs) { my $configname = $$configref{NAME}; print "note: configuration $configname\n"; } my $failed = 0; my $testconfigs = @configs; my $failconfigs = 0; my $skipconfigs = 0; my $testcount = 0; my $failcount = 0; for my $configref (@configs) { my $configname = $$configref{NAME}; print "note: -----\n"; print "note: \nnote: $configname\nnote: \n"; (my $t, my $f, my $skipconfig) = eval { build_and_run_one_config($configref, @tests); }; $skipconfigs += $skipconfig; if ($@) { chomp $@; colorprint $red, "FAIL: $configname"; colorprint $red, "FAIL: $@"; $failconfigs++; } else { my $color = ($f ? $red : ""); print "note:\n"; colorprint $color, "note: $configname\n"; colorprint $color, "note: $t tests, $f failures"; $testcount += $t; $failcount += $f; $failconfigs++ if ($f); } } print "note: -----\n"; my $color = ($failconfigs ? $red : ""); colorprint $color, "note: $testconfigs configurations, " . "$failconfigs with failures, $skipconfigs skipped"; colorprint $color, "note: $testcount tests, $failcount failures"; $failed = ($failconfigs ? 1 : 0); if ($BUILD && $BATS && !$failed) { # Collect BATS execution instructions for all tests. # Each BATS "test" is all configurations together of one of our tests. for my $testname (@tests) { append_bats_test($testname); } # Write the BATS plist to disk. my $json = encode_json(\%bats_plist); my $filename = "$DSTROOT$BATSBASE/objc4.plist"; print "note: writing BATS config to $filename\n"; open(my $file, '>', $filename); print $file $json; close $file; } exit ($failed ? 1 : 0);