/core/externals/google-toolbox-for-mac/UnitTesting/GTMSenTestCase.m
Objective C | 505 lines | 374 code | 65 blank | 66 comment | 39 complexity | 2855ced15c9e5b1daf5db5e92d998216 MD5 | raw file
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 <UIKit/UIKit.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 NSInteger 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 403 404- (void)invokeTest { 405 NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; 406 Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog"); 407 if (devLogClass) { 408 [devLogClass performSelector:@selector(enableTracking)]; 409 [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; 410 411 } 412 [super invokeTest]; 413 if (devLogClass) { 414 [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; 415 [devLogClass performSelector:@selector(disableTracking)]; 416 } 417 [localPool drain]; 418} 419 420+ (BOOL)isAbstractTestCase { 421 NSString *name = NSStringFromClass(self); 422 return [name rangeOfString:@"AbstractTest"].location != NSNotFound; 423} 424 425#if GTM_IPHONE_SDK 426- (UIImage *)imageFromResource:(NSString *)resource { 427 NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 428 NSString *path = [bundle pathForResource:resource ofType:nil]; 429 UIImage *image = [UIImage imageWithContentsOfFile:path]; 430 STAssertNotNil(image, @"Could not load image from resource: %@", path); 431 return image; 432} 433#endif 434 435+ (NSArray *)testInvocations { 436 NSArray *invocations = nil; 437 if (![self isAbstractTestCase]) { 438 invocations = [super testInvocations]; 439 } 440 return invocations; 441} 442 443@end 444 445// Leak detection 446#if !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK 447// Don't want to get leaks on the iPhone Device as the device doesn't 448// have 'leaks'. The simulator does though. 449 450// COV_NF_START 451// We don't have leak checking on by default, so this won't be hit. 452static void _GTMRunLeaks(void) { 453 // This is an atexit handler. It runs leaks for us to check if we are 454 // leaking anything in our tests. 455 const char* cExclusionsEnv = getenv("GTM_LEAKS_SYMBOLS_TO_IGNORE"); 456 NSMutableString *exclusions = [NSMutableString string]; 457 if (cExclusionsEnv) { 458 NSString *exclusionsEnv = [NSString stringWithUTF8String:cExclusionsEnv]; 459 NSArray *exclusionsArray = [exclusionsEnv componentsSeparatedByString:@","]; 460 NSString *exclusion; 461 NSCharacterSet *wcSet = [NSCharacterSet whitespaceCharacterSet]; 462 GTM_FOREACH_OBJECT(exclusion, exclusionsArray) { 463 exclusion = [exclusion stringByTrimmingCharactersInSet:wcSet]; 464 [exclusions appendFormat:@"-exclude \"%@\" ", exclusion]; 465 } 466 } 467 // Clearing out DYLD_ROOT_PATH because iPhone Simulator framework libraries 468 // are different from regular OS X libraries and leaks will fail to run 469 // because of missing symbols. Also capturing the output of leaks and then 470 // pipe rather than a direct pipe, because otherwise if leaks failed, 471 // the system() call will still be successful. Bug: 472 // http://code.google.com/p/google-toolbox-for-mac/issues/detail?id=56 473 NSString *string 474 = [NSString stringWithFormat: 475 @"LeakOut=`DYLD_ROOT_PATH='' /usr/bin/leaks %@%d` &&" 476 @"echo \"$LeakOut\"|/usr/bin/sed -e 's/Leak: /Leaks:0: warning: Leak /'", 477 exclusions, getpid()]; 478 int ret = system([string UTF8String]); 479 if (ret) { 480 fprintf(stderr, 481 "%s:%d: Error: Unable to run leaks. 'system' returned: %d\n", 482 __FILE__, __LINE__, ret); 483 fflush(stderr); 484 } 485} 486// COV_NF_END 487 488static __attribute__((constructor)) void _GTMInstallLeaks(void) { 489 BOOL checkLeaks = getenv("GTM_ENABLE_LEAKS") ? YES : NO; 490 if (checkLeaks) { 491 // COV_NF_START 492 // We don't have leak checking on by default, so this won't be hit. 493 fprintf(stderr, "Leak Checking Enabled\n"); 494 fflush(stderr); 495 int ret = atexit(&_GTMRunLeaks); 496 // To avoid unused variable warning when _GTMDevAssert is stripped. 497 (void)ret; 498 _GTMDevAssert(ret == 0, 499 @"Unable to install _GTMRunLeaks as an atexit handler (%d)", 500 errno); 501 // COV_NF_END 502 } 503} 504 505#endif // !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK