| // | |
| // GTMSenTestCase.m | |
| // | |
| // Copyright 2007-2008 Google Inc. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
| // use this file except in compliance with the License. You may obtain a copy | |
| // of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
| // License for the specific language governing permissions and limitations under | |
| // the License. | |
| // | |
| #import "GTMSenTestCase.h" | |
| #import <unistd.h> | |
| #if GTM_IPHONE_SIMULATOR | |
| #import <objc/message.h> | |
| #endif | |
| #import "GTMObjC2Runtime.h" | |
| #import "GTMUnitTestDevLog.h" | |
| #if GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST | |
| #import <stdarg.h> | |
| @interface NSException (GTMSenTestPrivateAdditions) | |
| + (NSException *)failureInFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| reason:(NSString *)reason; | |
| @end | |
| @implementation NSException (GTMSenTestPrivateAdditions) | |
| + (NSException *)failureInFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| reason:(NSString *)reason { | |
| NSDictionary *userInfo = | |
| [NSDictionary dictionaryWithObjectsAndKeys: | |
| [NSNumber numberWithInteger:lineNumber], SenTestLineNumberKey, | |
| filename, SenTestFilenameKey, | |
| nil]; | |
| return [self exceptionWithName:SenTestFailureException | |
| reason:reason | |
| userInfo:userInfo]; | |
| } | |
| @end | |
| @implementation NSException (GTMSenTestAdditions) | |
| + (NSException *)failureInFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| withDescription:(NSString *)formatString, ... { | |
| NSString *testDescription = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| testDescription = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| NSString *reason = testDescription; | |
| return [self failureInFile:filename atLine:lineNumber reason:reason]; | |
| } | |
| + (NSException *)failureInCondition:(NSString *)condition | |
| isTrue:(BOOL)isTrue | |
| inFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| withDescription:(NSString *)formatString, ... { | |
| NSString *testDescription = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| testDescription = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@", | |
| condition, isTrue ? "false" : "true", testDescription]; | |
| return [self failureInFile:filename atLine:lineNumber reason:reason]; | |
| } | |
| + (NSException *)failureInEqualityBetweenObject:(id)left | |
| andObject:(id)right | |
| inFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| withDescription:(NSString *)formatString, ... { | |
| NSString *testDescription = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| testDescription = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| NSString *reason = | |
| [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@", | |
| [left description], [right description], testDescription]; | |
| return [self failureInFile:filename atLine:lineNumber reason:reason]; | |
| } | |
| + (NSException *)failureInEqualityBetweenValue:(NSValue *)left | |
| andValue:(NSValue *)right | |
| withAccuracy:(NSValue *)accuracy | |
| inFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| withDescription:(NSString *)formatString, ... { | |
| NSString *testDescription = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| testDescription = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| NSString *reason; | |
| if (accuracy) { | |
| reason = | |
| [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@", | |
| left, right, testDescription]; | |
| } else { | |
| reason = | |
| [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@", | |
| left, right, accuracy, testDescription]; | |
| } | |
| return [self failureInFile:filename atLine:lineNumber reason:reason]; | |
| } | |
| + (NSException *)failureInRaise:(NSString *)expression | |
| inFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| withDescription:(NSString *)formatString, ... { | |
| NSString *testDescription = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| testDescription = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@", | |
| expression, testDescription]; | |
| return [self failureInFile:filename atLine:lineNumber reason:reason]; | |
| } | |
| + (NSException *)failureInRaise:(NSString *)expression | |
| exception:(NSException *)exception | |
| inFile:(NSString *)filename | |
| atLine:(int)lineNumber | |
| withDescription:(NSString *)formatString, ... { | |
| NSString *testDescription = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| testDescription = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| NSString *reason; | |
| if ([[exception name] isEqualToString:SenTestFailureException]) { | |
| // it's our exception, assume it has the right description on it. | |
| reason = [exception reason]; | |
| } else { | |
| // not one of our exception, use the exceptions reason and our description | |
| reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@", | |
| expression, [exception reason], testDescription]; | |
| } | |
| return [self failureInFile:filename atLine:lineNumber reason:reason]; | |
| } | |
| @end | |
| NSString *STComposeString(NSString *formatString, ...) { | |
| NSString *reason = @""; | |
| if (formatString) { | |
| va_list vl; | |
| va_start(vl, formatString); | |
| reason = | |
| [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease]; | |
| va_end(vl); | |
| } | |
| return reason; | |
| } | |
| NSString *const SenTestFailureException = @"SenTestFailureException"; | |
| NSString *const SenTestFilenameKey = @"SenTestFilenameKey"; | |
| NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey"; | |
| @interface SenTestCase (SenTestCasePrivate) | |
| // our method of logging errors | |
| + (void)printException:(NSException *)exception fromTestName:(NSString *)name; | |
| @end | |
| @implementation SenTestCase | |
| + (id)testCaseWithInvocation:(NSInvocation *)anInvocation { | |
| return [[[self alloc] initWithInvocation:anInvocation] autorelease]; | |
| } | |
| - (id)initWithInvocation:(NSInvocation *)anInvocation { | |
| if ((self = [super init])) { | |
| invocation_ = [anInvocation retain]; | |
| } | |
| return self; | |
| } | |
| - (void)dealloc { | |
| [invocation_ release]; | |
| [super dealloc]; | |
| } | |
| - (void)failWithException:(NSException*)exception { | |
| [exception raise]; | |
| } | |
| - (void)setUp { | |
| } | |
| - (void)performTest { | |
| @try { | |
| [self invokeTest]; | |
| } @catch (NSException *exception) { | |
| [[self class] printException:exception | |
| fromTestName:NSStringFromSelector([self selector])]; | |
| [exception raise]; | |
| } | |
| } | |
| - (NSInvocation *)invocation { | |
| return invocation_; | |
| } | |
| - (SEL)selector { | |
| return [invocation_ selector]; | |
| } | |
| + (void)printException:(NSException *)exception fromTestName:(NSString *)name { | |
| NSDictionary *userInfo = [exception userInfo]; | |
| NSString *filename = [userInfo objectForKey:SenTestFilenameKey]; | |
| NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey]; | |
| NSString *className = NSStringFromClass([self class]); | |
| if ([filename length] == 0) { | |
| filename = @"Unknown.m"; | |
| } | |
| fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n", | |
| [filename UTF8String], | |
| (long)[lineNumber integerValue], | |
| [className UTF8String], | |
| [name UTF8String], | |
| [[exception reason] UTF8String]); | |
| fflush(stderr); | |
| } | |
| - (void)invokeTest { | |
| NSException *e = nil; | |
| @try { | |
| // Wrap things in autorelease pools because they may | |
| // have an STMacro in their dealloc which may get called | |
| // when the pool is cleaned up | |
| NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; | |
| // We don't log exceptions here, instead we let the person that called | |
| // this log the exception. This ensures they are only logged once but the | |
| // outer layers get the exceptions to report counts, etc. | |
| @try { | |
| [self setUp]; | |
| @try { | |
| NSInvocation *invocation = [self invocation]; | |
| #if GTM_IPHONE_SIMULATOR | |
| // We don't call [invocation invokeWithTarget:self]; because of | |
| // Radar 8081169: NSInvalidArgumentException can't be caught | |
| // It turns out that on iOS4 (and 3.2) exceptions thrown inside an | |
| // [invocation invoke] on the simulator cannot be caught. | |
| // http://openradar.appspot.com/8081169 | |
| objc_msgSend(self, [invocation selector]); | |
| #else | |
| [invocation invokeWithTarget:self]; | |
| #endif | |
| } @catch (NSException *exception) { | |
| e = [exception retain]; | |
| } | |
| [self tearDown]; | |
| } @catch (NSException *exception) { | |
| e = [exception retain]; | |
| } | |
| [pool release]; | |
| } @catch (NSException *exception) { | |
| e = [exception retain]; | |
| } | |
| if (e) { | |
| [e autorelease]; | |
| [e raise]; | |
| } | |
| } | |
| - (void)tearDown { | |
| } | |
| - (NSString *)description { | |
| // This matches the description OCUnit would return to you | |
| return [NSString stringWithFormat:@"-[%@ %@]", [self class], | |
| NSStringFromSelector([self selector])]; | |
| } | |
| // Used for sorting methods below | |
| static int MethodSort(id a, id b, void *context) { | |
| NSInvocation *invocationA = a; | |
| NSInvocation *invocationB = b; | |
| const char *nameA = sel_getName([invocationA selector]); | |
| const char *nameB = sel_getName([invocationB selector]); | |
| return strcmp(nameA, nameB); | |
| } | |
| + (NSArray *)testInvocations { | |
| NSMutableArray *invocations = nil; | |
| // Need to walk all the way up the parent classes collecting methods (in case | |
| // a test is a subclass of another test). | |
| Class senTestCaseClass = [SenTestCase class]; | |
| for (Class currentClass = self; | |
| currentClass && (currentClass != senTestCaseClass); | |
| currentClass = class_getSuperclass(currentClass)) { | |
| unsigned int methodCount; | |
| Method *methods = class_copyMethodList(currentClass, &methodCount); | |
| if (methods) { | |
| // This handles disposing of methods for us even if an exception should fly. | |
| [NSData dataWithBytesNoCopy:methods | |
| length:sizeof(Method) * methodCount]; | |
| if (!invocations) { | |
| invocations = [NSMutableArray arrayWithCapacity:methodCount]; | |
| } | |
| for (size_t i = 0; i < methodCount; ++i) { | |
| Method currMethod = methods[i]; | |
| SEL sel = method_getName(currMethod); | |
| char *returnType = NULL; | |
| const char *name = sel_getName(sel); | |
| // If it starts with test, takes 2 args (target and sel) and returns | |
| // void run it. | |
| if (strstr(name, "test") == name) { | |
| returnType = method_copyReturnType(currMethod); | |
| if (returnType) { | |
| // This handles disposing of returnType for us even if an | |
| // exception should fly. Length +1 for the terminator, not that | |
| // the length really matters here, as we never reference inside | |
| // the data block. | |
| [NSData dataWithBytesNoCopy:returnType | |
| length:strlen(returnType) + 1]; | |
| } | |
| } | |
| // TODO: If a test class is a subclass of another, and they reuse the | |
| // same selector name (ie-subclass overrides it), this current loop | |
| // and test here will cause cause it to get invoked twice. To fix this | |
| // the selector would have to be checked against all the ones already | |
| // added, so it only gets done once. | |
| if (returnType // True if name starts with "test" | |
| && strcmp(returnType, @encode(void)) == 0 | |
| && method_getNumberOfArguments(currMethod) == 2) { | |
| NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel]; | |
| NSInvocation *invocation | |
| = [NSInvocation invocationWithMethodSignature:sig]; | |
| [invocation setSelector:sel]; | |
| [invocations addObject:invocation]; | |
| } | |
| } | |
| } | |
| } | |
| // Match SenTestKit and run everything in alphbetical order. | |
| [invocations sortUsingFunction:MethodSort context:nil]; | |
| return invocations; | |
| } | |
| @end | |
| #endif // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST | |
| @implementation GTMTestCase : SenTestCase | |
| - (void)invokeTest { | |
| NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; | |
| Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog"); | |
| if (devLogClass) { | |
| [devLogClass performSelector:@selector(enableTracking)]; | |
| [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; | |
| } | |
| [super invokeTest]; | |
| if (devLogClass) { | |
| [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)]; | |
| [devLogClass performSelector:@selector(disableTracking)]; | |
| } | |
| [localPool drain]; | |
| } | |
| + (BOOL)isAbstractTestCase { | |
| NSString *name = NSStringFromClass(self); | |
| return [name rangeOfString:@"AbstractTest"].location != NSNotFound; | |
| } | |
| + (NSArray *)testInvocations { | |
| NSArray *invocations = nil; | |
| if (![self isAbstractTestCase]) { | |
| invocations = [super testInvocations]; | |
| } | |
| return invocations; | |
| } | |
| @end |