// // GTMNSObject+UnitTesting.m // // An informal protocol for doing advanced unittesting with objects. // // 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 "GTMNSObject+UnitTesting.h" #import "GTMSystemVersion.h" #import "GTMGarbageCollection.h" #import "GTMNSNumber+64Bit.h" #if GTM_IPHONE_SDK #import #else #import #endif NSString *const GTMUnitTestingEncodedObjectNotification = @"GTMUnitTestingEncodedObjectNotification"; NSString *const GTMUnitTestingEncoderKey = @"GTMUnitTestingEncoderKey"; #if GTM_IPHONE_SDK // No UTIs on iPhone. Only two we need. const CFStringRef kUTTypePNG = CFSTR("public.png"); const CFStringRef kUTTypeJPEG = CFSTR("public.jpeg"); #endif // This class exists so that we can locate our bundle using [NSBundle // bundleForClass:]. We don't use [NSBundle mainBundle] because when we are // being run as a unit test, we aren't the mainBundle @interface GTMUnitTestingAdditionsBundleFinder : NSObject { // Nothing here } // or here @end @implementation GTMUnitTestingAdditionsBundleFinder // Nothing here. We're just interested in the name for finding our bundle. @end BOOL GTMIsObjectImageEqualToImageNamed(id object, NSString* filename, NSString **error) { NSString *failString = nil; if (error) { *error = nil; } BOOL isGood = [object respondsToSelector:@selector(gtm_unitTestImage)]; if (isGood) { if ([object gtm_areSystemSettingsValidForDoingImage]) { NSString *aPath = [object gtm_pathForImageNamed:filename]; CGImageRef diff = nil; isGood = aPath != nil; if (isGood) { isGood = [object gtm_compareWithImageAt:aPath diffImage:&diff]; } if (!isGood) { if (aPath) { filename = [filename stringByAppendingString:@"_Failed"]; } BOOL aSaved = [object gtm_saveToImageNamed:filename]; NSString *fileNameWithExtension = [NSString stringWithFormat:@"%@.%@", filename, [object gtm_imageExtension]]; NSString *fullSavePath = [object gtm_saveToPathForImageNamed:filename]; if (NO == aSaved) { if (!aPath) { failString = [NSString stringWithFormat:@"File %@ did not exist in " @"bundle. Tried to save as %@ and failed.", fileNameWithExtension, fullSavePath]; } else { failString = [NSString stringWithFormat:@"Object image different " @"than file %@. Tried to save as %@ and failed.", aPath, fullSavePath]; } } else { if (!aPath) { failString = [NSString stringWithFormat:@"File %@ did not exist in " @" bundle. Saved to %@", fileNameWithExtension, fullSavePath]; } else { NSString *diffPath = [filename stringByAppendingString:@"_Diff"]; diffPath = [object gtm_saveToPathForImageNamed:diffPath]; NSData *data = nil; if (diff) { data = [object gtm_imageDataForImage:diff]; } if ([data writeToFile:diffPath atomically:YES]) { failString = [NSString stringWithFormat:@"Object image different " @"than file %@. Saved image to %@. " @"Saved diff to %@", aPath, fullSavePath, diffPath]; } else { failString = [NSString stringWithFormat:@"Object image different " @"than file %@. Saved image to %@. Unable to save " @"diff. Most likely the image and diff are " @"different sizes.", aPath, fullSavePath]; } } } } CGImageRelease(diff); } else { failString = @"systemSettings not valid for taking image"; // COV_NF_LINE } } else { failString = @"Object does not conform to GTMUnitTestingImaging protocol"; } if (error) { *error = failString; } return isGood; } BOOL GTMIsObjectStateEqualToStateNamed(id object, NSString* filename, NSString **error) { NSString *failString = nil; if (error) { *error = nil; } BOOL isGood = [object conformsToProtocol:@protocol(GTMUnitTestingEncoding)]; if (isGood) { NSString *aPath = [object gtm_pathForStateNamed:filename]; isGood = aPath != nil; if (isGood) { isGood = [object gtm_compareWithStateAt:aPath]; } if (!isGood) { if (aPath) { filename = [filename stringByAppendingString:@"_Failed"]; } BOOL aSaved = [object gtm_saveToStateNamed:filename]; NSString *fileNameWithExtension = [NSString stringWithFormat:@"%@.%@", filename, [object gtm_stateExtension]]; NSString *fullSavePath = [object gtm_saveToPathForStateNamed:filename]; if (NO == aSaved) { if (!aPath) { failString = [NSString stringWithFormat:@"File %@ did not exist in " @"bundle. Tried to save as %@ and failed.", fileNameWithExtension, fullSavePath]; } else { failString = [NSString stringWithFormat:@"Object state different " @"than file %@. Tried to save as %@ and failed.", aPath, fullSavePath]; } } else { if (!aPath) { failString = [NSString stringWithFormat:@"File %@ did not exist in " @ "bundle. Saved to %@", fileNameWithExtension, fullSavePath]; } else { failString = [NSString stringWithFormat:@"Object state different " @"than file %@. Saved to %@", aPath, fullSavePath]; } } } } else { failString = @"Object does not conform to GTMUnitTestingEncoding protocol"; } if (error) { *error = failString; } return isGood; } CGContextRef GTMCreateUnitTestBitmapContextOfSizeWithData(CGSize size, unsigned char **data) { CGContextRef context = NULL; size_t height = size.height; size_t width = size.width; size_t bytesPerRow = width * 4; size_t bitsPerComponent = 8; CGColorSpaceRef cs = NULL; #if GTM_IPHONE_SDK cs = CGColorSpaceCreateDeviceRGB(); #else cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); #endif _GTMDevAssert(cs, @"Couldn't create colorspace"); CGBitmapInfo info = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault; if (data) { *data = (unsigned char*)calloc(bytesPerRow, height); _GTMDevAssert(*data, @"Couldn't create bitmap"); } context = CGBitmapContextCreate(data ? *data : NULL, width, height, bitsPerComponent, bytesPerRow, cs, info); _GTMDevAssert(context, @"Couldn't create an context"); if (!data) { CGContextClearRect(context, CGRectMake(0, 0, size.width, size.height)); } CGContextSetRenderingIntent(context, kCGRenderingIntentRelativeColorimetric); CGContextSetInterpolationQuality(context, kCGInterpolationNone); CGContextSetShouldAntialias(context, NO); CGContextSetAllowsAntialiasing(context, NO); CGContextSetShouldSmoothFonts(context, NO); CGColorSpaceRelease(cs); return context; } @interface NSObject (GTMUnitTestingAdditionsPrivate) /// Find the path for a file named name.extension in your bundle. // Searches for the following: // "name.extension", // "name.arch.extension", // "name.arch.OSVersionMajor.extension" // "name.arch.OSVersionMajor.OSVersionMinor.extension" // "name.arch.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.extension" // "name.arch.OSVersionMajor.extension" // "name.OSVersionMajor.arch.extension" // "name.OSVersionMajor.OSVersionMinor.arch.extension" // "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.arch.extension" // "name.OSVersionMajor.extension" // "name.OSVersionMajor.OSVersionMinor.extension" // "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.extension" // Do not include the ".extension" extension on your name. // // Args: // name: The name for the file you would like to find. // extension: the extension for the file you would like to find // // Returns: // the path if the file exists in your bundle // or nil if no file is found // - (NSString *)gtm_pathForFileNamed:(NSString*)name extension:(NSString*)extension; - (NSString *)gtm_saveToPathForFileNamed:(NSString*)name extension:(NSString*)extension; - (CGImageRef)gtm_unitTestImage; // Returns nil if there is no override - (NSString *)gtm_getOverrideDefaultUnitTestSaveToDirectory; @end // This is a keyed coder for storing unit test state data. It is used only by // the GTMUnitTestingAdditions category. Most of the work is done in // encodeObject:forKey:. @interface GTMUnitTestingKeyedCoder : NSCoder { NSMutableDictionary *dictionary_; // storage for data (STRONG) } // get the data stored in coder. // // Returns: // NSDictionary with currently stored data. - (NSDictionary*)dictionary; @end // Small utility function for checking to see if a is b +/- 1. GTM_INLINE BOOL almostEqual(unsigned char a, unsigned char b) { unsigned char diff = a > b ? a - b : b - a; BOOL notEqual = diff < 2; return notEqual; } @implementation GTMUnitTestingKeyedCoder // Set up storage for coder. Stores type and version. // Version 1 // // Returns: // self - (id)init { self = [super init]; if (self != nil) { dictionary_ = [[NSMutableDictionary alloc] initWithCapacity:2]; [dictionary_ setObject:@"GTMUnitTestingArchive" forKey:@"$GTMArchive"]; // Version number can be changed here. [dictionary_ setObject:[NSNumber numberWithInt:1] forKey:@"$GTMVersion"]; } return self; } // Standard dealloc - (void)dealloc { [dictionary_ release]; [super dealloc]; } // Utility function for checking for a key value. We don't want duplicate keys // in any of our dictionaries as we may be writing over data stored by previous // objects. // // Arguments: // key - key to check for in dictionary - (void)checkForKey:(NSString*)key { _GTMDevAssert(![dictionary_ objectForKey:key], @"Key already exists for %@", key); } // Key routine for the encoder. We store objects in our dictionary based on // their key. As we encode objects we send out notifications to let other // classes doing tests add their specific data to the base types. If we can't // encode the object (it doesn't support gtm_unitTestEncodeState) and we don't // get any info back from the notifier, we attempt to store it's description. // // Arguments: // objv - object to be encoded // key - key to encode it with // - (void)encodeObject:(id)objv forKey:(NSString *)key { // Sanity checks if (!objv) return; [self checkForKey:key]; // Set up a new dictionary for the current object NSMutableDictionary *curDictionary = dictionary_; dictionary_ = [[NSMutableDictionary alloc] initWithCapacity:0]; // If objv responds to gtm_unitTestEncodeState get it to record // its data. if ([objv respondsToSelector:@selector(gtm_unitTestEncodeState:)]) { [objv gtm_unitTestEncodeState:self]; } // We then send out a notification to let other folks // add data for this object NSDictionary *notificationDict = [NSDictionary dictionaryWithObject:self forKey:GTMUnitTestingEncoderKey]; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc postNotificationName:GTMUnitTestingEncodedObjectNotification object:objv userInfo:notificationDict]; // If we got anything from the object, or from the notification, store it in // our dictionary. Otherwise store the description. if ([dictionary_ count] > 0) { [curDictionary setObject:dictionary_ forKey:key]; } else { NSString *description = [objv description]; // If description has a pointer value in it, we don't want to store it // as the pointer value can change from run to run if (description && [description rangeOfString:@"0x"].length == 0) { [curDictionary setObject:description forKey:key]; } else { _GTMDevAssert(NO, @"Unable to encode forKey: %@", key); // COV_NF_LINE } } [dictionary_ release]; dictionary_ = curDictionary; } // Basic encoding methods for POD types. // // Arguments: // *v - value to encode // key - key to encode it in - (void)encodeBool:(BOOL)boolv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSNumber numberWithBool:boolv] forKey:key]; } - (void)encodeInt:(int)intv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSNumber numberWithInt:intv] forKey:key]; } - (void)encodeInt32:(int32_t)intv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSNumber numberWithLong:intv] forKey:key]; } - (void)encodeInt64:(int64_t)intv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSNumber numberWithLongLong:intv] forKey:key]; } - (void)encodeFloat:(float)realv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSNumber numberWithFloat:realv] forKey:key]; } - (void)encodeDouble:(double)realv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSNumber numberWithDouble:realv] forKey:key]; } - (void)encodeBytes:(const uint8_t *)bytesp length:(NSUInteger)lenv forKey:(NSString *)key { [self checkForKey:key]; [dictionary_ setObject:[NSData dataWithBytes:bytesp length:lenv] forKey:key]; } // Get our storage back as an NSDictionary // // Returns: // NSDictionary containing our encoded info -(NSDictionary*)dictionary { return [[dictionary_ retain] autorelease]; } @end static NSString *gGTMUnitTestSaveToDirectory = nil; @implementation NSObject (GTMUnitTestingAdditions) + (void)gtm_setUnitTestSaveToDirectory:(NSString*)path { @synchronized([self class]) { [gGTMUnitTestSaveToDirectory autorelease]; gGTMUnitTestSaveToDirectory = [path copy]; } } + (NSString *)gtm_getUnitTestSaveToDirectory { NSString *result = nil; @synchronized([self class]) { if (!gGTMUnitTestSaveToDirectory) { #if GTM_IPHONE_SDK // Developer build, use their home directory Desktop. gGTMUnitTestSaveToDirectory = [[[[[NSHomeDirectory() stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"Desktop"]; #else NSArray *desktopDirs = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES); gGTMUnitTestSaveToDirectory = [desktopDirs objectAtIndex:0]; #endif // Did we get overridden? NSString *override = [self gtm_getOverrideDefaultUnitTestSaveToDirectory]; if (override) { gGTMUnitTestSaveToDirectory = override; } [gGTMUnitTestSaveToDirectory retain]; } result = gGTMUnitTestSaveToDirectory; } return result; } // Return nil if there is no override - (NSString *)gtm_getOverrideDefaultUnitTestSaveToDirectory { NSString *result = nil; // If we have an environment variable that ends in "BUILD_NUMBER" odds are // we're on an automated build system, so use the build products dir as an // override instead of writing on the desktop. NSDictionary *env = [[NSProcessInfo processInfo] environment]; NSString *key; GTM_FOREACH_KEY(key, env) { if ([key hasSuffix:@"BUILD_NUMBER"]) { break; } } if (key) { result = [env objectForKey:@"BUILT_PRODUCTS_DIR"]; } if (result && [result length] == 0) { result = nil; } return result; } /// Find the path for a file named name.extension in your bundle. // Searches for the following: // "name.CompilerSDK.OSVersionMajor.OSVersionMinor.OSVersionBugFix.arch.extension" // "name.CompilerSDK.OSVersionMajor.OSVersionMinor.arch.extension" // "name.CompilerSDK.OSVersionMajor.arch.extension" // "name.CompilerSDK.arch.extension" // "name.CompilerSDK.OSVersionMajor.OSVersionMinor.OSVersionBugFix.extension" // "name.CompilerSDK.OSVersionMajor.OSVersionMinor.extension" // "name.CompilerSDK.OSVersionMajor.extension" // "name.CompilerSDK.extension" // "name.OSVersionMajor.OSVersionMinor.OSVersionBugFix.arch.extension" // "name.OSVersionMajor.OSVersionMinor.arch.extension" // "name.OSVersionMajor.arch.extension" // "name.arch.extension" // "name.OSVersionMajor.OSVersionMinor.OSVersionBugFix.extension" // "name.OSVersionMajor.OSVersionMinor.extension" // "name.OSVersionMajor.extension" // "name.extension" // Do not include the ".extension" extension on your name. // // Args: // name: The name for the file you would like to find. // extension: the extension for the file you would like to find // // Returns: // the path if the file exists in your bundle // or nil if no file is found // - (NSString *)gtm_pathForFileNamed:(NSString*)name extension:(NSString*)extension { NSString *thePath = nil; Class bundleClass = [GTMUnitTestingAdditionsBundleFinder class]; NSBundle *myBundle = [NSBundle bundleForClass:bundleClass]; _GTMDevAssert(myBundle, @"Couldn't find bundle for class: %@ searching for file:%@.%@", NSStringFromClass(bundleClass), name, extension); // System Version SInt32 major, minor, bugFix; [GTMSystemVersion getMajor:&major minor:&minor bugFix:&bugFix]; NSString *systemVersions[4]; systemVersions[0] = [NSString stringWithFormat:@".%d.%d.%d", major, minor, bugFix]; systemVersions[1] = [NSString stringWithFormat:@".%d.%d", major, minor]; systemVersions[2] = [NSString stringWithFormat:@".%d", major]; systemVersions[3] = @""; // Architecture NSString *architecture[2]; architecture[0] = [NSString stringWithFormat:@".%@", [GTMSystemVersion runtimeArchitecture]]; architecture[1] = @""; // Compiler SDK #if GTM_MACOS_SDK // Some times Apple changes how things work based on the SDK built against. NSString *sdks[2]; # if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6 sdks[0] = @".10_6_SDK"; # elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 sdks[0] = @".10_5_SDK"; # elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4 sdks[0] = @".10_4_SDK"; # elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3 sdks[0] = @".10_3_SDK"; # elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_2 sdks[0] = @".10_2_SDK"; # elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_1 sdks[0] = @".10_1_SDK"; # else sdks[0] = @".10_0_SDK"; # endif sdks[1] = @""; #else // !GTM_MACOS_SDK // iPhone doesn't current support SDK specific images (hopefully it won't // be needed. NSString *sdks[] = { @"" }; #endif // GTM_MACOS_SDK // Note that we are searching for the most exact match first. for (size_t i = 0; !thePath && i < sizeof(sdks) / sizeof(*sdks); ++i) { for (size_t j = 0; !thePath && j < sizeof(architecture) / sizeof(*architecture); j++) { for (size_t k = 0; !thePath && k < sizeof(systemVersions) / sizeof(*systemVersions); k++) { NSString *fullName = [NSString stringWithFormat:@"%@%@%@%@", name, sdks[i], systemVersions[k], architecture[j]]; thePath = [myBundle pathForResource:fullName ofType:extension]; } } } return thePath; } - (NSString *)gtm_saveToPathForFileNamed:(NSString*)name extension:(NSString*)extension { NSString *systemArchitecture = [GTMSystemVersion runtimeArchitecture]; SInt32 major, minor, bugFix; [GTMSystemVersion getMajor:&major minor:&minor bugFix:&bugFix]; // We don't include the CompilerSDK in here because it is not something that // that is commonly needed. NSString *fullName = [NSString stringWithFormat:@"%@.%d.%d.%d.%@", name, major, minor, bugFix, systemArchitecture]; NSString *basePath = [[self class] gtm_getUnitTestSaveToDirectory]; return [[basePath stringByAppendingPathComponent:fullName] stringByAppendingPathExtension:extension]; } #pragma mark UnitTestImage // Checks to see that system settings are valid for doing an image comparison. // To be overridden by subclasses. // Returns: // YES if we can do image comparisons for this object type. - (BOOL)gtm_areSystemSettingsValidForDoingImage { return YES; } - (CFStringRef)gtm_imageUTI { #if GTM_IPHONE_SDK return kUTTypePNG; #else // Currently can't use PNG on Leopard. (10.5.2) // Radar:5844618 PNG importer/exporter in ImageIO is lossy return kUTTypeTIFF; #endif } // Return the extension to be used for saving unittest images // // Returns // An extension (e.g. "png") - (NSString*)gtm_imageExtension { CFStringRef uti = [self gtm_imageUTI]; #if GTM_IPHONE_SDK if (CFEqual(uti, kUTTypePNG)) { return @"png"; } else if (CFEqual(uti, kUTTypeJPEG)) { return @"jpg"; } else { _GTMDevAssert(NO, @"Illegal UTI for iPhone"); } return nil; #else CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); _GTMDevAssert(extension, @"No extension for uti: %@", uti); return GTMCFAutorelease(extension); #endif } // Return image data in the format expected for gtm_imageExtension // So for a "png" extension I would expect "png" data // // Returns // NSData for image - (NSData*)gtm_imageDataForImage:(CGImageRef)image { NSData *data = nil; #if GTM_IPHONE_SDK // iPhone support UIImage *uiImage = [UIImage imageWithCGImage:image]; CFStringRef uti = [self gtm_imageUTI]; if (CFEqual(uti, kUTTypePNG)) { data = UIImagePNGRepresentation(uiImage); } else if (CFEqual(uti, kUTTypeJPEG)) { data = UIImageJPEGRepresentation(uiImage, 1.0f); } else { _GTMDevAssert(NO, @"Illegal UTI for iPhone"); } #else data = [NSMutableData data]; CGImageDestinationRef dest = CGImageDestinationCreateWithData((CFMutableDataRef)data, [self gtm_imageUTI], 1, NULL); // LZW Compression for TIFF NSDictionary *tiffDict = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber gtm_numberWithUnsignedInteger:NSTIFFCompressionLZW], (const NSString*)kCGImagePropertyTIFFCompression, nil]; NSDictionary *destProps = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithFloat:1.0f], (const NSString*)kCGImageDestinationLossyCompressionQuality, tiffDict, (const NSString*)kCGImagePropertyTIFFDictionary, nil]; CGImageDestinationAddImage(dest, image, (CFDictionaryRef)destProps); CGImageDestinationFinalize(dest); CFRelease(dest); #endif return data; } // Save the unitTestImage to an image file with name |name| at // ~/Desktop/|name|.extension. // // Note: When running under Pulse automation output is redirected to the // Pulse base directory. // // Args: // name: The name for the image file you would like saved. // // Returns: // YES if the file was successfully saved. // - (BOOL)gtm_saveToImageNamed:(NSString*)name { NSString *newPath = [self gtm_saveToPathForImageNamed:name]; return [self gtm_saveToImageAt:newPath]; } // Save unitTestImage of |self| to an image file at path |path|. // // Args: // name: The name for the image file you would like saved. // // Returns: // YES if the file was successfully saved. // - (BOOL)gtm_saveToImageAt:(NSString*)path { if (!path) return NO; NSData *data = [self gtm_imageRepresentation]; return [data writeToFile:path atomically:YES]; } // Generates a CGImageRef from the image at |path| // Args: // path: The path to the image. // // Returns: // A CGImageRef that you own, or nil if no image at path - (CGImageRef)gtm_imageWithContentsOfFile:(NSString*)path { CGImageRef imageRef = nil; #if GTM_IPHONE_SDK UIImage *image = [UIImage imageWithContentsOfFile:path]; if (image) { imageRef = CGImageRetain(image.CGImage); } #else CFURLRef url = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)path, kCFURLPOSIXPathStyle, NO); if (url) { CGImageSourceRef imageSource = CGImageSourceCreateWithURL(url, NULL); CFRelease(url); if (imageSource) { imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); CFRelease(imageSource); } } #endif return (CGImageRef)GTMCFAutorelease(imageRef); } /// Compares unitTestImage of |self| to the image located at |path| // // Args: // path: the path to the image file you want to compare against. // If diff is non-nil, it will contain an auto-released diff of the images. // // Returns: // YES if they are equal, NO is they are not // If diff is non-nil, it will contain an auto-released diff of the images. // - (BOOL)gtm_compareWithImageAt:(NSString*)path diffImage:(CGImageRef*)diff { BOOL answer = NO; if (diff) { *diff = nil; } CGImageRef fileRep = [self gtm_imageWithContentsOfFile:path]; _GTMDevAssert(fileRep, @"Unable to create imagerep from %@", path); CGImageRef imageRep = [self gtm_unitTestImage]; _GTMDevAssert(imageRep, @"Unable to create imagerep for %@", self); size_t fileHeight = CGImageGetHeight(fileRep); size_t fileWidth = CGImageGetWidth(fileRep); size_t imageHeight = CGImageGetHeight(imageRep); size_t imageWidth = CGImageGetWidth(imageRep); if (fileHeight == imageHeight && fileWidth == imageWidth) { // if all the sizes are equal, run through the bytes and compare // them for equality. // Do an initial fast check, if this fails and the caller wants a // diff, we'll do the slow path and create the diff. The diff path // could be optimized, but probably not necessary at this point. answer = YES; CGSize imageSize = CGSizeMake(fileWidth, fileHeight); CGRect imageRect = CGRectMake(0, 0, fileWidth, fileHeight); unsigned char *fileData; unsigned char *imageData; CGContextRef fileContext = GTMCreateUnitTestBitmapContextOfSizeWithData(imageSize, &fileData); _GTMDevAssert(fileContext, @"Unable to create filecontext"); CGContextDrawImage(fileContext, imageRect, fileRep); CGContextRef imageContext = GTMCreateUnitTestBitmapContextOfSizeWithData(imageSize, &imageData); _GTMDevAssert(imageContext, @"Unable to create imageContext"); CGContextDrawImage(imageContext, imageRect, imageRep); size_t fileBytesPerRow = CGBitmapContextGetBytesPerRow(fileContext); size_t imageBytesPerRow = CGBitmapContextGetBytesPerRow(imageContext); size_t row, col; _GTMDevAssert(imageWidth * 4 <= imageBytesPerRow, @"We expect image data to be 32bit RGBA"); for (row = 0; row < fileHeight && answer; row++) { answer = memcmp(fileData + fileBytesPerRow * row, imageData + imageBytesPerRow * row, imageWidth * 4) == 0; } if (!answer && diff) { answer = YES; unsigned char *diffData; CGContextRef diffContext = GTMCreateUnitTestBitmapContextOfSizeWithData(imageSize, &diffData); _GTMDevAssert(diffContext, @"Can't make diff context"); size_t diffRowBytes = CGBitmapContextGetBytesPerRow(diffContext); for (row = 0; row < imageHeight; row++) { uint32_t *imageRow = (uint32_t*)(imageData + imageBytesPerRow * row); uint32_t *fileRow = (uint32_t*)(fileData + fileBytesPerRow * row); uint32_t* diffRow = (uint32_t*)(diffData + diffRowBytes * row); for (col = 0; col < imageWidth; col++) { uint32_t imageColor = imageRow[col]; uint32_t fileColor = fileRow[col]; unsigned char imageAlpha = imageColor & 0xF; unsigned char imageBlue = imageColor >> 8 & 0xF; unsigned char imageGreen = imageColor >> 16 & 0xF; unsigned char imageRed = imageColor >> 24 & 0xF; unsigned char fileAlpha = fileColor & 0xF; unsigned char fileBlue = fileColor >> 8 & 0xF; unsigned char fileGreen = fileColor >> 16 & 0xF; unsigned char fileRed = fileColor >> 24 & 0xF; // Check to see if color is almost right. // No matter how hard I've tried, I've still gotten occasionally // screwed over by colorspaces not mapping correctly, and small // sampling errors coming in. This appears to work for most cases. // Almost equal is defined to check within 1% on all components. BOOL equal = almostEqual(imageRed, fileRed) && almostEqual(imageGreen, fileGreen) && almostEqual(imageBlue, fileBlue) && almostEqual(imageAlpha, fileAlpha); answer &= equal; if (diff) { uint32_t newColor; if (equal) { newColor = (((uint32_t)imageRed) << 24) + (((uint32_t)imageGreen) << 16) + (((uint32_t)imageBlue) << 8) + (((uint32_t)imageAlpha) / 2); } else { newColor = 0xFF0000FF; } diffRow[col] = newColor; } } } *diff = CGBitmapContextCreateImage(diffContext); free(diffData); CFRelease(diffContext); } free(fileData); CFRelease(fileContext); free(imageData); CFRelease(imageContext); } return answer; } // Find the path for an image by name in your bundle. // Do not include the extension on your name. // // Args: // name: The name for the image file you would like to find. // // Returns: // the path if the image exists in your bundle // or nil if no image to be found // - (NSString *)gtm_pathForImageNamed:(NSString*)name { return [self gtm_pathForFileNamed:name extension:[self gtm_imageExtension]]; } - (NSString *)gtm_saveToPathForImageNamed:(NSString*)name { return [self gtm_saveToPathForFileNamed:name extension:[self gtm_imageExtension]]; } // Gives us a representation of unitTestImage of |self|. // // Returns: // a representation of image if successful // nil if failed // - (NSData *)gtm_imageRepresentation { CGImageRef imageRep = [self gtm_unitTestImage]; NSData *data = [self gtm_imageDataForImage:imageRep]; _GTMDevAssert(data, @"unable to create %@ from %@", [self gtm_imageExtension], self); return data; } #pragma mark UnitTestState // Return the extension to be used for saving unittest states // // Returns // An extension (e.g. "gtmUTState") - (NSString*)gtm_stateExtension { return @"gtmUTState"; } // Save the encoded unit test state to a state file with name |name| at // ~/Desktop/|name|.extension. // // Note: When running under Pulse automation output is redirected to the // Pulse base directory. // // Args: // name: The name for the state file you would like saved. // // Returns: // YES if the file was successfully saved. // - (BOOL)gtm_saveToStateNamed:(NSString*)name { NSString *newPath = [self gtm_saveToPathForStateNamed:name]; return [self gtm_saveToStateAt:newPath]; } // Save encoded unit test state of |self| to a state file at path |path|. // // Args: // name: The name for the state file you would like saved. // // Returns: // YES if the file was successfully saved. // - (BOOL)gtm_saveToStateAt:(NSString*)path { if (!path) return NO; NSDictionary *dictionary = [self gtm_stateRepresentation]; return [dictionary writeToFile:path atomically:YES]; } // Compares encoded unit test state of |self| to the state file located at // |path| // // Args: // path: the path to the state file you want to compare against. // // Returns: // YES if they are equal, NO is they are not // - (BOOL)gtm_compareWithStateAt:(NSString*)path { NSDictionary *masterDict = [NSDictionary dictionaryWithContentsOfFile:path]; _GTMDevAssert(masterDict, @"Unable to create dictionary from %@", path); NSDictionary *selfDict = [self gtm_stateRepresentation]; return [selfDict isEqual: masterDict]; } // Find the path for a state by name in your bundle. // Do not include the extension. // // Args: // name: The name for the state file you would like to find. // // Returns: // the path if the state exists in your bundle // or nil if no state to be found // - (NSString *)gtm_pathForStateNamed:(NSString*)name { return [self gtm_pathForFileNamed:name extension:[self gtm_stateExtension]]; } - (NSString *)gtm_saveToPathForStateNamed:(NSString*)name { return [self gtm_saveToPathForFileNamed:name extension:[self gtm_stateExtension]]; } // Gives us the encoded unit test state |self| // // Returns: // the encoded state if successful // nil if failed // - (NSDictionary *)gtm_stateRepresentation { NSDictionary *dictionary = nil; if ([self conformsToProtocol:@protocol(GTMUnitTestingEncoding)]) { id encoder = (id)self; GTMUnitTestingKeyedCoder *archiver; archiver = [[[GTMUnitTestingKeyedCoder alloc] init] autorelease]; [encoder gtm_unitTestEncodeState:archiver]; dictionary = [archiver dictionary]; } return dictionary; } // Encodes the state of an object in a manner suitable for comparing // against a master state file so we can determine whether the // object is in a suitable state. Encode data in the coder in the same // manner that you would encode data in any other Keyed NSCoder subclass. // // Arguments: // inCoder - the coder to encode our state into - (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { // All impls of gtm_unitTestEncodeState // should be calling [super gtm_unitTestEncodeState] as their first action. _GTMDevAssert([inCoder isKindOfClass:[GTMUnitTestingKeyedCoder class]], @"Coder must be of kind GTMUnitTestingKeyedCoder"); // If the object has a delegate, give it a chance to respond if ([self respondsToSelector:@selector(delegate)]) { id delegate = [self performSelector:@selector(delegate)]; if (delegate && [delegate respondsToSelector:@selector(gtm_unitTestEncoderWillEncode:inCoder:)]) { [delegate gtm_unitTestEncoderWillEncode:self inCoder:inCoder]; } } } @end