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 && !GTM_IPHONE_USE_SENTEST 30#import <stdarg.h> 31 32@interface NSException (GTMSenTestPrivateAdditions) 33+ (NSException *)failureInFile:(NSString *)filename 34 atLine:(int)lineNumber 35 reason:(NSString *)reason; 36@end 37 38@implementation NSException (GTMSenTestPrivateAdditions) 39+ (NSException *)failureInFile:(NSString *)filename 40 atLine:(int)lineNumber 41 reason:(NSString *)reason { 42 NSDictionary *userInfo = 43 [NSDictionary dictionaryWithObjectsAndKeys: 44 [NSNumber numberWithInteger:lineNumber], SenTestLineNumberKey, 45 filename, SenTestFilenameKey, 46 nil]; 47 48 return [self exceptionWithName:SenTestFailureException 49 reason:reason 50 userInfo:userInfo]; 51} 52@end 53 54@implementation NSException (GTMSenTestAdditions) 55 56+ (NSException *)failureInFile:(NSString *)filename 57 atLine:(int)lineNumber 58 withDescription:(NSString *)formatString, ... { 59 60 NSString *testDescription = @""; 61 if (formatString) { 62 va_list vl; 63 va_start(vl, formatString); 64 testDescription = 65 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 66 va_end(vl); 67 } 68 69 NSString *reason = testDescription; 70 71 return [self failureInFile:filename atLine:lineNumber reason:reason]; 72} 73 74+ (NSException *)failureInCondition:(NSString *)condition 75 isTrue:(BOOL)isTrue 76 inFile:(NSString *)filename 77 atLine:(int)lineNumber 78 withDescription:(NSString *)formatString, ... { 79 80 NSString *testDescription = @""; 81 if (formatString) { 82 va_list vl; 83 va_start(vl, formatString); 84 testDescription = 85 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 86 va_end(vl); 87 } 88 89 NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@", 90 condition, isTrue ? "false" : "true", testDescription]; 91 92 return [self failureInFile:filename atLine:lineNumber reason:reason]; 93} 94 95+ (NSException *)failureInEqualityBetweenObject:(id)left 96 andObject:(id)right 97 inFile:(NSString *)filename 98 atLine:(int)lineNumber 99 withDescription:(NSString *)formatString, ... { 100 101 NSString *testDescription = @""; 102 if (formatString) { 103 va_list vl; 104 va_start(vl, formatString); 105 testDescription = 106 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 107 va_end(vl); 108 } 109 110 NSString *reason = 111 [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@", 112 [left description], [right description], testDescription]; 113 114 return [self failureInFile:filename atLine:lineNumber reason:reason]; 115} 116 117+ (NSException *)failureInEqualityBetweenValue:(NSValue *)left 118 andValue:(NSValue *)right 119 withAccuracy:(NSValue *)accuracy 120 inFile:(NSString *)filename 121 atLine:(int)lineNumber 122 withDescription:(NSString *)formatString, ... { 123 124 NSString *testDescription = @""; 125 if (formatString) { 126 va_list vl; 127 va_start(vl, formatString); 128 testDescription = 129 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 130 va_end(vl); 131 } 132 133 NSString *reason; 134 if (accuracy) { 135 reason = 136 [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@", 137 left, right, testDescription]; 138 } else { 139 reason = 140 [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@", 141 left, right, accuracy, testDescription]; 142 } 143 144 return [self failureInFile:filename atLine:lineNumber reason:reason]; 145} 146 147+ (NSException *)failureInRaise:(NSString *)expression 148 inFile:(NSString *)filename 149 atLine:(int)lineNumber 150 withDescription:(NSString *)formatString, ... { 151 152 NSString *testDescription = @""; 153 if (formatString) { 154 va_list vl; 155 va_start(vl, formatString); 156 testDescription = 157 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 158 va_end(vl); 159 } 160 161 NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@", 162 expression, testDescription]; 163 164 return [self failureInFile:filename atLine:lineNumber reason:reason]; 165} 166 167+ (NSException *)failureInRaise:(NSString *)expression 168 exception:(NSException *)exception 169 inFile:(NSString *)filename 170 atLine:(int)lineNumber 171 withDescription:(NSString *)formatString, ... { 172 173 NSString *testDescription = @""; 174 if (formatString) { 175 va_list vl; 176 va_start(vl, formatString); 177 testDescription = 178 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 179 va_end(vl); 180 } 181 182 NSString *reason; 183 if ([[exception name] isEqualToString:SenTestFailureException]) { 184 // it's our exception, assume it has the right description on it. 185 reason = [exception reason]; 186 } else { 187 // not one of our exception, use the exceptions reason and our description 188 reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@", 189 expression, [exception reason], testDescription]; 190 } 191 192 return [self failureInFile:filename atLine:lineNumber reason:reason]; 193} 194 195@end 196 197NSString *STComposeString(NSString *formatString, ...) { 198 NSString *reason = @""; 199 if (formatString) { 200 va_list vl; 201 va_start(vl, formatString); 202 reason = 203 [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; 204 va_end(vl); 205 } 206 return reason; 207} 208 209NSString *const SenTestFailureException = @"SenTestFailureException"; 210NSString *const SenTestFilenameKey = @"SenTestFilenameKey"; 211NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey"; 212 213@interface SenTestCase (SenTestCasePrivate) 214// our method of logging errors 215+ (void)printException:(NSException *)exception fromTestName:(NSString *)name; 216@end 217 218@implementation SenTestCase 219+ (id)testCaseWithInvocation:(NSInvocation *)anInvocation { 220 return [[[self alloc] initWithInvocation:anInvocation] autorelease]; 221} 222 223- (id)initWithInvocation:(NSInvocation *)anInvocation { 224 if ((self = [super init])) { 225 invocation_ = [anInvocation retain]; 226 } 227 return self; 228} 229 230- (void)dealloc { 231 [invocation_ release]; 232 [super dealloc]; 233} 234 235- (void)failWithException:(NSException*)exception { 236 [exception raise]; 237} 238 239- (void)setUp { 240} 241 242- (void)performTest { 243 @try { 244 [self invokeTest]; 245 } @catch (NSException *exception) { 246 [[self class] printException:exception 247 fromTestName:NSStringFromSelector([self selector])]; 248 [exception raise]; 249 } 250} 251 252- (NSInvocation *)invocation { 253 return invocation_; 254} 255 256- (SEL)selector { 257 return [invocation_ selector]; 258} 259 260+ (void)printException:(NSException *)exception fromTestName:(NSString *)name { 261 NSDictionary *userInfo = [exception userInfo]; 262 NSString *filename = [userInfo objectForKey:SenTestFilenameKey]; 263 NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey]; 264 NSString *className = NSStringFromClass([self class]); 265 if ([filename length] == 0) { 266 filename = @"Unknown.m"; 267 } 268 fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n", 269 [filename UTF8String], 270 (long)[lineNumber integerValue], 271 [className UTF8String], 272 [name UTF8String], 273 [[exception reason] UTF8String]); 274 fflush(stderr); 275} 276 277- (void)invokeTest { 278 NSException *e = nil; 279 @try { 280 // Wrap things in autorelease pools because they may 281 // have an STMacro in their dealloc which may get called 282 // when the pool is cleaned up 283 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 284 // We don't log exceptions here, instead we let the person that called 285 // this log the exception. This ensures they are only logged once but the 286 // outer layers get the exceptions to report counts, etc. 287 @try { 288 [self setUp]; 289 @try { 290 NSInvocation *invocation = [self invocation]; 291#if GTM_IPHONE_SIMULATOR 292 // We don't call [invocation invokeWithTarget:self]; because of 293 // Radar 8081169: NSInvalidArgumentException can't be caught 294 // It turns out that on iOS4 (and 3.2) exceptions thrown inside an 295 // [invocation invoke] on the simulator cannot be caught. 296 // http://openradar.appspot.com/8081169 297 objc_msgSend(self, [invocation selector]); 298#else 299 [invocation invokeWithTarget:self]; 300#endif 301 } @catch (NSException *exception) { 302 e = [exception retain]; 303 } 304 [self tearDown]; 305 } @catch (NSException *exception) { 306 e = [exception retain]; 307 } 308 [pool release]; 309 } @catch (NSException *exception) { 310 e = [exception retain]; 311 } 312 if (e) { 313 [e autorelease]; 314 [e raise]; 315 } 316} 317 318- (void)tearDown { 319} 320 321- (NSString *)description { 322 // This matches the description OCUnit would return to you 323 return [NSString stringWithFormat:@"-[%@ %@]", [self class], 324 NSStringFromSelector([self selector])]; 325} 326 327// Used for sorting methods below 328static int MethodSort(id a, id b, void *context) { 329 NSInvocation *invocationA = a; 330 NSInvocation *invocationB = b; 331 const char *nameA = sel_getName([invocationA selector]); 332 const char *nameB = sel_getName([invocationB selector]); 333 return strcmp(nameA, nameB); 334} 335 336 337+ (NSArray *)testInvocations { 338 NSMutableArray *invocations = nil; 339 // Need to walk all the way up the parent classes collecting methods (in case 340 // a test is a subclass of another test). 341 Class senTestCaseClass = [SenTestCase class]; 342 for (Class currentClass = self; 343 currentClass && (currentClass != senTestCaseClass); 344 currentClass = class_getSuperclass(currentClass)) { 345 unsigned int methodCount; 346 Method *methods = class_copyMethodList(currentClass, &methodCount); 347 if (methods) { 348 // This handles disposing of methods for us even if an exception should fly. 349 [NSData dataWithBytesNoCopy:methods 350 length:sizeof(Method) * methodCount]; 351 if (!invocations) { 352 invocations = [NSMutableArray arrayWithCapacity:methodCount]; 353 } 354 for (size_t i = 0; i < methodCount; ++i) { 355 Method currMethod = methods[i]; 356 SEL sel = method_getName(currMethod); 357 char *returnType = NULL; 358 const char *name = sel_getName(sel); 359 // If it starts with test, takes 2 args (target and sel) and returns 360 // void run it. 361 if (strstr(name, "test") == name) { 362 returnType = method_copyReturnType(currMethod); 363 if (returnType) { 364 // This handles disposing of returnType for us even if an 365 // exception should fly. Length +1 for the terminator, not that 366 // the length really matters here, as we never reference inside 367 // the data block. 368 [NSData dataWithBytesNoCopy:returnType 369 length:strlen(returnType) + 1]; 370 } 371 } 372 // TODO: If a test class is a subclass of another, and they reuse the 373 // same selector name (ie-subclass overrides it), this current loop 374 // and test here will cause cause it to get invoked twice. To fix this 375 // the selector would have to be checked against all the ones already 376 // added, so it only gets done once. 377 if (returnType // True if name starts with "test" 378 && strcmp(returnType, @encode(void)) == 0 379 && method_getNumberOfArguments(currMethod) == 2) { 380 NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel]; 381 NSInvocation *invocation 382 = [NSInvocation invocationWithMethodSignature:sig]; 383 [invocation setSelector:sel]; 384 [invocations addObject:invocation]; 385 } 386 } 387 } 388 } 389 // Match SenTestKit and run everything in alphbetical order. 390 [invocations sortUsingFunction:MethodSort context:nil]; 391 return invocations; 392} 393 394@end 395 396#endif // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST 397 398@implementation GTMTestCase : SenTestCase 399- (void)invokeTest { 400 NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; 401 Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog"); 402 if (devLogClass) { 403 [devLogClass performSelector:@selector(enableTracking)]; 404 [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; 405 406 } 407 [super invokeTest]; 408 if (devLogClass) { 409 [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; 410 [devLogClass performSelector:@selector(disableTracking)]; 411 } 412 [localPool drain]; 413} 414 415+ (BOOL)isAbstractTestCase { 416 NSString *name = NSStringFromClass(self); 417 return [name rangeOfString:@"AbstractTest"].location != NSNotFound; 418} 419 420+ (NSArray *)testInvocations { 421 NSArray *invocations = nil; 422 if (![self isAbstractTestCase]) { 423 invocations = [super testInvocations]; 424 } 425 return invocations; 426} 427 428@end 429