From cdf070c8d76ffc4eaa24e8671756cbbe9ceb2890 Mon Sep 17 00:00:00 2001 From: thomasvl Date: Mon, 14 Apr 2008 17:21:02 +0000 Subject: See the ReleaseNotes for the full details, highlights: - bug fixes - code coverage support - more complete unittests - full support for unittesting UIs - support for the iphone sdk (include ui unittesting) --- DebugUtils/GTMDebugSelectorValidation.h | 86 +++++++++++++++++++ DebugUtils/GTMMethodCheck.h | 88 +++++++++++++++++++ DebugUtils/GTMMethodCheck.m | 144 ++++++++++++++++++++++++++++++++ DebugUtils/GTMMethodCheckTest.m | 69 +++++++++++++++ 4 files changed, 387 insertions(+) create mode 100644 DebugUtils/GTMDebugSelectorValidation.h create mode 100644 DebugUtils/GTMMethodCheck.h create mode 100644 DebugUtils/GTMMethodCheck.m create mode 100644 DebugUtils/GTMMethodCheckTest.m (limited to 'DebugUtils') diff --git a/DebugUtils/GTMDebugSelectorValidation.h b/DebugUtils/GTMDebugSelectorValidation.h new file mode 100644 index 0000000..b3f1e73 --- /dev/null +++ b/DebugUtils/GTMDebugSelectorValidation.h @@ -0,0 +1,86 @@ +// +// GTMDebugSelectorValidation.h +// +// This file should only be included within an implimation file. In any +// function that takes an object and selector to invoke, you should call: +// +// GTMAssertSelectorNilOrImplementedWithArguments(obj, sel, @encode(arg1type), ..., NULL) +// +// This will then validate that the selector is defined and using the right +// type(s), this can help catch errors much earlier then waiting for the +// selector to actually fire (and in the case of error selectors, might never +// really be tested until in the field). +// +// 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. +// + +#if DEBUG + +#import +#import "GTMDefines.h" + +static void GTMAssertSelectorNilOrImplementedWithArguments(id obj, SEL sel, ...) { + + // verify that the object's selector is implemented with the proper + // number and type of arguments + va_list argList; + va_start(argList, sel); + + if (obj && sel) { + // check that the selector is implemented + if (![obj respondsToSelector:sel]) { + _GTMDevAssert(NO, + @"\"%@\" selector \"%@\" is unimplemented or misnamed", + NSStringFromClass([obj class]), + NSStringFromSelector(sel)); + } else { + const char *expectedArgType; + int argCount = 2; // skip self and _cmd + NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; + + // check that each expected argument is present and of the correct type + while ((expectedArgType = va_arg(argList, const char*)) != 0) { + + if ([sig numberOfArguments] > argCount) { + const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; + + _GTMDevAssert(0 == strncmp(foundArgType, expectedArgType, strlen(expectedArgType)), + @"\"%@\" selector \"%@\" argument %d should be type %s", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), + (argCount - 2), + expectedArgType); + } + argCount++; + } + + // check that the proper number of arguments are present in the selector + _GTMDevAssert(argCount == [sig numberOfArguments], + @"\"%@\" selector \"%@\" should have %d arguments", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), + (argCount - 2)); + } + } + + va_end(argList); +} + +#else // DEBUG + +// make it go away if not debug +#define GTMAssertSelectorNilOrImplementedWithArguments(obj, sel, ...) do { } while (0) + +#endif // DEBUG diff --git a/DebugUtils/GTMMethodCheck.h b/DebugUtils/GTMMethodCheck.h new file mode 100644 index 0000000..0915c0b --- /dev/null +++ b/DebugUtils/GTMMethodCheck.h @@ -0,0 +1,88 @@ +// +// GTMMethodCheck.h +// +// Copyright 2006-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 +#import +#import + +/// A macro for enforcing debug time checks to make sure all required methods are linked in +// +// When using categories, it can be very easy to forget to include the +// implementation of a category. +// Let's say you had a class foo that depended on method bar of class baz, and +// method bar was implemented as a member of a category. +// You could add the following code: +// @implementation foo +// GTM_METHOD_CHECK(baz, bar) +// @end +// and the code would check to make sure baz was implemented just before main +// was called. This works for both dynamic libraries, and executables. +// +// Classes (or one of their superclasses) being checked must conform to the +// NSObject protocol. We will check this, and spit out a warning if a class does +// not conform to NSObject. +// +// This is not compiled into release builds. + +#ifdef DEBUG + +#ifdef __cplusplus +extern "C" { +#endif + +// If you get an error for GTMMethodCheckMethodChecker not being defined, +// you need to link in GTMMethodCheck.m. We keep it hidden so that we can have +// it living in several separate images without conflict. +// Functions with the ((constructor)) attribute are called after all +loads +// have been called. See "Initializing Objective-C Classes" in +// http://developer.apple.com/documentation/DeveloperTools/Conceptual/DynamicLibraries/Articles/DynamicLibraryDesignGuidelines.html#//apple_ref/doc/uid/TP40002013-DontLinkElementID_20 + +__attribute__ ((constructor, visibility("hidden"))) void GTMMethodCheckMethodChecker(void); + +#ifdef __cplusplus +}; +#endif + +// This is the "magic". +// A) we need a multi layer define here so that the stupid preprocessor +// expands __LINE__ out the way we want it. We need LINE so that each of +// out GTM_METHOD_CHECKs generates a unique class method for the class. +#define GTM_METHOD_CHECK(class, method) GTM_METHOD_CHECK_INNER(class, method, __LINE__) +#define GTM_METHOD_CHECK_INNER(class, method, line) GTM_METHOD_CHECK_INNER_INNER(class, method, line) + +// B) Create up a class method called xxGMethodCheckMethod+class+line that the +// GTMMethodCheckMethodChecker function can look for and call. We +// look for GTMMethodCheckMethodChecker to enforce linkage of +// GTMMethodCheck.m. +#define GTM_METHOD_CHECK_INNER_INNER(class, method, line) \ ++ (void)xxGMMethodCheckMethod ## class ## line { \ + void (*addr)() = GTMMethodCheckMethodChecker; \ + if (addr && ![class instancesRespondToSelector:@selector(method)] \ + && ![class respondsToSelector:@selector(method)]) { \ + fprintf(stderr, "%s:%d: error: We need method '%s' to be linked in for class '%s'\n", \ + __FILE__, line, #method, #class); \ + exit(EX_SOFTWARE); \ + } \ +} + +#else // !DEBUG + +// Do nothing in debug +#define GTM_METHOD_CHECK(class, method) + +#endif // DEBUG diff --git a/DebugUtils/GTMMethodCheck.m b/DebugUtils/GTMMethodCheck.m new file mode 100644 index 0000000..379f006 --- /dev/null +++ b/DebugUtils/GTMMethodCheck.m @@ -0,0 +1,144 @@ +// +// GTMMethodCheck.m +// +// Copyright 2006-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. +// + +// Don't want any of this in release builds +#ifdef DEBUG +#import "GTMDefines.h" +#import "GTMMethodCheck.h" +#import "GTMObjC2Runtime.h" +#import + +// Checks to see if the cls passed in (or one of it's superclasses) conforms +// to NSObject protocol. Inheriting from NSObject is the easiest way to do this +// but not all classes (i.e. NSProxy) inherit from NSObject. Also, some classes +// inherit from Object instead of NSObject which is fine, and we'll count as +// conforming to NSObject for our needs. +static BOOL ConformsToNSObjectProtocol(Class cls) { + // If we get nil, obviously doesn't conform. + if (!cls) return NO; + const char *className = class_getName(cls); + if (!className) return NO; + + // We're going to assume that all Apple classes will work + // (and aren't being checked) + // Note to apple: why doesn't obj-c have real namespaces instead of two + // letter hacks? If you name your own classes starting with NS this won't + // work for you. + // Some classes (like _NSZombie) start with _NS. + // On Leopard we have to look for CFObject as well. + if ((strncmp(className, "NS", 2) == 0) || + (strncmp(className, "_NS", 3) == 0) || + (strcmp(className, "CFObject") == 0)) { + return YES; + } + + // Iterate through all the protocols |cls| supports looking for NSObject. + if (cls == [Object class] + || class_conformsToProtocol(cls, @protocol(NSObject))) { + return YES; + } + + // Recursively check the superclasses. + return ConformsToNSObjectProtocol(class_getSuperclass(cls)); +} + +void GTMMethodCheckMethodChecker(void) { + // Run through all the classes looking for class methods that are + // prefixed with xxGMMethodCheckMethod. If it finds one, it calls it. + // See GTMMethodCheck.h to see what it does. + int numClasses = 0; + int newNumClasses = objc_getClassList(NULL, 0); + int i; + Class *classes = NULL; + while (numClasses < newNumClasses) { + numClasses = newNumClasses; + classes = realloc(classes, sizeof(Class) * numClasses); + _GTMDevAssert(classes, @"Unable to allocate memory for classes"); + newNumClasses = objc_getClassList(classes, numClasses); + } + for (i = 0; i < numClasses; ++i) { + Class cls = classes[i]; + + // Since we are directly calling objc_msgSend, we need to conform to + // @protocol(NSObject), or else we will tumble into a _objc_msgForward + // recursive loop when we try and call a function by name. + if (!ConformsToNSObjectProtocol(cls)) { + _GTMDevLog(@"GTMMethodCheckMethodChecker: Class %s does not conform to " + "@protocol(NSObject), so won't be checked", + class_getName(cls)); + continue; + } + // Since we are looking for a class method (+xxGMMethodCheckMethod...) + // we need to query the isa pointer to see what methods it support, but + // send the method (if it's supported) to the class itself. + unsigned int count; + Class metaClass = objc_getMetaClass(class_getName(cls)); + Method *methods = class_copyMethodList(metaClass, &count); + unsigned int j; + for (j = 0; j < count; ++j) { + SEL selector = method_getName(methods[j]); + const char *name = sel_getName(selector); + if (strstr(name, "xxGTMMethodCheckMethod") == name) { + // Check to make sure that the method we are checking comes + // from the same image that we are in. Since GTMMethodCheckMethodChecker + // is not exported, we should always find the copy in our local + // image. We compare the address of it's image with the address of + // the image which implements the method we want to check. If + // they match we continue. This does two things: + // a) minimizes the amount of calls we make to the xxxGTMMethodCheck + // methods. They should only be called once. + // b) prevents initializers for various classes being called too early + Dl_info methodCheckerInfo; + if (!dladdr(GTMMethodCheckMethodChecker, + &methodCheckerInfo)) { + // COV_NF_START + // Don't know how to force this case in a unittest. + // Certainly hope we never see it. + _GTMDevLog(@"GTMMethodCheckMethodChecker: Unable to get dladdr info " + "for GTMMethodCheckMethodChecker while introspecting +[%@ %@]]", + class_getName(cls), name); + continue; + // COV_NF_END + } + Dl_info methodInfo; + if (!dladdr(method_getImplementation(methods[j]), + &methodInfo)) { + // COV_NF_START + // Don't know how to force this case in a unittest + // Certainly hope we never see it. + _GTMDevLog(@"GTMMethodCheckMethodChecker: Unable to get dladdr info " + "for %@ while introspecting +[%@ %@]]", name, + class_getName(cls), name); + continue; + // COV_NF_END + } + if (methodCheckerInfo.dli_fbase == methodInfo.dli_fbase) { + objc_msgSend(cls, selector); + } + } + } + if (methods) { + free(methods); + } + } + if (classes) { + free(classes); + } +} + +#endif // DEBUG diff --git a/DebugUtils/GTMMethodCheckTest.m b/DebugUtils/GTMMethodCheckTest.m new file mode 100644 index 0000000..b5e7e6b --- /dev/null +++ b/DebugUtils/GTMMethodCheckTest.m @@ -0,0 +1,69 @@ +// +// GTMMethodCheckTest.m +// Copyright 2006-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 "GTMMethodCheck.h" + +static BOOL gTestCheckVar = NO; + +// This is a contrived class that doesn't inherit from NSObject, but does +// implement some of it's functionality to force test a case in +// GTMMethodCheck. +@interface GTMClassThatDoesntInheritFromNSObject ++ (BOOL)instancesRespondToSelector:(SEL)selector; ++ (BOOL)respondsToSelector:(SEL)selector; +@end + +@implementation GTMClassThatDoesntInheritFromNSObject +GTM_METHOD_CHECK(GTMClassThatDoesntInheritFromNSObject, GTMMethodCheckTestMethod); +- (void)GTMMethodCheckTestMethod { +} ++ (BOOL)instancesRespondToSelector:(SEL)selector { + return YES; +} + ++ (BOOL)respondsToSelector:(SEL)selector { + return YES; +} +@end + +@interface GTMMethodCheckTest : SenTestCase +@end + +@implementation GTMMethodCheckTest +GTM_METHOD_CHECK(GTMMethodCheckTest, GTMMethodCheckTestMethod); +GTM_METHOD_CHECK(GTMMethodCheckTest, GTMMethodCheckTestClassMethod); + +- (void)GTMMethodCheckTestMethod { +} + ++ (void)GTMMethodCheckTestClassMethod { +} + ++ (void)xxGTMMethodCheckMethodTestCheck { + // This gets called because of its special name by GMMethodCheck + // Look at the Macros in GMMethodCheck.h for details. + gTestCheckVar = YES; +} + +- (void)testGTMMethodCheck { +#ifdef DEBUG + // GTMMethodCheck only runs in debug + STAssertTrue(gTestCheckVar, @"Should be true"); +#endif +} +@end -- cgit v1.2.3