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 201NSString *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 213NSString *const SenTestFailureException = @"SenTestFailureException"; 214NSString *const SenTestFilenameKey = @"SenTestFilenameKey"; 215NSString *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 332static 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. 441static 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 477static __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