Home | History | Annotate | Download | only in testing
      1 //
      2 //  GTMSenTestCase.m
      3 //
      4 //  Copyright 2007-2008 Google Inc.
      5 //
      6 //  Licensed under the Apache License, Version 2.0 (the "License"); you may not
      7 //  use this file except in compliance with the License.  You may obtain a copy
      8 //  of the License at
      9 //
     10 //  http://www.apache.org/licenses/LICENSE-2.0
     11 //
     12 //  Unless required by applicable law or agreed to in writing, software
     13 //  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     14 //  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
     15 //  License for the specific language governing permissions and limitations under
     16 //  the License.
     17 //
     18 
     19 #import "GTMSenTestCase.h"
     20 
     21 #import <unistd.h>
     22 #if GTM_IPHONE_SIMULATOR
     23 #import <objc/message.h>
     24 #endif
     25 
     26 #import "GTMObjC2Runtime.h"
     27 #import "GTMUnitTestDevLog.h"
     28 
     29 #if !GTM_IPHONE_SDK
     30 #import "GTMGarbageCollection.h"
     31 #endif  // !GTM_IPHONE_SDK
     32 
     33 #if GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
     34 #import <stdarg.h>
     35 
     36 @interface NSException (GTMSenTestPrivateAdditions)
     37 + (NSException *)failureInFile:(NSString *)filename
     38                         atLine:(int)lineNumber
     39                         reason:(NSString *)reason;
     40 @end
     41 
     42 @implementation NSException (GTMSenTestPrivateAdditions)
     43 + (NSException *)failureInFile:(NSString *)filename
     44                         atLine:(int)lineNumber
     45                         reason:(NSString *)reason {
     46   NSDictionary *userInfo =
     47     [NSDictionary dictionaryWithObjectsAndKeys:
     48      [NSNumber numberWithInteger:lineNumber], SenTestLineNumberKey,
     49      filename, SenTestFilenameKey,
     50      nil];
     51 
     52   return [self exceptionWithName:SenTestFailureException
     53                           reason:reason
     54                         userInfo:userInfo];
     55 }
     56 @end
     57 
     58 @implementation NSException (GTMSenTestAdditions)
     59 
     60 + (NSException *)failureInFile:(NSString *)filename
     61                         atLine:(int)lineNumber
     62                withDescription:(NSString *)formatString, ... {
     63 
     64   NSString *testDescription = @"";
     65   if (formatString) {
     66     va_list vl;
     67     va_start(vl, formatString);
     68     testDescription =
     69       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
     70     va_end(vl);
     71   }
     72 
     73   NSString *reason = testDescription;
     74 
     75   return [self failureInFile:filename atLine:lineNumber reason:reason];
     76 }
     77 
     78 + (NSException *)failureInCondition:(NSString *)condition
     79                              isTrue:(BOOL)isTrue
     80                              inFile:(NSString *)filename
     81                              atLine:(int)lineNumber
     82                     withDescription:(NSString *)formatString, ... {
     83 
     84   NSString *testDescription = @"";
     85   if (formatString) {
     86     va_list vl;
     87     va_start(vl, formatString);
     88     testDescription =
     89       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
     90     va_end(vl);
     91   }
     92 
     93   NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@",
     94                       condition, isTrue ? "false" : "true", testDescription];
     95 
     96   return [self failureInFile:filename atLine:lineNumber reason:reason];
     97 }
     98 
     99 + (NSException *)failureInEqualityBetweenObject:(id)left
    100                                       andObject:(id)right
    101                                          inFile:(NSString *)filename
    102                                          atLine:(int)lineNumber
    103                                 withDescription:(NSString *)formatString, ... {
    104 
    105   NSString *testDescription = @"";
    106   if (formatString) {
    107     va_list vl;
    108     va_start(vl, formatString);
    109     testDescription =
    110       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
    111     va_end(vl);
    112   }
    113 
    114   NSString *reason =
    115     [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
    116      [left description], [right description], testDescription];
    117 
    118   return [self failureInFile:filename atLine:lineNumber reason:reason];
    119 }
    120 
    121 + (NSException *)failureInEqualityBetweenValue:(NSValue *)left
    122                                       andValue:(NSValue *)right
    123                                   withAccuracy:(NSValue *)accuracy
    124                                         inFile:(NSString *)filename
    125                                         atLine:(int)lineNumber
    126                                withDescription:(NSString *)formatString, ... {
    127 
    128   NSString *testDescription = @"";
    129   if (formatString) {
    130     va_list vl;
    131     va_start(vl, formatString);
    132     testDescription =
    133       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
    134     va_end(vl);
    135   }
    136 
    137   NSString *reason;
    138   if (accuracy) {
    139     reason =
    140       [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
    141        left, right, testDescription];
    142   } else {
    143     reason =
    144       [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@",
    145        left, right, accuracy, testDescription];
    146   }
    147 
    148   return [self failureInFile:filename atLine:lineNumber reason:reason];
    149 }
    150 
    151 + (NSException *)failureInRaise:(NSString *)expression
    152                          inFile:(NSString *)filename
    153                          atLine:(int)lineNumber
    154                 withDescription:(NSString *)formatString, ... {
    155 
    156   NSString *testDescription = @"";
    157   if (formatString) {
    158     va_list vl;
    159     va_start(vl, formatString);
    160     testDescription =
    161       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
    162     va_end(vl);
    163   }
    164 
    165   NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@",
    166                       expression, testDescription];
    167 
    168   return [self failureInFile:filename atLine:lineNumber reason:reason];
    169 }
    170 
    171 + (NSException *)failureInRaise:(NSString *)expression
    172                       exception:(NSException *)exception
    173                          inFile:(NSString *)filename
    174                          atLine:(int)lineNumber
    175                 withDescription:(NSString *)formatString, ... {
    176 
    177   NSString *testDescription = @"";
    178   if (formatString) {
    179     va_list vl;
    180     va_start(vl, formatString);
    181     testDescription =
    182       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
    183     va_end(vl);
    184   }
    185 
    186   NSString *reason;
    187   if ([[exception name] isEqualToString:SenTestFailureException]) {
    188     // it's our exception, assume it has the right description on it.
    189     reason = [exception reason];
    190   } else {
    191     // not one of our exception, use the exceptions reason and our description
    192     reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@",
    193               expression, [exception reason], testDescription];
    194   }
    195 
    196   return [self failureInFile:filename atLine:lineNumber reason:reason];
    197 }
    198 
    199 @end
    200 
    201 NSString *STComposeString(NSString *formatString, ...) {
    202   NSString *reason = @"";
    203   if (formatString) {
    204     va_list vl;
    205     va_start(vl, formatString);
    206     reason =
    207       [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
    208     va_end(vl);
    209   }
    210   return reason;
    211 }
    212 
    213 NSString *const SenTestFailureException = @"SenTestFailureException";
    214 NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
    215 NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey";
    216 
    217 @interface SenTestCase (SenTestCasePrivate)
    218 // our method of logging errors
    219 + (void)printException:(NSException *)exception fromTestName:(NSString *)name;
    220 @end
    221 
    222 @implementation SenTestCase
    223 + (id)testCaseWithInvocation:(NSInvocation *)anInvocation {
    224   return [[[self alloc] initWithInvocation:anInvocation] autorelease];
    225 }
    226 
    227 - (id)initWithInvocation:(NSInvocation *)anInvocation {
    228   if ((self = [super init])) {
    229     invocation_ = [anInvocation retain];
    230   }
    231   return self;
    232 }
    233 
    234 - (void)dealloc {
    235   [invocation_ release];
    236   [super dealloc];
    237 }
    238 
    239 - (void)failWithException:(NSException*)exception {
    240   [exception raise];
    241 }
    242 
    243 - (void)setUp {
    244 }
    245 
    246 - (void)performTest {
    247   @try {
    248     [self invokeTest];
    249   } @catch (NSException *exception) {
    250     [[self class] printException:exception
    251                     fromTestName:NSStringFromSelector([self selector])];
    252     [exception raise];
    253   }
    254 }
    255 
    256 - (NSInvocation *)invocation {
    257   return invocation_;
    258 }
    259 
    260 - (SEL)selector {
    261   return [invocation_ selector];
    262 }
    263 
    264 + (void)printException:(NSException *)exception fromTestName:(NSString *)name {
    265   NSDictionary *userInfo = [exception userInfo];
    266   NSString *filename = [userInfo objectForKey:SenTestFilenameKey];
    267   NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey];
    268   NSString *className = NSStringFromClass([self class]);
    269   if ([filename length] == 0) {
    270     filename = @"Unknown.m";
    271   }
    272   fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n",
    273           [filename UTF8String],
    274           (long)[lineNumber integerValue],
    275           [className UTF8String],
    276           [name UTF8String],
    277           [[exception reason] UTF8String]);
    278   fflush(stderr);
    279 }
    280 
    281 - (void)invokeTest {
    282   NSException *e = nil;
    283   @try {
    284     // Wrap things in autorelease pools because they may
    285     // have an STMacro in their dealloc which may get called
    286     // when the pool is cleaned up
    287     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    288     // We don't log exceptions here, instead we let the person that called
    289     // this log the exception.  This ensures they are only logged once but the
    290     // outer layers get the exceptions to report counts, etc.
    291     @try {
    292       [self setUp];
    293       @try {
    294         NSInvocation *invocation = [self invocation];
    295 #if GTM_IPHONE_SIMULATOR
    296         // We don't call [invocation invokeWithTarget:self]; because of
    297         // Radar 8081169: NSInvalidArgumentException can't be caught
    298         // It turns out that on iOS4 (and 3.2) exceptions thrown inside an
    299         // [invocation invoke] on the simulator cannot be caught.
    300         // http://openradar.appspot.com/8081169
    301         objc_msgSend(self, [invocation selector]);
    302 #else
    303         [invocation invokeWithTarget:self];
    304 #endif
    305       } @catch (NSException *exception) {
    306         e = [exception retain];
    307       }
    308       [self tearDown];
    309     } @catch (NSException *exception) {
    310       e = [exception retain];
    311     }
    312     [pool release];
    313   } @catch (NSException *exception) {
    314     e = [exception retain];
    315   }
    316   if (e) {
    317     [e autorelease];
    318     [e raise];
    319   }
    320 }
    321 
    322 - (void)tearDown {
    323 }
    324 
    325 - (NSString *)description {
    326   // This matches the description OCUnit would return to you
    327   return [NSString stringWithFormat:@"-[%@ %@]", [self class],
    328           NSStringFromSelector([self selector])];
    329 }
    330 
    331 // Used for sorting methods below
    332 static int MethodSort(id a, id b, void *context) {
    333   NSInvocation *invocationA = a;
    334   NSInvocation *invocationB = b;
    335   const char *nameA = sel_getName([invocationA selector]);
    336   const char *nameB = sel_getName([invocationB selector]);
    337   return strcmp(nameA, nameB);
    338 }
    339 
    340 
    341 + (NSArray *)testInvocations {
    342   NSMutableArray *invocations = nil;
    343   // Need to walk all the way up the parent classes collecting methods (in case
    344   // a test is a subclass of another test).
    345   Class senTestCaseClass = [SenTestCase class];
    346   for (Class currentClass = self;
    347        currentClass && (currentClass != senTestCaseClass);
    348        currentClass = class_getSuperclass(currentClass)) {
    349     unsigned int methodCount;
    350     Method *methods = class_copyMethodList(currentClass, &methodCount);
    351     if (methods) {
    352       // This handles disposing of methods for us even if an exception should fly.
    353       [NSData dataWithBytesNoCopy:methods
    354                            length:sizeof(Method) * methodCount];
    355       if (!invocations) {
    356         invocations = [NSMutableArray arrayWithCapacity:methodCount];
    357       }
    358       for (size_t i = 0; i < methodCount; ++i) {
    359         Method currMethod = methods[i];
    360         SEL sel = method_getName(currMethod);
    361         char *returnType = NULL;
    362         const char *name = sel_getName(sel);
    363         // If it starts with test, takes 2 args (target and sel) and returns
    364         // void run it.
    365         if (strstr(name, "test") == name) {
    366           returnType = method_copyReturnType(currMethod);
    367           if (returnType) {
    368             // This handles disposing of returnType for us even if an
    369             // exception should fly. Length +1 for the terminator, not that
    370             // the length really matters here, as we never reference inside
    371             // the data block.
    372             [NSData dataWithBytesNoCopy:returnType
    373                                  length:strlen(returnType) + 1];
    374           }
    375         }
    376         // TODO: If a test class is a subclass of another, and they reuse the
    377         // same selector name (ie-subclass overrides it), this current loop
    378         // and test here will cause cause it to get invoked twice.  To fix this
    379         // the selector would have to be checked against all the ones already
    380         // added, so it only gets done once.
    381         if (returnType  // True if name starts with "test"
    382             && strcmp(returnType, @encode(void)) == 0
    383             && method_getNumberOfArguments(currMethod) == 2) {
    384           NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel];
    385           NSInvocation *invocation
    386             = [NSInvocation invocationWithMethodSignature:sig];
    387           [invocation setSelector:sel];
    388           [invocations addObject:invocation];
    389         }
    390       }
    391     }
    392   }
    393   // Match SenTestKit and run everything in alphbetical order.
    394   [invocations sortUsingFunction:MethodSort context:nil];
    395   return invocations;
    396 }
    397 
    398 @end
    399 
    400 #endif  // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
    401 
    402 @implementation GTMTestCase : SenTestCase
    403 - (void)invokeTest {
    404   NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init];
    405   Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog");
    406   if (devLogClass) {
    407     [devLogClass performSelector:@selector(enableTracking)];
    408     [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
    409 
    410   }
    411   [super invokeTest];
    412   if (devLogClass) {
    413     [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
    414     [devLogClass performSelector:@selector(disableTracking)];
    415   }
    416   [localPool drain];
    417 }
    418 
    419 + (BOOL)isAbstractTestCase {
    420   NSString *name = NSStringFromClass(self);
    421   return [name rangeOfString:@"AbstractTest"].location != NSNotFound;
    422 }
    423 
    424 + (NSArray *)testInvocations {
    425   NSArray *invocations = nil;
    426   if (![self isAbstractTestCase]) {
    427     invocations = [super testInvocations];
    428   }
    429   return invocations;
    430 }
    431 
    432 @end
    433 
    434 // Leak detection
    435 #if !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK
    436 // Don't want to get leaks on the iPhone Device as the device doesn't
    437 // have 'leaks'. The simulator does though.
    438 
    439 // COV_NF_START
    440 // We don't have leak checking on by default, so this won't be hit.
    441 static void _GTMRunLeaks(void) {
    442   // This is an atexit handler. It runs leaks for us to check if we are
    443   // leaking anything in our tests.
    444   const char* cExclusionsEnv = getenv("GTM_LEAKS_SYMBOLS_TO_IGNORE");
    445   NSMutableString *exclusions = [NSMutableString string];
    446   if (cExclusionsEnv) {
    447     NSString *exclusionsEnv = [NSString stringWithUTF8String:cExclusionsEnv];
    448     NSArray *exclusionsArray = [exclusionsEnv componentsSeparatedByString:@","];
    449     NSString *exclusion;
    450     NSCharacterSet *wcSet = [NSCharacterSet whitespaceCharacterSet];
    451     GTM_FOREACH_OBJECT(exclusion, exclusionsArray) {
    452       exclusion = [exclusion stringByTrimmingCharactersInSet:wcSet];
    453       [exclusions appendFormat:@"-exclude \"%@\" ", exclusion];
    454     }
    455   }
    456   // Clearing out DYLD_ROOT_PATH because iPhone Simulator framework libraries
    457   // are different from regular OS X libraries and leaks will fail to run
    458   // because of missing symbols. Also capturing the output of leaks and then
    459   // pipe rather than a direct pipe, because otherwise if leaks failed,
    460   // the system() call will still be successful. Bug:
    461   // http://code.google.com/p/google-toolbox-for-mac/issues/detail?id=56
    462   NSString *string
    463     = [NSString stringWithFormat:
    464        @"LeakOut=`DYLD_ROOT_PATH='' /usr/bin/leaks %@%d` &&"
    465        @"echo \"$LeakOut\"|/usr/bin/sed -e 's/Leak: /Leaks:0: warning: Leak /'",
    466        exclusions, getpid()];
    467   int ret = system([string UTF8String]);
    468   if (ret) {
    469     fprintf(stderr,
    470             "%s:%d: Error: Unable to run leaks. 'system' returned: %d\n",
    471             __FILE__, __LINE__, ret);
    472     fflush(stderr);
    473   }
    474 }
    475 // COV_NF_END
    476 
    477 static __attribute__((constructor)) void _GTMInstallLeaks(void) {
    478   BOOL checkLeaks = YES;
    479 #if !GTM_IPHONE_SDK
    480   checkLeaks = GTMIsGarbageCollectionEnabled() ? NO : YES;
    481 #endif  // !GTM_IPHONE_SDK
    482   if (checkLeaks) {
    483     checkLeaks = getenv("GTM_ENABLE_LEAKS") ? YES : NO;
    484     if (checkLeaks) {
    485       // COV_NF_START
    486       // We don't have leak checking on by default, so this won't be hit.
    487       fprintf(stderr, "Leak Checking Enabled\n");
    488       fflush(stderr);
    489       int ret = atexit(&_GTMRunLeaks);
    490       // To avoid unused variable warning when _GTMDevAssert is stripped.
    491       (void)ret;
    492       _GTMDevAssert(ret == 0,
    493                     @"Unable to install _GTMRunLeaks as an atexit handler (%d)",
    494                     errno);
    495       // COV_NF_END
    496     }
    497   }
    498 }
    499 
    500 #endif   // !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK
    501