// // DPUnitTestMain.m // DPUnitTest // // Created by Ofri Wolfus on 07/10/06. // Copyright 2006 Ofri Wolfus. All rights reserved. // #include #include #include #import "DPUnitTest.h" #ifndef _C_CONST #define _C_CONST 'r' #endif #ifndef _C_IN #define _C_IN 'n' #endif #ifndef _C_INOUT #define _C_INOUT 'N' #endif #ifndef _C_OUT #define _C_OUT 'o' #endif #ifndef _C_BYCOPY #define _C_BYCOPY 'O' #endif #ifndef _C_BYREF #define _C_BYREF 'R' #endif #ifndef _C_ONEWAY #define _C_ONEWAY 'V' #endif #ifndef _C_GCINVISIBLE #define _C_GCINVISIBLE '!' #endif @interface DPUnitTest (private) - (void)_DPThrow:(NSException *)e; - (NSString *)failReason; @end BOOL DPBlockUnitTestsExceptions = NO; typedef BOOL (* hom_BOOL_msgSend)(id self, SEL op, ...); static inline char * dput_getReturnType(Method m) __attribute__((always_inline)); static inline char * dput_getReturnType(Method m) { char *type = m->method_types; //Skip type qualifiers while (*type == _C_CONST || *type == _C_IN || *type == _C_INOUT || *type == _C_OUT || *type == _C_BYCOPY || *type == _C_BYREF || *type == _C_ONEWAY || *type == _C_GCINVISIBLE) { type += 1; } return type; } static inline BOOL dput_isSubclassOfClass(Class obj, Class cls) __attribute__((always_inline)); static inline BOOL dput_isSubclassOfClass(Class cls, Class super) { while ((cls = cls->super_class)) { if (cls == super) return YES; } return NO; } static inline BOOL dput_testIsDisabledDisabledTests(Class testCase) { NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init]; NSDictionary *env = [[NSProcessInfo processInfo] environment]; NSEnumerator *e = [env keyEnumerator]; NSString *str, *name = NSStringFromClass(testCase); BOOL r = NO; while ((str = [e nextObject]) && !r) if ([name hasPrefix:str]) r = [[env valueForKey:str] isEqualToString:@"NO"]; [p release]; return r; } static inline BOOL dput_leaksModeEnabled() { NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init]; NSDictionary *env = [[NSProcessInfo processInfo] environment]; NSString *str = [env valueForKey:@"MallocHeapLogging"]; BOOL r = NO; if (str) r = [str isEqualToString:@"YES"]; else { str = [env valueForKey:@"MallocStackLogging"]; r = str != nil && [str isEqualToString:@"1"]; } [p release]; return r; } #if defined (DPUT_NO_INIT) int DPUTInitialized = 0; #else int DPUTInitialized = 1; #endif #if defined (DEBUG_BUILD) static FILE *output_file = stderr; #else static FILE *output_file = stdout; #endif void DPUTRunTests() { if (!DPUTInitialized) { int count = objc_getClassList(NULL, 0); // The number of classes registered with the ObjC runtime Class *classes = calloc(count, sizeof(Class)); // A block of memory for pointers to all classes Class testCls = objc_getClass("DPUnitTest"); // The class for simple unit tests Class speedCls = objc_getClass("DPSpeedTest"); // The class of speed tests int i; // Get an array of all registered classes objc_getClassList(classes, count); // Loop through all classes and find subclasses of DPUnitTest to work with for (i = 0; i < count; i++) { Class cls = classes[i]; // Is our class a subclass of DPUnitTest? if (cls != speedCls && dput_isSubclassOfClass(cls, testCls)) { // Break if our test is disabled if (dput_testIsDisabledDisabledTests(cls)) continue; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Everything in Cocoa needs an autorelease pool void *iterator = 0; // An iterator for class_nextMethodList() struct objc_method_list *mlist; // A list of methods that our class implements id instance = [[cls alloc] init]; // An instance of our class on which we'll invoke our test methods NSString *clsName = [[cls className] retain]; // The name of our class BOOL throws = [instance throwsForFaildTests]; // Are we supposed to throw an exception when a test fails? BOOL logs = [instance logsSuccessfulTests]; // Should we log successful tests? BOOL isSpeedTest = dput_isSubclassOfClass(cls, speedCls); unsigned iterNum = isSpeedTest ? [instance numberOfIterations] : 1U; // The number of times we should run every test BOOL infoDisplayed = NO; // Inidicates whether the beginning message was displayed BOOL verifyAllTests = isSpeedTest ? [instance verifiesAllTests] : NO; Method defaultValidateMeth = class_getInstanceMethod(cls, @selector(verifyResult:ofTest:)); // Loop through all methods lists of this class while ((mlist = class_nextMethodList(cls, &iterator))) { int j; // Loop through the methods in this method list and find the ones which are tests for (j = 0; j < mlist->method_count; j++) { Method m = &(mlist->method_list[j]); const char *methName = sel_getName(m->method_name); // We invoke only methods that their name begins with 'test' if (!strncmp(methName, "test", 4)) { unsigned l; /* Loop counter */ CFAbsoluteTime time = 0; /* Total time for this test */ CFAbsoluteTime t; /* Time of a single call */ BOOL b = YES; /* A flag indicating the success of the last test */ Method verifyMeth = NULL; if (isSpeedTest) { size_t selNameSize = sizeof(char) * (strlen(methName) + 6 + 2); /* == strlen("verify") + ':' + NULL */ char *verifyMethName = malloc(selNameSize); SEL verifySel; strlcat(verifyMethName, "verifyTest", selNameSize); strlcat(verifyMethName, methName + 4 /* skip the 'test' at the beginning */, selNameSize); strlcat(verifyMethName, ":", selNameSize); verifySel = sel_registerName(verifyMethName); verifyMeth = class_getInstanceMethod(cls, verifySel) ?: defaultValidateMeth; // Clean up free(verifyMethName); } if (!infoDisplayed) { //NSLog(@"=== Starting tests in %@ ===", clsName); fprintf(output_file, "=== Starting tests in %s ===\n", [clsName UTF8String]); infoDisplayed = YES; } // Invoke our method as many times as we're told to. // This is customizable per test class through overriding -[DPSpeedTest numberOfIterations] for (l = 0U; l < iterNum; l++) { id result; // Clean up our autorelease pool [pool release]; pool = [[NSAutoreleasePool alloc] init]; if (*dput_getReturnType(m) == *@encode(BOOL)) { // Get the current time t = CFAbsoluteTimeGetCurrent(); // Invoke our method (this is not a message call, but a function call). b = ((hom_BOOL_msgSend)m->method_imp)(instance, m->method_name); // The time took for this test is the current time - the time before invoking the test t = CFAbsoluteTimeGetCurrent() - t; // Add it to the total time += t; } else { // Get the current time t = CFAbsoluteTimeGetCurrent(); // Invoke our method (this is not a message call, but a function call). result = m->method_imp(instance, m->method_name); // The time took for this test is the current time - the time before invoking the test t = CFAbsoluteTimeGetCurrent() - t; // Add it to the total time += t; } // Let our instance a chance to verify the result of this test if (verifyMeth && (l == 0 || verifyAllTests)) b = ((hom_BOOL_msgSend)(verifyMeth->method_imp))(instance, verifyMeth->method_name, result, m->method_name); // Test methods may either return BOOL or void. // If the return type is BOOL, it indicates whether the test succeeded or not. if ((*dput_getReturnType(m) == *@encode(BOOL) || verifyMeth) && !b) { // Our test may have given us a detailed explanation why it failed. // If not, -failReason will return nil. NSString *failReason = [instance failReason]; // Build our fail message NSMutableString *msg = [NSMutableString stringWithFormat:@"*** %@ failed.", NSStringFromSelector(m->method_name)]; // Append the fail reason to it if (failReason) [msg appendFormat:@" %@", failReason]; // Clean up the fail reason [instance setFailReason:nil]; // Throw an exception if needed, if (throws) // We let our instance throw the exception so that when running under the debugger, // the user will be able to examine instance variables if needed. [instance _DPThrow:[NSException exceptionWithName:NSInternalInconsistencyException reason:msg userInfo:nil]]; // Or just log it else //NSLog(@"%@", msg); fprintf(output_file, "%s\n\n", [msg UTF8String]); // Since our test failed, we won't continue this loop break; // Make sure b == YES if our method returns something other than BOOL. // Although this is not pretty, it's the shortest way I can think of for making // sure logging will occure in case of non-BOOL, non-void return types. } else { b = YES; } } // Log if the test succeeded (b == YES), and logging is enabled. // This includes the avarage time of this test. if (logs && b) //NSLog(@"-[%@ %@] passed successfully.\nTest took %f seconds", clsName, NSStringFromSelector(m->method_name), time/iterNum); fprintf(output_file, "* %s passed successfully. Test took %f seconds.\n\n", sel_getName(m->method_name), time/iterNum); } } } // Let our instance a chance to display any info vefore the test is complete [instance release]; // Log if needed if (infoDisplayed) //NSLog(@"=== Tests in %@ finished successfully. ===\n\n", clsName); fprintf(output_file, "=== Tests in %s finished successfully. ===\n\n\n", [clsName UTF8String]); // Clean our autorelease pool [pool release]; } } // Clean up some more free(classes); DPUTInitialized = 1; } } #if defined (DPUT_NO_INIT) __attribute__((destructor)) void _DPUTInit() { DPUTRunTests(); } #else int main (int argc, const char * argv[]) { DPUTInitialized = 0; DPUTRunTests(); // Loop if we're running under MallocDebug if (dput_leaksModeEnabled()) while (1); return 0; } #endif