1#!/usr/bin/perl
2
3# test.pl
4# Run unit tests.
5
6use strict;
7use File::Basename;
8
9chdir dirname $0;
10chomp (my $DIR = `pwd`);
11
12# these names may be adjusted after reading the SDK setting
13my $TESTLIBNAME = "libsystem_blocks.dylib";
14my $TESTLIBPATH = "/usr/lib/system/$TESTLIBNAME";
15my $BUILDDIR = "/tmp/test-$TESTLIBNAME-build";
16
17# xterm colors
18my $red = "\e[41;37m";
19my $yellow = "\e[43;37m";
20my $def = "\e[0m";
21
22# clean, help
23if (scalar(@ARGV) == 1) {
24    my $arg = $ARGV[0];
25    if ($arg eq "clean") {
26        my $cmd = "rm -rf $BUILDDIR *~";
27        print "$cmd\n";
28        `$cmd`;
29        exit 0;
30    }
31    elsif ($arg eq "-h" || $arg eq "-H" || $arg eq "-help" || $arg eq "help") {
32        print(<<END);
33usage: $0 [options] [testname ...]
34       $0 clean
35       $0 help
36
37testname:
38    `testname` runs a specific test. If no testnames are given, runs all tests.
39
40options:
41    ARCH=<arch>
42    SDK=<sdk name>
43    ROOT=/path/to/project.roots/
44
45    CC=<compiler name>
46
47    MEM=mrc,arc,gc
48    STDLIB=libc++,libstdc++
49    GUARDMALLOC=0|1
50
51    BUILD=0|1
52    RUN=0|1
53    VERBOSE=0|1
54
55examples:
56
57    test installed library, x86_64, no gc
58    $0
59
60    test buildit-built root, i386 and x86_64, MRC and ARC and GC, clang compiler
61    $0 ARCH=i386,x86_64 ROOT=/tmp/libclosure.roots MEM=mrc,arc,gc CC=clang
62
63    test buildit-built root with iOS simulator
64    $0 ARCH=i386 ROOT=/tmp/libclosure.roots SDK=iphonesimulator
65
66    test buildit-built root on attached iOS device
67    $0 ARCH=armv7 ROOT=/tmp/libclosure.roots SDK=iphoneos
68END
69        exit 0;
70    }
71}
72
73#########################################################################
74## Tests
75
76my %ALL_TESTS;
77
78#########################################################################
79## Variables for use in complex build and run rules
80
81# variable         # example value
82
83# things you can multiplex on the command line
84# ARCH=i386,x86_64,armv6,armv7
85# SDK=system,macosx,iphoneos,iphonesimulator
86# LANGUAGE=c,c++,objective-c,objective-c++
87# CC=clang,gcc-4.2,llvm-gcc-4.2
88# MEM=mrc,arc,gc
89# STDLIB=libc++,libstdc++
90# GUARDMALLOC=0,1
91
92# things you can set once on the command line
93# ROOT=/path/to/project.roots
94# BUILD=0|1
95# RUN=0|1
96# VERBOSE=0|1
97
98
99
100my $BUILD;
101my $RUN;
102my $VERBOSE;
103
104my $crashcatch = <<'END';
105// interpose-able code to catch crashes, print, and exit cleanly
106#include <signal.h>
107#include <string.h>
108#include <unistd.h>
109
110// from dyld-interposing.h
111#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 };
112
113static void catchcrash(int sig)
114{
115    const char *msg;
116    switch (sig) {
117    case SIGILL:  msg = "CRASHED: SIGILL\\n";  break;
118    case SIGBUS:  msg = "CRASHED: SIGBUS\\n";  break;
119    case SIGSYS:  msg = "CRASHED: SIGSYS\\n";  break;
120    case SIGSEGV: msg = "CRASHED: SIGSEGV\\n"; break;
121    case SIGTRAP: msg = "CRASHED: SIGTRAP\\n"; break;
122    case SIGABRT: msg = "CRASHED: SIGABRT\\n"; break;
123    default: msg = "SIG\?\?\?\?\\n"; break;
124    }
125    write(STDERR_FILENO, msg, strlen(msg));
126    _exit(0);
127}
128
129static void setupcrash(void) __attribute__((constructor));
130static void setupcrash(void)
131{
132    signal(SIGILL, &catchcrash);
133    signal(SIGBUS, &catchcrash);
134    signal(SIGSYS, &catchcrash);
135    signal(SIGSEGV, &catchcrash);
136    signal(SIGTRAP, &catchcrash);
137    signal(SIGABRT, &catchcrash);
138}
139
140
141static int hacked = 0;
142ssize_t hacked_write(int fildes, const void *buf, size_t nbyte)
143{
144    if (!hacked) {
145        setupcrash();
146        hacked = 1;
147    }
148    return write(fildes, buf, nbyte);
149}
150
151DYLD_INTERPOSE(hacked_write, write);
152
153END
154
155
156#########################################################################
157## Harness
158
159
160# map language to buildable extensions for that language
161my %extensions_for_language = (
162    "c"     => ["c"],
163    "objective-c" => ["c", "m"],
164    "c++" => ["c", "cc", "cp", "cpp", "cxx", "c++"],
165    "objective-c++" => ["c", "m", "cc", "cp", "cpp", "cxx", "c++", "mm"],
166
167    "any" => ["c", "m", "cc", "cp", "cpp", "cxx", "c++", "mm"],
168    );
169
170# map extension to languages
171my %languages_for_extension = (
172    "c" => ["c", "objective-c", "c++", "objective-c++"],
173    "m" => ["objective-c", "objective-c++"],
174    "mm" => ["objective-c++"],
175    "cc" => ["c++", "objective-c++"],
176    "cp" => ["c++", "objective-c++"],
177    "cpp" => ["c++", "objective-c++"],
178    "cxx" => ["c++", "objective-c++"],
179    "c++" => ["c++", "objective-c++"],
180    );
181
182# Run some newline-separated commands like `make` would, stopping if any fail
183# run("cmd1 \n cmd2 \n cmd3")
184sub make {
185    my $output = "";
186    my @cmds = split("\n", $_[0]);
187    die if scalar(@cmds) == 0;
188    $? = 0;
189    foreach my $cmd (@cmds) {
190        chomp $cmd;
191        next if $cmd =~ /^\s*$/;
192        $cmd .= " 2>&1";
193        print "$cmd\n" if $VERBOSE;
194        $output .= `$cmd`;
195        last if $?;
196    }
197    print "$output\n" if $VERBOSE;
198    return $output;
199}
200
201sub chdir_verbose {
202    my $dir = shift;
203    chdir $dir || die;
204    print "cd $dir\n" if $VERBOSE;
205}
206
207
208# Return test names from the command line.
209# Returns all tests if no tests were named.
210sub gettests {
211    my @tests;
212
213    foreach my $arg (@ARGV) {
214        push @tests, $arg  if ($arg !~ /=/  &&  $arg !~ /^-/);
215    }
216
217    opendir(my $dir, $DIR) || die;
218    while (my $file = readdir($dir)) {
219        my ($name, $ext) = ($file =~ /^([^.]+)\.([^.]+)$/);
220        next if ! $languages_for_extension{$ext};
221
222        open(my $in, "< $file") || die "$file";
223        my $contents = join "", <$in>;
224        die if defined $ALL_TESTS{$name};
225        $ALL_TESTS{$name} = $ext  if ($contents =~ m#^[/*\s]*TEST_#m);
226        close($in);
227    }
228    closedir($dir);
229
230    if (scalar(@tests) == 0) {
231        @tests = keys %ALL_TESTS;
232    }
233
234    @tests = sort @tests;
235
236    return @tests;
237}
238
239
240# Turn a C compiler name into a C++ compiler name.
241sub cplusplus {
242    my ($c) = @_;
243    if ($c =~ /cc/) {
244        $c =~ s/cc/\+\+/;
245        return $c;
246    }
247    return $c . "++";                         # e.g. clang => clang++
248}
249
250# Returns an array of all sdks from `xcodebuild -showsdks`
251my @sdks_memo;
252sub getsdks {
253    if (!@sdks_memo) {
254        @sdks_memo = ("system", `xcodebuild -showsdks` =~ /-sdk (.+)$/mg);
255    }
256    return @sdks_memo;
257}
258
259# Returns whether the given sdk supports -lauto
260sub supportslibauto {
261    my ($sdk) = @_;
262    return 1 if $sdk eq "system";
263    return 1 if $sdk =~ /^macosx/;
264    return 0 if $sdk =~ /^iphone/;
265    die;
266}
267
268# print text with a colored prefix on each line
269sub colorprint {
270    my $color = shift;
271    while (my @lines = split("\n", shift)) {
272        for my $line (@lines) {
273            chomp $line;
274            print "$color $def$line\n";
275        }
276    }
277}
278
279sub rewind {
280    seek($_[0], 0, 0);
281}
282
283# parse name=value,value pairs
284sub readconditions {
285    my ($conditionstring) = @_;
286
287    my %results;
288    my @conditions = ($conditionstring =~ /\w+=(?:[^\s,]+,?)+/g);
289    for my $condition (@conditions) {
290        my ($name, $values) = ($condition =~ /(\w+)=(.+)/);
291        $results{$name} = [split ',', $values];
292    }
293
294    return %results;
295}
296
297# Get the name of the system SDK from sw_vers
298sub systemsdkname {
299    my @lines = `/usr/bin/sw_vers`;
300    my $name;
301    my $vers;
302    for my $line (@lines) {
303        ($name) = ($line =~ /^ProductName:\s+(.*)/)  if !$name;
304        ($vers) = ($line =~ /^ProductVersion:\s+(.*)/)  if !$vers;
305    }
306
307    $name =~ s/ //g;
308    $name = lc($name);
309    my $internal = "";
310    if (-d "/usr/local/include/objc") {
311        if ($name eq "macosx") {
312            $internal = "internal";
313        } else {
314            $internal = ".internal";
315        }
316    }
317    return $name . $vers . $internal;
318}
319
320sub check_output {
321    my %C = %{shift()};
322    my $name = shift;
323    my @output = @_;
324
325    my %T = %{$C{"TEST_$name"}};
326    my @original_output = @output;
327
328    # Run result-checking passes, reducing @output each time
329    my $xit = 1;
330    my $bad = "";
331    my $warn = "";
332    my $runerror = $T{TEST_RUN_OUTPUT};
333    filter_verbose(\@output);
334    $warn = filter_warn(\@output);
335    $bad |= filter_guardmalloc(\@output) if ($C{GUARDMALLOC});
336    $bad |= filter_valgrind(\@output) if ($C{VALGRIND});
337    $bad = filter_expected(\@output, \%C, $name) if ($bad eq "");
338    $bad = filter_bad(\@output)  if ($bad eq "");
339
340    # OK line should be the only one left
341    $bad = "(output not 'OK: $name')" if ($bad eq ""  &&  (scalar(@output) != 1  ||  $output[0] !~ /^OK: $name/));
342
343    if ($bad ne "") {
344        my $red = "\e[41;37m";
345        my $def = "\e[0m";
346        print "${red}FAIL: /// test '$name' \\\\\\$def\n";
347        colorprint($red, @original_output);
348        print "${red}FAIL: \\\\\\ test '$name' ///$def\n";
349        print "${red}FAIL: $name: $bad$def\n";
350        $xit = 0;
351    }
352    elsif ($warn ne "") {
353        my $yellow = "\e[43;37m";
354        my $def = "\e[0m";
355        print "${yellow}PASS: /// test '$name' \\\\\\$def\n";
356        colorprint($yellow, @original_output);
357        print "${yellow}PASS: \\\\\\ test '$name' ///$def\n";
358        print "PASS: $name (with warnings)\n";
359    }
360    else {
361        print "PASS: $name\n";
362    }
363    return $xit;
364}
365
366sub filter_expected
367{
368    my $outputref = shift;
369    my %C = %{shift()};
370    my $name = shift;
371
372    my %T = %{$C{"TEST_$name"}};
373    my $check = $T{TEST_RUN_OUTPUT}  ||  return "";
374
375    my $bad = "";
376
377    my $output = join("\n", @$outputref) . "\n";
378    if ($output !~ /$check/s) {
379	$bad = "(run output does not match TEST_RUN_OUTPUT)";
380	@$outputref = ("FAIL: $name");
381    } else {
382	@$outputref = ("OK: $name");  # pacify later filter
383    }
384
385    return $bad;
386}
387
388sub filter_bad
389{
390    my $outputref = shift;
391    my $bad = "";
392
393    my @new_output;
394    for my $line (@$outputref) {
395	if ($line =~ /^BAD: (.*)/) {
396	    $bad = "(failed)";
397	} else {
398	    push @new_output, $line;
399	}
400    }
401
402    @$outputref = @new_output;
403    return $bad;
404}
405
406sub filter_warn
407{
408    my $outputref = shift;
409    my $warn = "";
410
411    my @new_output;
412    for my $line (@$outputref) {
413	if ($line !~ /^WARN: (.*)/) {
414	    push @new_output, $line;
415        } else {
416	    $warn = "(warned)";
417	}
418    }
419
420    @$outputref = @new_output;
421    return $warn;
422}
423
424sub filter_verbose
425{
426    my $outputref = shift;
427
428    my @new_output;
429    for my $line (@$outputref) {
430	if ($line !~ /^VERBOSE: (.*)/) {
431	    push @new_output, $line;
432	}
433    }
434
435    @$outputref = @new_output;
436}
437
438sub filter_valgrind
439{
440    my $outputref = shift;
441    my $errors = 0;
442    my $leaks = 0;
443
444    my @new_output;
445    for my $line (@$outputref) {
446	if ($line =~ /^Approx: do_origins_Dirty\([RW]\): missed \d bytes$/) {
447	    # --track-origins warning (harmless)
448	    next;
449	}
450	if ($line =~ /^UNKNOWN __disable_threadsignal is unsupported. This warning will not be repeated.$/) {
451	    # signals unsupported (harmless)
452	    next;
453	}
454	if ($line =~ /^UNKNOWN __pthread_sigmask is unsupported. This warning will not be repeated.$/) {
455	    # signals unsupported (harmless)
456	    next;
457	}
458	if ($line !~ /^^\.*==\d+==/) {
459	    # not valgrind output
460	    push @new_output, $line;
461	    next;
462	}
463
464	my ($errcount) = ($line =~ /==\d+== ERROR SUMMARY: (\d+) errors/);
465	if (defined $errcount  &&  $errcount > 0) {
466	    $errors = 1;
467	}
468
469	(my $leakcount) = ($line =~ /==\d+==\s+(?:definitely|possibly) lost:\s+([0-9,]+)/);
470	if (defined $leakcount  &&  $leakcount > 0) {
471	    $leaks = 1;
472	}
473    }
474
475    @$outputref = @new_output;
476
477    my $bad = "";
478    $bad .= "(valgrind errors)" if ($errors);
479    $bad .= "(valgrind leaks)" if ($leaks);
480    return $bad;
481}
482
483sub filter_guardmalloc
484{
485    my $outputref = shift;
486    my $errors = 0;
487
488    my @new_output;
489    my $count = 0;
490    for my $line (@$outputref) {
491	if ($line !~ /^GuardMalloc\[[^\]]+\]: /) {
492	    # not guardmalloc output
493	    push @new_output, $line;
494	    next;
495	}
496
497        # Ignore 4 lines of guardmalloc prologue.
498        # Anything further is a guardmalloc error.
499        if (++$count > 4) {
500            $errors = 1;
501        }
502    }
503
504    @$outputref = @new_output;
505
506    my $bad = "";
507    $bad .= "(guardmalloc errors)" if ($errors);
508    return $bad;
509}
510
511sub gather_simple {
512    my $CREF = shift;
513    my %C = %{$CREF};
514    my $name = shift;
515    chdir_verbose $DIR;
516
517    my $ext = $ALL_TESTS{$name};
518    my $file = "$name.$ext";
519    return 0 if !$file;
520
521    # search file for 'TEST_CONFIG' or '#include "test.h"'
522    # also collect other values:
523    # TEST_CONFIG test conditions
524    # TEST_ENV environment prefix
525    # TEST_CFLAGS compile flags
526    # TEST_BUILD build instructions
527    # TEST_BUILD_OUTPUT expected build stdout/stderr
528    # TEST_RUN_OUTPUT expected run stdout/stderr
529    open(my $in, "< $file") || die;
530    my $contents = join "", <$in>;
531
532    my $test_h = ($contents =~ /^\s*#\s*(include|import)\s*"test\.h"/m);
533    my $disabled = ($contents =~ /\bTEST_DISABLED\b/m);
534    my $crashes = ($contents =~ /\bTEST_CRASHES\b/m);
535    my ($conditionstring) = ($contents =~ /\bTEST_CONFIG\b(.*)$/m);
536    my ($envstring) = ($contents =~ /\bTEST_ENV\b(.*)$/m);
537    my ($cflags) = ($contents =~ /\bTEST_CFLAGS\b(.*)$/m);
538    my ($buildcmd) = ($contents =~ /TEST_BUILD\n(.*?\n)END[ *\/]*\n/s);
539    my ($builderror) = ($contents =~ /TEST_BUILD_OUTPUT\n(.*?\n)END[ *\/]*\n/s);
540    my ($runerror) = ($contents =~ /TEST_RUN_OUTPUT\n(.*?\n)END[ *\/]*\n/s);
541
542    return 0 if !$test_h && !$disabled && !$crashes && !defined($conditionstring) && !defined($envstring) && !defined($cflags) && !defined($buildcmd) && !defined($builderror) && !defined($runerror);
543
544    if ($disabled) {
545        print "${yellow}SKIP: $name    (disabled by TEST_DISABLED)$def\n";
546        return 0;
547    }
548
549    # check test conditions
550
551    my $run = 1;
552    my %conditions = readconditions($conditionstring);
553    if (! $conditions{LANGUAGE}) {
554        # implicit language restriction from file extension
555        $conditions{LANGUAGE} = $languages_for_extension{$ext};
556    }
557    for my $condkey (keys %conditions) {
558        my @condvalues = @{$conditions{$condkey}};
559
560        # special case: RUN=0 does not affect build
561        if ($condkey eq "RUN"  &&  @condvalues == 1  &&  $condvalues[0] == 0) {
562            $run = 0;
563            next;
564        }
565
566        my $testvalue = $C{$condkey};
567        next if !defined($testvalue);
568        # testvalue is the configuration being run now
569        # condvalues are the allowed values for this test
570
571        # special case: look up the name of SDK "system"
572        if ($condkey eq "SDK"  &&  $testvalue eq "system") {
573            $testvalue = systemsdkname();
574        }
575
576        my $ok = 0;
577        for my $condvalue (@condvalues) {
578
579            # special case: objc and objc++
580            if ($condkey eq "LANGUAGE") {
581                $condvalue = "objective-c" if $condvalue eq "objc";
582                $condvalue = "objective-c++" if $condvalue eq "objc++";
583            }
584
585            $ok = 1  if ($testvalue eq $condvalue);
586
587            # special case: SDK allows prefixes, and "system" is "macosx"
588            if ($condkey eq "SDK") {
589                $ok = 1  if ($testvalue =~ /^$condvalue/);
590                $ok = 1  if ($testvalue eq "system"  &&  "macosx" =~ /^$condvalue/);
591            }
592
593            # special case: CC and CXX allow substring matches
594            if ($condkey eq "CC"  ||  $condkey eq "CXX") {
595                $ok = 1  if ($testvalue =~ /$condvalue/);
596            }
597
598            last if $ok;
599        }
600
601        if (!$ok) {
602            my $plural = (@condvalues > 1) ? "one of: " : "";
603            print "SKIP: $name    ($condkey=$testvalue, but test requires $plural", join(' ', @condvalues), ")\n";
604            return 0;
605        }
606    }
607
608    # builderror is multiple REs separated by OR
609    if (defined $builderror) {
610        $builderror =~ s/\nOR\n/\n|/sg;
611        $builderror = "^(" . $builderror . ")\$";
612    }
613    # runerror is multiple REs separated by OR
614    if (defined $runerror) {
615        $runerror =~ s/\nOR\n/\n|/sg;
616        $runerror = "^(" . $runerror . ")\$";
617    }
618
619    # save some results for build and run phases
620    $$CREF{"TEST_$name"} = {
621        TEST_BUILD => $buildcmd,
622        TEST_BUILD_OUTPUT => $builderror,
623        TEST_CRASHES => $crashes,
624        TEST_RUN_OUTPUT => $runerror,
625        TEST_CFLAGS => $cflags,
626        TEST_ENV => $envstring,
627        TEST_RUN => $run,
628    };
629
630    return 1;
631}
632
633# Builds a simple test
634sub build_simple {
635    my %C = %{shift()};
636    my $name = shift;
637    my %T = %{$C{"TEST_$name"}};
638    chdir_verbose "$C{DIR}/$name.build";
639
640    my $ext = $ALL_TESTS{$name};
641    my $file = "$DIR/$name.$ext";
642
643    if ($T{TEST_CRASHES}) {
644        `echo '$crashcatch' > crashcatch.c`;
645        make("$C{COMPILE_C} -dynamiclib -o libcrashcatch.dylib -x c crashcatch.c");
646        die "$?" if $?;
647    }
648
649    my $cmd = $T{TEST_BUILD} ? eval "return \"$T{TEST_BUILD}\"" : "$C{COMPILE}   $T{TEST_CFLAGS} $file -o $name.out";
650
651    my $output = make($cmd);
652
653    my $ok;
654    if (my $builderror = $T{TEST_BUILD_OUTPUT}) {
655        # check for expected output and ignore $?
656        if ($output =~ /$builderror/s) {
657            $ok = 1;
658        } else {
659            print "${red}FAIL: /// test '$name' \\\\\\$def\n";
660            colorprint $red, $output;
661            print "${red}FAIL: \\\\\\ test '$name' ///$def\n";
662            print "${red}FAIL: $name (build output does not match TEST_BUILD_OUTPUT)$def\n";
663            $ok = 0;
664        }
665    } elsif ($?) {
666        print "${red}FAIL: /// test '$name' \\\\\\$def\n";
667        colorprint $red, $output;
668        print "${red}FAIL: \\\\\\ test '$name' ///$def\n";
669        print "${red}FAIL: $name (build failed)$def\n";
670        $ok = 0;
671    } elsif ($output ne "") {
672        print "${red}FAIL: /// test '$name' \\\\\\$def\n";
673        colorprint $red, $output;
674        print "${red}FAIL: \\\\\\ test '$name' ///$def\n";
675        print "${red}FAIL: $name (unexpected build output)$def\n";
676        $ok = 0;
677    } else {
678        $ok = 1;
679    }
680
681
682    if ($ok) {
683        foreach my $file (glob("*.out *.dylib *.bundle")) {
684            make("dsymutil $file");
685        }
686    }
687
688    return $ok;
689}
690
691# Run a simple test (testname.out, with error checking of stdout and stderr)
692sub run_simple {
693    my %C = %{shift()};
694    my $name = shift;
695    my %T = %{$C{"TEST_$name"}};
696
697    if (! $T{TEST_RUN}) {
698        print "PASS: $name (build only)\n";
699        return 1;
700    }
701    else {
702        chdir_verbose "$C{DIR}/$name.build";
703    }
704
705    my $env = "$C{ENV} $T{TEST_ENV}";
706    if ($T{TEST_CRASHES}) {
707        $env .= " DYLD_INSERT_LIBRARIES=libcrashcatch.dylib";
708    }
709
710    my $output;
711
712    if ($C{ARCH} =~ /^arm/ && `unamep -p` !~ /^arm/) {
713        # run on iOS device
714
715        my $remotedir = "/var/root/test/" . basename($C{DIR}) . "/$name.build";
716        my $remotedyld = " DYLD_LIBRARY_PATH=$remotedir";
717        $remotedyld .= ":/var/root/test/"  if ($C{TESTLIB} ne $TESTLIBPATH);
718
719        # elide host-specific paths
720        $env =~ s/DYLD_LIBRARY_PATH=\S+//;
721        $env =~ s/DYLD_ROOT_PATH=\S+//;
722
723        my $cmd = "ssh iphone 'cd $remotedir && env $env $remotedyld ./$name.out'";
724        $output = make("$cmd");
725    }
726    else {
727        # run locally
728
729        my $cmd = "env $env ./$name.out";
730        $output = make("sh -c '$cmd 2>&1' 2>&1");
731        # need extra sh level to capture "sh: Illegal instruction" after crash
732        # fixme fail if $? except tests that expect to crash
733    }
734
735    return check_output(\%C, $name, split("\n", $output));
736}
737
738
739my %compiler_memo;
740sub find_compiler {
741    my ($cc, $sdk, $sdk_path) = @_;
742
743    # memoize
744    my $key = $cc . ':' . $sdk;
745    my $result = $compiler_memo{$key};
746    return $result if defined $result;
747
748    if (-e $cc) {
749        $result = $cc;
750    } elsif (-e "$sdk_path/$cc") {
751        $result = "$sdk_path/$cc";
752    } elsif ($sdk eq "system"  &&  -e "/usr/bin/$cc") {
753        $result = "/usr/bin/$cc";
754    } elsif ($sdk eq "system") {
755        $result  = `xcrun -find $cc 2>/dev/null`;
756    } else {
757        $result  = `xcrun -sdk $sdk -find $cc 2>/dev/null`;
758    }
759
760    chomp $result;
761    $compiler_memo{$key} = $result;
762    return $result;
763}
764
765sub make_one_config {
766    my $configref = shift;
767    my $root = shift;
768    my %C = %{$configref};
769
770    # Aliases
771    $C{LANGUAGE} = "objective-c"  if $C{LANGUAGE} eq "objc";
772    $C{LANGUAGE} = "objective-c++"  if $C{LANGUAGE} eq "objc++";
773
774    # Look up SDK
775    # Try exact match first.
776    # Then try lexically-last prefix match (so "macosx" => "macosx10.7internal").
777    my @sdks = getsdks();
778    if ($VERBOSE) {
779        print "Installed SDKs: @sdks\n";
780    }
781    my $exactsdk = undef;
782    my $prefixsdk = undef;
783    foreach my $sdk (@sdks) {
784        my $SDK = $C{SDK};
785        $exactsdk = $sdk  if ($sdk eq $SDK);
786        # check for digits to prevent e.g. "iphone" => "iphonesimulator4.2"
787        $prefixsdk = $sdk  if ($sdk =~ /^$SDK[0-9]/  &&  $sdk gt $prefixsdk);
788    }
789    if ($exactsdk) {
790        $C{SDK} = $exactsdk;
791    } elsif ($prefixsdk) {
792        $C{SDK} = $prefixsdk;
793    } else {
794        die "unknown SDK '$C{SDK}'\nInstalled SDKs: @sdks\n";
795    }
796
797    # set the config name now, after massaging the language and sdk,
798    # but before adding other settings
799    my $configname = config_name(%C);
800    die if ($configname =~ /'/);
801    die if ($configname =~ / /);
802    ($C{NAME} = $configname) =~ s/~/ /g;
803    (my $configdir = $configname) =~ s#/##g;
804    $C{DIR} = "$BUILDDIR/$configdir";
805
806    $C{SDK_PATH} = "/";
807    if ($C{SDK} ne "system") {
808        ($C{SDK_PATH}) = (`xcodebuild -version -sdk $C{SDK} Path` =~ /^\s*(.+?)\s*$/);
809    }
810
811    # Look up test library (possible in root or SDK_PATH)
812
813    if (-e (glob "$root/*~dst")[0]) {
814        $root = (glob "$root/*~dst")[0];
815    }
816
817    if ($root ne ""  &&  -e "$root$C{SDK_PATH}$TESTLIBPATH") {
818        $C{TESTLIB} = "$root$C{SDK_PATH}$TESTLIBPATH";
819    } elsif (-e "$root$TESTLIBPATH") {
820        $C{TESTLIB} = "$root$TESTLIBPATH";
821    } elsif (-e "$root/$TESTLIBNAME") {
822        $C{TESTLIB} = "$root/$TESTLIBNAME";
823    } else {
824        die "No $TESTLIBNAME in root '$root' and sdk '$C{SDK_PATH}'\n";
825    }
826
827    if ($VERBOSE) {
828        my @uuids = `/usr/bin/dwarfdump -u '$C{TESTLIB}'`;
829        while (my $uuid = shift @uuids) {
830            print "note: $uuid";
831        }
832    }
833
834    # Look up compilers
835    my $cc = $C{CC};
836    my $cxx = cplusplus($C{CC});
837    if (! $BUILD) {
838        $C{CC} = $cc;
839        $C{CXX} = $cxx;
840    } else {
841        $C{CC} = find_compiler($cc, $C{SDK}, $C{SDK_PATH});
842        $C{CXX} = find_compiler($cxx, $C{SDK}, $C{SDK_PATH});
843
844        die "No compiler '$cc' ('$C{CC}') in SDK '$C{SDK}'\n" if !-e $C{CC};
845        die "No compiler '$cxx' ('$C{CXX}') in SDK '$C{SDK}'\n" if !-e $C{CXX};
846    }
847
848    # Populate cflags
849
850    # save-temps so dsymutil works so debug info works
851    # fixme not save-temps because rdar://10062179
852    my $cflags = "-I$DIR -W -Wall -Wno-deprecated-declarations -Wshorten-64-to-32 -g -Os -arch $C{ARCH} ";
853    my $objcflags = "";
854
855    if ($C{SDK} ne "system") {
856        $cflags .= " -isysroot '$C{SDK_PATH}'";
857        $cflags .= " '-Wl,-syslibroot,$C{SDK_PATH}'";
858    }
859
860    if ($C{SDK} =~ /^iphoneos[0-9]/  &&  $cflags !~ /-miphoneos-version-min/) {
861        my ($vers) = ($C{SDK} =~ /^iphoneos([0-9]+\.[0-9+])/);
862        $cflags .= " -miphoneos-version-min=$vers";
863    }
864    if ($C{SDK} =~ /^iphonesimulator[0-9]/  &&  $cflags !~ /-D__IPHONE_OS_VERSION_MIN_REQUIRED/) {
865        my ($vers) = ($C{SDK} =~ /^iphonesimulator([0-9]+\.[0-9+])/);
866        $vers = int($vers * 10000);  # 4.2 => 42000
867        $cflags .= " -D__IPHONE_OS_VERSION_MIN_REQUIRED=$vers";
868    }
869    if ($C{SDK} =~ /^iphonesimulator/) {
870        $objcflags .= " -fobjc-abi-version=2 -fobjc-legacy-dispatch";
871    }
872
873    if ($root ne "") {
874        my $library_path = dirname($C{TESTLIB});
875        $cflags .= " -L$library_path";
876        $cflags .= " -isystem '$root/usr/include'";
877        $cflags .= " -isystem '$root/usr/local/include'";
878
879        if ($C{SDK_PATH} ne "/") {
880            $cflags .= " -isystem '$root$C{SDK_PATH}/usr/include'";
881            $cflags .= " -isystem '$root$C{SDK_PATH}/usr/local/include'";
882        }
883    }
884
885    if ($C{CC} =~ /clang/) {
886        $cflags .= " -Qunused-arguments -fno-caret-diagnostics";
887        $cflags .= " -stdlib=$C{STDLIB}";
888    }
889
890
891    # Populate objcflags
892
893    $objcflags .= " -lobjc";
894    if ($C{MEM} eq "gc") {
895        $objcflags .= " -fobjc-gc";
896    }
897    elsif ($C{MEM} eq "arc") {
898        $objcflags .= " -fobjc-arc";
899    }
900    elsif ($C{MEM} eq "mrc") {
901        # nothing
902    }
903    else {
904        die "unrecognized MEM '$C{MEM}'\n";
905    }
906
907    if (supportslibauto($C{SDK})) {
908        # do this even for non-GC tests
909        $objcflags .= " -lauto";
910    }
911
912    # Populate ENV_PREFIX
913    $C{ENV} = "LANG=C";
914    $C{ENV} .= " VERBOSE=1"  if $VERBOSE;
915    if ($root ne "") {
916        my $library_path = dirname($C{TESTLIB});
917        die "no spaces allowed in root" if $library_path =~ /\s+/;
918        $C{ENV} .= " DYLD_LIBRARY_PATH=$library_path"  if ($library_path ne "/usr/lib");
919    }
920    if ($C{SDK_PATH} ne "/") {
921        die "no spaces allowed in sdk" if $C{SDK_PATH} =~ /\s+/;
922        $C{ENV} .= " DYLD_ROOT_PATH=$C{SDK_PATH}";
923    }
924    if ($C{GUARDMALLOC}) {
925        $ENV{GUARDMALLOC} = "1";  # checked by tests and errcheck.pl
926        $C{ENV} .= " DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib";
927    }
928    if ($C{SDK} =~ /^iphonesimulator[0-9]/) {
929        my ($vers) = ($C{SDK} =~ /^iphonesimulator([0-9]+\.[0-9+])/);
930        $C{ENV} .=
931            " CFFIXED_USER_HOME=$ENV{HOME}/Library/Application\\ Support/iPhone\\ Simulator/$vers" .
932            " IPHONE_SIMULATOR_ROOT=$C{SDK_PATH}" .
933            " IPHONE_SHARED_RESOURCES_DIRECTORY=$ENV{HOME}/Library/Application\\ Support/iPhone\\ Simulator/$vers";
934    }
935
936    # Populate compiler commands
937    $C{COMPILE_C}   = "env LANG=C '$C{CC}'  $cflags -x c -std=gnu99";
938    $C{COMPILE_CXX} = "env LANG=C '$C{CXX}' $cflags -x c++";
939    $C{COMPILE_M}   = "env LANG=C '$C{CC}'  $cflags $objcflags -x objective-c -std=gnu99";
940    $C{COMPILE_MM}  = "env LANG=C '$C{CXX}' $cflags $objcflags -x objective-c++";
941
942    $C{COMPILE} = $C{COMPILE_C}    if $C{LANGUAGE} eq "c";
943    $C{COMPILE} = $C{COMPILE_CXX}  if $C{LANGUAGE} eq "c++";
944    $C{COMPILE} = $C{COMPILE_M}    if $C{LANGUAGE} eq "objective-c";
945    $C{COMPILE} = $C{COMPILE_MM}   if $C{LANGUAGE} eq "objective-c++";
946    die "unknown language '$C{LANGUAGE}'\n" if !defined $C{COMPILE};
947
948    ($C{COMPILE_NOMEM} = $C{COMPILE}) =~ s/ -fobjc-(?:gc|arc)\S*//g;
949    ($C{COMPILE_NOLINK} = $C{COMPILE}) =~ s/ '?-(?:Wl,|l)\S*//g;
950    ($C{COMPILE_NOLINK_NOMEM} = $C{COMPILE_NOMEM}) =~ s/ '?-(?:Wl,|l)\S*//g;
951
952
953    # Reject some self-inconsistent configurations
954    if ($C{MEM} !~ /^(mrc|arc|gc)$/) {
955        die "unknown MEM=$C{MEM} (expected one of mrc arc gc)\n";
956    }
957
958    if ($C{MEM} eq "gc"  &&  $C{SDK} =~ /^iphone/) {
959        print "note: skipping configuration $C{NAME}\n";
960        print "note:   because SDK=$C{SDK} does not support MEM=$C{MEM}\n";
961        return 0;
962    }
963    if ($C{MEM} eq "arc"  &&  $C{SDK} !~ /^iphone/  &&  $C{ARCH} eq "i386") {
964        print "note: skipping configuration $C{NAME}\n";
965        print "note:   because 32-bit Mac does not support MEM=$C{MEM}\n";
966        return 0;
967    }
968    if ($C{MEM} eq "arc"  &&  $C{CC} !~ /clang/) {
969        print "note: skipping configuration $C{NAME}\n";
970        print "note:   because CC=$C{CC} does not support MEM=$C{MEM}\n";
971        return 0;
972    }
973
974    if ($C{STDLIB} ne "libstdc++"  &&  $C{CC} !~ /clang/) {
975        print "note: skipping configuration $C{NAME}\n";
976        print "note:   because CC=$C{CC} does not support STDLIB=$C{STDLIB}\n";
977        return 0;
978    }
979
980    %$configref = %C;
981}
982
983sub make_configs {
984    my ($root, %args) = @_;
985
986    my @results = ({});  # start with one empty config
987
988    for my $key (keys %args) {
989        my @newresults;
990        my @values = @{$args{$key}};
991        for my $configref (@results) {
992            my %config = %{$configref};
993            for my $value (@values) {
994                my %newconfig = %config;
995                $newconfig{$key} = $value;
996                push @newresults, \%newconfig;
997            }
998        }
999        @results = @newresults;
1000    }
1001
1002    my @newresults;
1003    for my $configref(@results) {
1004        if (make_one_config($configref, $root)) {
1005            push @newresults, $configref;
1006        }
1007    }
1008
1009    return @newresults;
1010}
1011
1012sub config_name {
1013    my %config = @_;
1014    my $name = "";
1015    for my $key (sort keys %config) {
1016        $name .= '~'  if $name ne "";
1017        $name .= "$key=$config{$key}";
1018    }
1019    return $name;
1020}
1021
1022sub run_one_config {
1023    my %C = %{shift()};
1024    my @tests = @_;
1025
1026    # Build and run
1027    my $testcount = 0;
1028    my $failcount = 0;
1029
1030    my @gathertests;
1031    foreach my $test (@tests) {
1032        if ($VERBOSE) {
1033            print "\nGATHER $test\n";
1034        }
1035
1036        if ($ALL_TESTS{$test}) {
1037            gather_simple(\%C, $test) || next;  # not pass, not fail
1038            push @gathertests, $test;
1039        } else {
1040            die "No test named '$test'\n";
1041        }
1042    }
1043
1044    my @builttests;
1045    if (!$BUILD) {
1046        @builttests = @gathertests;
1047        $testcount = scalar(@gathertests);
1048    } else {
1049        my $configdir = $C{DIR};
1050        print $configdir, "\n"  if $VERBOSE;
1051        mkdir $configdir  || die;
1052
1053        foreach my $test (@gathertests) {
1054            if ($VERBOSE) {
1055                print "\nBUILD $test\n";
1056            }
1057            mkdir "$configdir/$test.build"  || die;
1058
1059            if ($ALL_TESTS{$test}) {
1060                $testcount++;
1061                if (!build_simple(\%C, $test)) {
1062                    $failcount++;
1063                } else {
1064                    push @builttests, $test;
1065                }
1066            } else {
1067                die "No test named '$test'\n";
1068            }
1069        }
1070    }
1071
1072    if (!$RUN  ||  !scalar(@builttests)) {
1073        # nothing to do
1074    }
1075    else {
1076        if ($C{ARCH} =~ /^arm/ && `unamep -p` !~ /^arm/) {
1077            # upload all tests to iOS device
1078            make("RSYNC_PASSWORD=alpine rsync -av $C{DIR} rsync://root\@localhost:10873/root/var/root/test/");
1079            die "Couldn't rsync tests to device\n" if ($?);
1080
1081            # upload library to iOS device
1082            if ($C{TESTLIB} ne $TESTLIBPATH) {
1083                # hack - send thin library because device may use lib=armv7
1084                # even though app=armv6, and we want to set the lib's arch
1085                make("lipo -output /tmp/$TESTLIBNAME -thin $C{ARCH} $C{TESTLIB}  ||  cp $C{TESTLIB} /tmp/$TESTLIBNAME");
1086                die "Couldn't thin $C{TESTLIB} to $C{ARCH}\n" if ($?);
1087                make("RSYNC_PASSWORD=alpine rsync -av /tmp/$TESTLIBNAME rsync://root\@localhost:10873/root/var/root/test/");
1088                die "Couldn't rsync $C{TESTLIB} to device\n" if ($?);
1089            }
1090        }
1091
1092        foreach my $test (@builttests) {
1093            print "\nRUN $test\n"  if ($VERBOSE);
1094
1095            if ($ALL_TESTS{$test})
1096            {
1097                if (!run_simple(\%C, $test)) {
1098                    $failcount++;
1099                }
1100            } else {
1101                die "No test named '$test'\n";
1102            }
1103        }
1104    }
1105
1106    return ($testcount, $failcount);
1107}
1108
1109
1110
1111# Return value if set by "$argname=value" on the command line
1112# Return $default if not set.
1113sub getargs {
1114    my ($argname, $default) = @_;
1115
1116    foreach my $arg (@ARGV) {
1117        my ($value) = ($arg =~ /^$argname=(.+)$/);
1118        return [split ',', $value] if defined $value;
1119    }
1120
1121    return [$default];
1122}
1123
1124# Return 1 or 0 if set by "$argname=1" or "$argname=0" on the
1125# command line. Return $default if not set.
1126sub getbools {
1127    my ($argname, $default) = @_;
1128
1129    my @values = @{getargs($argname, $default)};
1130    return [( map { ($_ eq "0") ? 0 : 1 } @values )];
1131}
1132
1133sub getarg {
1134    my ($argname, $default) = @_;
1135    my @values = @{getargs($argname, $default)};
1136    die "Only one value allowed for $argname\n"  if @values > 1;
1137    return $values[0];
1138}
1139
1140sub getbool {
1141    my ($argname, $default) = @_;
1142    my @values = @{getbools($argname, $default)};
1143    die "Only one value allowed for $argname\n"  if @values > 1;
1144    return $values[0];
1145}
1146
1147
1148# main
1149my %args;
1150
1151
1152my $default_arch = (`/usr/sbin/sysctl hw.optional.x86_64` eq "hw.optional.x86_64: 1\n") ? "x86_64" : "i386";
1153$args{ARCH} = getargs("ARCH", 0);
1154$args{ARCH} = getargs("ARCHS", $default_arch)  if !@{$args{ARCH}}[0];
1155
1156$args{SDK} = getargs("SDK", "system");
1157
1158$args{MEM} = getargs("MEM", "mrc");
1159$args{LANGUAGE} = [ map { lc($_) } @{getargs("LANGUAGE", "objective-c")} ];
1160$args{STDLIB} = getargs("STDLIB", "libstdc++");
1161
1162$args{CC} = getargs("CC", "clang");
1163
1164$args{GUARDMALLOC} = getbools("GUARDMALLOC", 0);
1165
1166$BUILD = getbool("BUILD", 1);
1167$RUN = getbool("RUN", 1);
1168$VERBOSE = getbool("VERBOSE", 0);
1169
1170my $root = getarg("ROOT", "");
1171$root =~ s#/*$##;
1172
1173my @tests = gettests();
1174
1175print "note: -----\n";
1176print "note: testing root '$root'\n";
1177
1178my @configs = make_configs($root, %args);
1179
1180print "note: -----\n";
1181print "note: testing ", scalar(@configs), " configurations:\n";
1182for my $configref (@configs) {
1183    my $configname = $$configref{NAME};
1184    print "note: configuration $configname\n";
1185}
1186
1187if ($BUILD) {
1188    `rm -rf '$BUILDDIR'`;
1189    mkdir "$BUILDDIR" || die;
1190}
1191
1192my $failed = 0;
1193
1194my $testconfigs = @configs;
1195my $failconfigs = 0;
1196my $testcount = 0;
1197my $failcount = 0;
1198for my $configref (@configs) {
1199    my $configname = $$configref{NAME};
1200    print "note: -----\n";
1201    print "note: \nnote: $configname\nnote: \n";
1202
1203    (my $t, my $f) = eval { run_one_config($configref, @tests); };
1204    if ($@) {
1205        chomp $@;
1206        print "${red}FAIL: $configname${def}\n";
1207        print "${red}FAIL: $@${def}\n";
1208        $failconfigs++;
1209    } else {
1210        my $color = ($f ? $red : "");
1211        print "note:\n";
1212        print "${color}note: $configname$def\n";
1213        print "${color}note: $t tests, $f failures$def\n";
1214        $testcount += $t;
1215        $failcount += $f;
1216        $failconfigs++ if ($f);
1217    }
1218}
1219
1220print "note: -----\n";
1221my $color = ($failconfigs ? $red : "");
1222print "${color}note: $testconfigs configurations, $failconfigs with failures$def\n";
1223print "${color}note: $testcount tests, $failcount failures$def\n";
1224
1225$failed = ($failconfigs ? 1 : 0);
1226
1227exit ($failed ? 1 : 0);
1228