michael@0: // michael@0: // GTMSenTestCase.m michael@0: // michael@0: // Copyright 2007-2008 Google Inc. michael@0: // michael@0: // Licensed under the Apache License, Version 2.0 (the "License"); you may not michael@0: // use this file except in compliance with the License. You may obtain a copy michael@0: // of the License at michael@0: // michael@0: // http://www.apache.org/licenses/LICENSE-2.0 michael@0: // michael@0: // Unless required by applicable law or agreed to in writing, software michael@0: // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT michael@0: // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the michael@0: // License for the specific language governing permissions and limitations under michael@0: // the License. michael@0: // michael@0: michael@0: #import "GTMSenTestCase.h" michael@0: michael@0: #import michael@0: #if GTM_IPHONE_SIMULATOR michael@0: #import michael@0: #endif michael@0: michael@0: #import "GTMObjC2Runtime.h" michael@0: #import "GTMUnitTestDevLog.h" michael@0: michael@0: #if !GTM_IPHONE_SDK michael@0: #import "GTMGarbageCollection.h" michael@0: #endif // !GTM_IPHONE_SDK michael@0: michael@0: #if GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST michael@0: #import michael@0: michael@0: @interface NSException (GTMSenTestPrivateAdditions) michael@0: + (NSException *)failureInFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: reason:(NSString *)reason; michael@0: @end michael@0: michael@0: @implementation NSException (GTMSenTestPrivateAdditions) michael@0: + (NSException *)failureInFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: reason:(NSString *)reason { michael@0: NSDictionary *userInfo = michael@0: [NSDictionary dictionaryWithObjectsAndKeys: michael@0: [NSNumber numberWithInteger:lineNumber], SenTestLineNumberKey, michael@0: filename, SenTestFilenameKey, michael@0: nil]; michael@0: michael@0: return [self exceptionWithName:SenTestFailureException michael@0: reason:reason michael@0: userInfo:userInfo]; michael@0: } michael@0: @end michael@0: michael@0: @implementation NSException (GTMSenTestAdditions) michael@0: michael@0: + (NSException *)failureInFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: withDescription:(NSString *)formatString, ... { michael@0: michael@0: NSString *testDescription = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: testDescription = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: michael@0: NSString *reason = testDescription; michael@0: michael@0: return [self failureInFile:filename atLine:lineNumber reason:reason]; michael@0: } michael@0: michael@0: + (NSException *)failureInCondition:(NSString *)condition michael@0: isTrue:(BOOL)isTrue michael@0: inFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: withDescription:(NSString *)formatString, ... { michael@0: michael@0: NSString *testDescription = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: testDescription = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: michael@0: NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@", michael@0: condition, isTrue ? "false" : "true", testDescription]; michael@0: michael@0: return [self failureInFile:filename atLine:lineNumber reason:reason]; michael@0: } michael@0: michael@0: + (NSException *)failureInEqualityBetweenObject:(id)left michael@0: andObject:(id)right michael@0: inFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: withDescription:(NSString *)formatString, ... { michael@0: michael@0: NSString *testDescription = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: testDescription = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: michael@0: NSString *reason = michael@0: [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@", michael@0: [left description], [right description], testDescription]; michael@0: michael@0: return [self failureInFile:filename atLine:lineNumber reason:reason]; michael@0: } michael@0: michael@0: + (NSException *)failureInEqualityBetweenValue:(NSValue *)left michael@0: andValue:(NSValue *)right michael@0: withAccuracy:(NSValue *)accuracy michael@0: inFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: withDescription:(NSString *)formatString, ... { michael@0: michael@0: NSString *testDescription = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: testDescription = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: michael@0: NSString *reason; michael@0: if (accuracy) { michael@0: reason = michael@0: [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@", michael@0: left, right, testDescription]; michael@0: } else { michael@0: reason = michael@0: [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@", michael@0: left, right, accuracy, testDescription]; michael@0: } michael@0: michael@0: return [self failureInFile:filename atLine:lineNumber reason:reason]; michael@0: } michael@0: michael@0: + (NSException *)failureInRaise:(NSString *)expression michael@0: inFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: withDescription:(NSString *)formatString, ... { michael@0: michael@0: NSString *testDescription = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: testDescription = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: michael@0: NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@", michael@0: expression, testDescription]; michael@0: michael@0: return [self failureInFile:filename atLine:lineNumber reason:reason]; michael@0: } michael@0: michael@0: + (NSException *)failureInRaise:(NSString *)expression michael@0: exception:(NSException *)exception michael@0: inFile:(NSString *)filename michael@0: atLine:(int)lineNumber michael@0: withDescription:(NSString *)formatString, ... { michael@0: michael@0: NSString *testDescription = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: testDescription = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: michael@0: NSString *reason; michael@0: if ([[exception name] isEqualToString:SenTestFailureException]) { michael@0: // it's our exception, assume it has the right description on it. michael@0: reason = [exception reason]; michael@0: } else { michael@0: // not one of our exception, use the exceptions reason and our description michael@0: reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@", michael@0: expression, [exception reason], testDescription]; michael@0: } michael@0: michael@0: return [self failureInFile:filename atLine:lineNumber reason:reason]; michael@0: } michael@0: michael@0: @end michael@0: michael@0: NSString *STComposeString(NSString *formatString, ...) { michael@0: NSString *reason = @""; michael@0: if (formatString) { michael@0: va_list vl; michael@0: va_start(vl, formatString); michael@0: reason = michael@0: [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; michael@0: va_end(vl); michael@0: } michael@0: return reason; michael@0: } michael@0: michael@0: NSString *const SenTestFailureException = @"SenTestFailureException"; michael@0: NSString *const SenTestFilenameKey = @"SenTestFilenameKey"; michael@0: NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey"; michael@0: michael@0: @interface SenTestCase (SenTestCasePrivate) michael@0: // our method of logging errors michael@0: + (void)printException:(NSException *)exception fromTestName:(NSString *)name; michael@0: @end michael@0: michael@0: @implementation SenTestCase michael@0: + (id)testCaseWithInvocation:(NSInvocation *)anInvocation { michael@0: return [[[self alloc] initWithInvocation:anInvocation] autorelease]; michael@0: } michael@0: michael@0: - (id)initWithInvocation:(NSInvocation *)anInvocation { michael@0: if ((self = [super init])) { michael@0: invocation_ = [anInvocation retain]; michael@0: } michael@0: return self; michael@0: } michael@0: michael@0: - (void)dealloc { michael@0: [invocation_ release]; michael@0: [super dealloc]; michael@0: } michael@0: michael@0: - (void)failWithException:(NSException*)exception { michael@0: [exception raise]; michael@0: } michael@0: michael@0: - (void)setUp { michael@0: } michael@0: michael@0: - (void)performTest { michael@0: @try { michael@0: [self invokeTest]; michael@0: } @catch (NSException *exception) { michael@0: [[self class] printException:exception michael@0: fromTestName:NSStringFromSelector([self selector])]; michael@0: [exception raise]; michael@0: } michael@0: } michael@0: michael@0: - (NSInvocation *)invocation { michael@0: return invocation_; michael@0: } michael@0: michael@0: - (SEL)selector { michael@0: return [invocation_ selector]; michael@0: } michael@0: michael@0: + (void)printException:(NSException *)exception fromTestName:(NSString *)name { michael@0: NSDictionary *userInfo = [exception userInfo]; michael@0: NSString *filename = [userInfo objectForKey:SenTestFilenameKey]; michael@0: NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey]; michael@0: NSString *className = NSStringFromClass([self class]); michael@0: if ([filename length] == 0) { michael@0: filename = @"Unknown.m"; michael@0: } michael@0: fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n", michael@0: [filename UTF8String], michael@0: (long)[lineNumber integerValue], michael@0: [className UTF8String], michael@0: [name UTF8String], michael@0: [[exception reason] UTF8String]); michael@0: fflush(stderr); michael@0: } michael@0: michael@0: - (void)invokeTest { michael@0: NSException *e = nil; michael@0: @try { michael@0: // Wrap things in autorelease pools because they may michael@0: // have an STMacro in their dealloc which may get called michael@0: // when the pool is cleaned up michael@0: NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; michael@0: // We don't log exceptions here, instead we let the person that called michael@0: // this log the exception. This ensures they are only logged once but the michael@0: // outer layers get the exceptions to report counts, etc. michael@0: @try { michael@0: [self setUp]; michael@0: @try { michael@0: NSInvocation *invocation = [self invocation]; michael@0: #if GTM_IPHONE_SIMULATOR michael@0: // We don't call [invocation invokeWithTarget:self]; because of michael@0: // Radar 8081169: NSInvalidArgumentException can't be caught michael@0: // It turns out that on iOS4 (and 3.2) exceptions thrown inside an michael@0: // [invocation invoke] on the simulator cannot be caught. michael@0: // http://openradar.appspot.com/8081169 michael@0: objc_msgSend(self, [invocation selector]); michael@0: #else michael@0: [invocation invokeWithTarget:self]; michael@0: #endif michael@0: } @catch (NSException *exception) { michael@0: e = [exception retain]; michael@0: } michael@0: [self tearDown]; michael@0: } @catch (NSException *exception) { michael@0: e = [exception retain]; michael@0: } michael@0: [pool release]; michael@0: } @catch (NSException *exception) { michael@0: e = [exception retain]; michael@0: } michael@0: if (e) { michael@0: [e autorelease]; michael@0: [e raise]; michael@0: } michael@0: } michael@0: michael@0: - (void)tearDown { michael@0: } michael@0: michael@0: - (NSString *)description { michael@0: // This matches the description OCUnit would return to you michael@0: return [NSString stringWithFormat:@"-[%@ %@]", [self class], michael@0: NSStringFromSelector([self selector])]; michael@0: } michael@0: michael@0: // Used for sorting methods below michael@0: static int MethodSort(id a, id b, void *context) { michael@0: NSInvocation *invocationA = a; michael@0: NSInvocation *invocationB = b; michael@0: const char *nameA = sel_getName([invocationA selector]); michael@0: const char *nameB = sel_getName([invocationB selector]); michael@0: return strcmp(nameA, nameB); michael@0: } michael@0: michael@0: michael@0: + (NSArray *)testInvocations { michael@0: NSMutableArray *invocations = nil; michael@0: // Need to walk all the way up the parent classes collecting methods (in case michael@0: // a test is a subclass of another test). michael@0: Class senTestCaseClass = [SenTestCase class]; michael@0: for (Class currentClass = self; michael@0: currentClass && (currentClass != senTestCaseClass); michael@0: currentClass = class_getSuperclass(currentClass)) { michael@0: unsigned int methodCount; michael@0: Method *methods = class_copyMethodList(currentClass, &methodCount); michael@0: if (methods) { michael@0: // This handles disposing of methods for us even if an exception should fly. michael@0: [NSData dataWithBytesNoCopy:methods michael@0: length:sizeof(Method) * methodCount]; michael@0: if (!invocations) { michael@0: invocations = [NSMutableArray arrayWithCapacity:methodCount]; michael@0: } michael@0: for (size_t i = 0; i < methodCount; ++i) { michael@0: Method currMethod = methods[i]; michael@0: SEL sel = method_getName(currMethod); michael@0: char *returnType = NULL; michael@0: const char *name = sel_getName(sel); michael@0: // If it starts with test, takes 2 args (target and sel) and returns michael@0: // void run it. michael@0: if (strstr(name, "test") == name) { michael@0: returnType = method_copyReturnType(currMethod); michael@0: if (returnType) { michael@0: // This handles disposing of returnType for us even if an michael@0: // exception should fly. Length +1 for the terminator, not that michael@0: // the length really matters here, as we never reference inside michael@0: // the data block. michael@0: [NSData dataWithBytesNoCopy:returnType michael@0: length:strlen(returnType) + 1]; michael@0: } michael@0: } michael@0: // TODO: If a test class is a subclass of another, and they reuse the michael@0: // same selector name (ie-subclass overrides it), this current loop michael@0: // and test here will cause cause it to get invoked twice. To fix this michael@0: // the selector would have to be checked against all the ones already michael@0: // added, so it only gets done once. michael@0: if (returnType // True if name starts with "test" michael@0: && strcmp(returnType, @encode(void)) == 0 michael@0: && method_getNumberOfArguments(currMethod) == 2) { michael@0: NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel]; michael@0: NSInvocation *invocation michael@0: = [NSInvocation invocationWithMethodSignature:sig]; michael@0: [invocation setSelector:sel]; michael@0: [invocations addObject:invocation]; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: // Match SenTestKit and run everything in alphbetical order. michael@0: [invocations sortUsingFunction:MethodSort context:nil]; michael@0: return invocations; michael@0: } michael@0: michael@0: @end michael@0: michael@0: #endif // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST michael@0: michael@0: @implementation GTMTestCase : SenTestCase michael@0: - (void)invokeTest { michael@0: NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; michael@0: Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog"); michael@0: if (devLogClass) { michael@0: [devLogClass performSelector:@selector(enableTracking)]; michael@0: [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; michael@0: michael@0: } michael@0: [super invokeTest]; michael@0: if (devLogClass) { michael@0: [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; michael@0: [devLogClass performSelector:@selector(disableTracking)]; michael@0: } michael@0: [localPool drain]; michael@0: } michael@0: michael@0: + (BOOL)isAbstractTestCase { michael@0: NSString *name = NSStringFromClass(self); michael@0: return [name rangeOfString:@"AbstractTest"].location != NSNotFound; michael@0: } michael@0: michael@0: + (NSArray *)testInvocations { michael@0: NSArray *invocations = nil; michael@0: if (![self isAbstractTestCase]) { michael@0: invocations = [super testInvocations]; michael@0: } michael@0: return invocations; michael@0: } michael@0: michael@0: @end michael@0: michael@0: // Leak detection michael@0: #if !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK michael@0: // Don't want to get leaks on the iPhone Device as the device doesn't michael@0: // have 'leaks'. The simulator does though. michael@0: michael@0: // COV_NF_START michael@0: // We don't have leak checking on by default, so this won't be hit. michael@0: static void _GTMRunLeaks(void) { michael@0: // This is an atexit handler. It runs leaks for us to check if we are michael@0: // leaking anything in our tests. michael@0: const char* cExclusionsEnv = getenv("GTM_LEAKS_SYMBOLS_TO_IGNORE"); michael@0: NSMutableString *exclusions = [NSMutableString string]; michael@0: if (cExclusionsEnv) { michael@0: NSString *exclusionsEnv = [NSString stringWithUTF8String:cExclusionsEnv]; michael@0: NSArray *exclusionsArray = [exclusionsEnv componentsSeparatedByString:@","]; michael@0: NSString *exclusion; michael@0: NSCharacterSet *wcSet = [NSCharacterSet whitespaceCharacterSet]; michael@0: GTM_FOREACH_OBJECT(exclusion, exclusionsArray) { michael@0: exclusion = [exclusion stringByTrimmingCharactersInSet:wcSet]; michael@0: [exclusions appendFormat:@"-exclude \"%@\" ", exclusion]; michael@0: } michael@0: } michael@0: // Clearing out DYLD_ROOT_PATH because iPhone Simulator framework libraries michael@0: // are different from regular OS X libraries and leaks will fail to run michael@0: // because of missing symbols. Also capturing the output of leaks and then michael@0: // pipe rather than a direct pipe, because otherwise if leaks failed, michael@0: // the system() call will still be successful. Bug: michael@0: // http://code.google.com/p/google-toolbox-for-mac/issues/detail?id=56 michael@0: NSString *string michael@0: = [NSString stringWithFormat: michael@0: @"LeakOut=`DYLD_ROOT_PATH='' /usr/bin/leaks %@%d` &&" michael@0: @"echo \"$LeakOut\"|/usr/bin/sed -e 's/Leak: /Leaks:0: warning: Leak /'", michael@0: exclusions, getpid()]; michael@0: int ret = system([string UTF8String]); michael@0: if (ret) { michael@0: fprintf(stderr, michael@0: "%s:%d: Error: Unable to run leaks. 'system' returned: %d\n", michael@0: __FILE__, __LINE__, ret); michael@0: fflush(stderr); michael@0: } michael@0: } michael@0: // COV_NF_END michael@0: michael@0: static __attribute__((constructor)) void _GTMInstallLeaks(void) { michael@0: BOOL checkLeaks = YES; michael@0: #if !GTM_IPHONE_SDK michael@0: checkLeaks = GTMIsGarbageCollectionEnabled() ? NO : YES; michael@0: #endif // !GTM_IPHONE_SDK michael@0: if (checkLeaks) { michael@0: checkLeaks = getenv("GTM_ENABLE_LEAKS") ? YES : NO; michael@0: if (checkLeaks) { michael@0: // COV_NF_START michael@0: // We don't have leak checking on by default, so this won't be hit. michael@0: fprintf(stderr, "Leak Checking Enabled\n"); michael@0: fflush(stderr); michael@0: int ret = atexit(&_GTMRunLeaks); michael@0: // To avoid unused variable warning when _GTMDevAssert is stripped. michael@0: (void)ret; michael@0: _GTMDevAssert(ret == 0, michael@0: @"Unable to install _GTMRunLeaks as an atexit handler (%d)", michael@0: errno); michael@0: // COV_NF_END michael@0: } michael@0: } michael@0: } michael@0: michael@0: #endif // !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK