/* This source is part of UnitKit, a unit test framework for Mac OS X development. You can find more information about UnitKit at: http://x180.net/Code/UnitKit Copyright (c)2004 James Duncan Davidson Contributions by Mark Dalrymple, Nicolas Roard, Quentin mathe 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. The use of the Apache License does not indicate that this project is affiliated with the Apache Software Foundation. */ #import "UKRunner.h" #import "UKTest.h" #import "UKTestHandler.h" // FIXME: we shouldn't need to put import for AppKit.h here (see UKRunner.h) #import /* For GNUstep, but we should check if it is really needed */ #import #ifndef GNUSTEP #ifdef AVAILABLE_MAC_OS_X_VERSION_10_3_AND_LATER #define NEW_EXCEPTION_MODEL #endif #import #else #import #endif @implementation UKRunner + (NSString *) localizedString:(NSString *)key { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; return NSLocalizedStringFromTableInBundle(key, @"UKRunner", bundle, @""); } + (NSString *) displayStringForException:(id)exc { if ([exc isKindOfClass:[NSException class]]) { return [NSString stringWithFormat:@"NSException: %@ %@", [exc name], [exc reason]]; } else { return NSStringFromClass([exc class]); } } + (int) runTests { /* We expect the following usage: $ ukrun [BundleName] If there are no arguments given, then we'll just execute every test class found. Otherwise */ NSApplication *app = [NSApplication sharedApplication]; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSString *cwd = [[NSFileManager defaultManager] currentDirectoryPath]; //printf("ukrun starting\n"); //printf("cwd: %s\n", [cwd UTF8String]); NSArray *args = [[NSProcessInfo processInfo] arguments]; int argCount = [args count]; UKRunner *runner = [[UKRunner alloc] init]; NSBundle *testBundle; if (argCount >= 2) { printf("ukrun version 1.1\n"); // XXX replace with a real auto version int i = 1; // Mark Dalrymple contributed this bit about going quiet. if ([[args objectAtIndex:1] isEqualToString: @"-q"]) { [[UKTestHandler handler] setQuiet: YES]; i++; } while (i < argCount) { NSString *bundlePath = [args objectAtIndex:i]; NSLog (@"bundle path: %@", bundlePath); bundlePath = [bundlePath stringByExpandingTildeInPath]; if ( ![bundlePath isAbsolutePath]) { bundlePath = [cwd stringByAppendingPathComponent:bundlePath]; bundlePath = [bundlePath stringByStandardizingPath]; } printf("looking for bundle at path: %s\n", [bundlePath UTF8String]); // make sure bundle exists and is loaded testBundle = [NSBundle bundleWithPath:bundlePath]; if (testBundle == nil) { // XXX i18n as well as message improvements printf("Test bundle %s could not be found\n", [bundlePath UTF8String]); [pool release]; return -1; } if (![testBundle load]) { // XXX i18n as well as message improvements printf("Test bundle could not be loaded\n"); [pool release]; return -1; } [runner runTestsInBundle:testBundle]; i++; } } else { printf("Usage: ukrun [-q] [bundlename]\n"); [pool release]; return -1; } int testsPassed = [[UKTestHandler handler] testsPassed]; int testsFailed = [[UKTestHandler handler] testsFailed]; int testClasses = runner->testClassesRun; int testMethods = runner->testMethodsRun; [runner release]; [pool release]; // XXX i18n printf("Result: %i classes, %i methods, %i tests, %i failed\n", testClasses, testMethods, (testsPassed + testsFailed), testsFailed); if (testsFailed == 0) { return 0; } else { return -1; } } #ifndef GNUSTEP - (void) runTest:(SEL)testSelector onObject:(id)testObject { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop performSelector:testSelector target:testObject argument:nil order:0 modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]; CFRunLoopRef cfRunLoop = [runLoop getCFRunLoop]; [runLoop runUntilDate:nil]; while (CFRunLoopIsWaiting(cfRunLoop)) { [runLoop runUntilDate:nil]; } } #endif /*! @method runTests:onClass: @param testMethods An array containing the list of method names to execute on the test class. @param testClass The class on which to perform the test methods on @abstract Runs a set of tests on instances of the given class @discussion This method takes a class and a list of methods that should be executed on it. For each method in the list, an object instance of the class will be created and the method called on it. If there is a problem with the instanation of the class, or in the release of that object instance, an error will be reported and all test execution on the class will end. If there is an error while running the test method, an error will be reported and execution will move on to the next method. */ - (void) runTests:(NSArray *)testMethods onClass:(Class)testClass { /* The hairy thing about this method is catching and dealing with all of the permutations of uncaught exceptions that might be heading our way. */ //NSLog(@"testClass %@", testClass); NSEnumerator *e = [testMethods objectEnumerator]; NSString *testMethodName; while (testMethodName = [e nextObject]) { testMethodsRun++; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id testObject; #ifdef NEW_EXCEPTION_MODEL @try { testObject = [[testClass alloc] init]; } @catch (id exc) { NSString *msg = [UKRunner localizedString:@"errExceptionOnInit"]; NSString *excstring = [UKRunner displayStringForException:exc]; msg = [NSString stringWithFormat:msg, NSStringFromClass(testClass), excstring]; [[UKTestHandler handler] reportWarning:msg]; [pool release]; return; } @try { SEL testSel = NSSelectorFromString(testMethodName); [self runTest:testSel onObject:testObject]; } @catch (id exc) { NSString *msg = [UKRunner localizedString:@"errExceptionInTestMethod"]; NSString *excstring = [UKRunner displayStringForException:exc]; msg = [NSString stringWithFormat:msg, NSStringFromClass(testClass), testMethodName, excstring]; [[UKTestHandler handler] reportWarning:msg]; } @try { [testObject release]; } @catch (id exc) { NSString *msg = [UKRunner localizedString:@"errExceptionOnRelease"]; NSString *excstring = [UKRunner displayStringForException:exc]; msg = [NSString stringWithFormat:msg, NSStringFromClass(testClass), excstring]; [[UKTestHandler handler] reportWarning:msg]; [pool release]; return; } #else NS_DURING { testObject = [testClass alloc]; if ([testObject respondsToSelector: @selector(initForTest)]) { testObject = [testObject initForTest]; } else { testObject = [testObject init]; } NSLog(@"testObject %@", testObject); } NS_HANDLER { NSString *msg = [UKRunner localizedString:@"errExceptionOnInit"]; msg = [NSString stringWithFormat:msg, NSStringFromClass(testClass), [localException name]]; [[UKTestHandler handler] reportWarning:msg]; [pool release]; return; } NS_ENDHANDLER NS_DURING { SEL testSel = NSSelectorFromString(testMethodName); [testObject performSelector:testSel]; } NS_HANDLER { NSString *msg = [UKRunner localizedString:@"errExceptionInTestMethod"]; msg = [NSString stringWithFormat:msg, NSStringFromClass(testClass), testMethodName, [localException name]]; [[UKTestHandler handler] reportWarning:msg]; [pool release]; return; } NS_ENDHANDLER NS_DURING { if ([testObject respondsToSelector: @selector(releaseForTest)]) { [testObject releaseForTest]; } else { [testObject release]; } } NS_HANDLER { NSString *msg = [UKRunner localizedString:@"errExceptionOnRelease"]; msg = [NSString stringWithFormat:msg, NSStringFromClass(testClass), [localException name]]; [[UKTestHandler handler] reportWarning:msg]; [pool release]; return; } NS_ENDHANDLER #endif [pool release]; } } - (void) runTestsInClass:(Class)testClass { testClassesRun++; NSArray *testMethods = UKTestMethodNamesFromClass(testClass); [self runTests:testMethods onClass:testClass]; } - (void) runTestsInBundle:(NSBundle *)bundle { NSArray *testClasses = UKTestClasseNamesFromBundle(bundle); NSEnumerator *e = [testClasses objectEnumerator]; NSString *testClassName; while (testClassName = [e nextObject]) { [self runTestsInClass:NSClassFromString(testClassName)]; } } @end NSArray *UKTestClasseNamesFromBundle(NSBundle *bundle) { NSMutableArray *testClasseNames = [NSMutableArray array]; #ifndef GNU_RUNTIME /* I found the code to walk the classes in the system from an example in Apple's documentation. Pretty much all I changed was the bit that tested which bundle a class came from and that it implements the UKTest protocol. It's a bit low level (hell, there's a malloc and free here!), but there doesn't seem to be any easier way to get a list of classes from a bundle. I keep thinking that this kind of functionality could be factored out into a general "What classes are in which bundles and what classes implement what protocols. But so far I've resisted the urge. */ int numClasses; numClasses = objc_getClassList(NULL, 0); if (numClasses > 0) { Class *classes = malloc(sizeof(Class) * numClasses); (void) objc_getClassList (classes, numClasses); int i; for (i = 0; i < numClasses; i++) { Class c = classes[i]; NSBundle *classBundle = [NSBundle bundleForClass:c]; if (bundle == classBundle && [c conformsToProtocol:@protocol(UKTest)]) { [testClasseNames addObject:NSStringFromClass(c)]; } } free(classes); } #else /* Nicolas Roard contributed the following code to pick up test classes from a bundle. */ Class c; void *es = NULL; while ((c = objc_next_class (&es)) != Nil) { NSBundle *classBundle = [NSBundle bundleForClass: c]; if (bundle == classBundle && [c conformsToProtocol:@protocol(UKTest)]) { [testClasseNames addObject:NSStringFromClass(c)]; } } //NSLog(@"testClasses %@", testClasseNames); #endif return [testClasseNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; } NSArray *UKTestMethodNamesFromClass(Class c) { NSMutableArray *testMethods = [NSMutableArray array]; #ifndef GNU_RUNTIME /* I think I picked this code up originaly from some Apple sample code. But I could be wrong. It's landed here from all of my previous UnitKit iterations. Of course, it's been modified to look for methods that start with a test prefix. */ void *iterator = 0; struct objc_method_list *mlist = class_nextMethodList(c, &iterator); while (mlist != NULL) { int i; for (i = 0; i < mlist->method_count; i++) { Method method = &(mlist->method_list[i]); if (method == NULL) { continue; } SEL sel = method->method_name; NSString *methodName = NSStringFromSelector(sel); if ([methodName hasPrefix:@"test"]) { [testMethods addObject:methodName]; } } mlist = class_nextMethodList(c, &iterator); } #else /* Nicolas Roard contributed the following code to pick up test classes from a bundle. */ MethodList_t methods = c->methods; while (methods != NULL) { int i; for (i = 0; i < methods->method_count; i++) { Method_t method = &(methods->method_list[i]); if (method == METHOD_NULL) { continue; } SEL sel = method->method_name; NSString *methodName = NSStringFromSelector(sel); if ([methodName hasPrefix:@"test"]) { [testMethods addObject:methodName]; } } methods = methods->method_next; } //NSLog(@"testMethods %@", testMethods); #endif return [testMethods sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; }