PageRenderTime 178ms CodeModel.GetById 12ms app.highlight 156ms RepoModel.GetById 1ms app.codeStats 0ms

/MapView/GTM/GTMNSObject+UnitTesting.m

http://github.com/route-me/route-me
Objective C | 1014 lines | 651 code | 81 blank | 282 comment | 93 complexity | c3caeb0cf00188c3020effc183e260b2 MD5 | raw file
   1//
   2//  GTMNSObject+UnitTesting.m
   3//  
   4//  An informal protocol for doing advanced unittesting with objects.
   5//
   6//  Copyright 2006-2008 Google Inc.
   7//
   8//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
   9//  use this file except in compliance with the License.  You may obtain a copy
  10//  of the License at
  11// 
  12//  http://www.apache.org/licenses/LICENSE-2.0
  13// 
  14//  Unless required by applicable law or agreed to in writing, software
  15//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  16//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
  17//  License for the specific language governing permissions and limitations under
  18//  the License.
  19//
  20
  21#import "GTMNSObject+UnitTesting.h"
  22#import "GTMSystemVersion.h"
  23#import "GTMGarbageCollection.h"
  24#import "GTMNSNumber+64Bit.h"
  25
  26#if GTM_IPHONE_SDK
  27#import <UIKit/UIKit.h>
  28#else
  29#import <AppKit/AppKit.h>
  30#endif
  31
  32NSString *const GTMUnitTestingEncodedObjectNotification 
  33  = @"GTMUnitTestingEncodedObjectNotification";
  34NSString *const GTMUnitTestingEncoderKey = @"GTMUnitTestingEncoderKey";
  35
  36#if GTM_IPHONE_SDK
  37// No UTIs on iPhone. Only two we need.
  38const CFStringRef kUTTypePNG = CFSTR("public.png");
  39const CFStringRef kUTTypeJPEG = CFSTR("public.jpeg");
  40#endif
  41
  42// This class exists so that we can locate our bundle using [NSBundle
  43// bundleForClass:]. We don't use [NSBundle mainBundle] because when we are
  44// being run as a unit test, we aren't the mainBundle
  45@interface GTMUnitTestingAdditionsBundleFinder : NSObject {
  46  // Nothing here
  47}
  48// or here
  49@end
  50
  51@implementation GTMUnitTestingAdditionsBundleFinder 
  52// Nothing here. We're just interested in the name for finding our bundle.
  53@end
  54
  55BOOL GTMIsObjectImageEqualToImageNamed(id object, 
  56                                       NSString* filename, 
  57                                       NSString **error) {
  58  NSString *failString = nil;
  59  if (error) {
  60    *error = nil;
  61  }
  62  BOOL isGood = [object respondsToSelector:@selector(gtm_unitTestImage)];
  63  if (isGood) {
  64    if ([object gtm_areSystemSettingsValidForDoingImage]) {
  65      NSString *aPath = [object gtm_pathForImageNamed:filename];
  66      CGImageRef diff = nil;
  67      isGood = aPath != nil;
  68      if (isGood) {
  69        isGood = [object gtm_compareWithImageAt:aPath diffImage:&diff];
  70      }
  71      if (!isGood) {
  72        if (aPath) {
  73          filename = [filename stringByAppendingString:@"_Failed"];
  74        }
  75        BOOL aSaved = [object gtm_saveToImageNamed:filename];
  76        NSString *fileNameWithExtension 
  77          = [NSString stringWithFormat:@"%@.%@",
  78             filename, [object gtm_imageExtension]];
  79        NSString *fullSavePath = [object gtm_saveToPathForImageNamed:filename];
  80        if (NO == aSaved) {
  81          if (!aPath) {
  82            failString = [NSString stringWithFormat:@"File %@ did not exist in "
  83                          @"bundle. Tried to save as %@ and failed.", 
  84                          fileNameWithExtension, fullSavePath];
  85          } else {
  86            failString = [NSString stringWithFormat:@"Object image different "
  87                          @"than file %@. Tried to save as %@ and failed.", 
  88                          aPath, fullSavePath];
  89          }
  90        } else {
  91          if (!aPath) {
  92            failString = [NSString stringWithFormat:@"File %@ did not exist in "
  93                          @" bundle. Saved to %@", fileNameWithExtension, 
  94                          fullSavePath];
  95          } else {
  96            NSString *diffPath = [filename stringByAppendingString:@"_Diff"];
  97            diffPath = [object gtm_saveToPathForImageNamed:diffPath];
  98            NSData *data = nil;
  99            if (diff) {
 100              data = [object gtm_imageDataForImage:diff];
 101            }
 102            if ([data writeToFile:diffPath atomically:YES]) {
 103              failString = [NSString stringWithFormat:@"Object image different "
 104                            @"than file %@. Saved image to %@. "
 105                            @"Saved diff to %@", 
 106                            aPath, fullSavePath, diffPath];
 107            } else {
 108              failString = [NSString stringWithFormat:@"Object image different "
 109                            @"than file %@. Saved image to %@. Unable to save "
 110                            @"diff. Most likely the image and diff are "
 111                            @"different sizes.", 
 112                            aPath, fullSavePath];
 113            }
 114          }
 115        }
 116      }
 117      CGImageRelease(diff);
 118    } else {
 119      failString = @"systemSettings not valid for taking image";  // COV_NF_LINE
 120    }
 121  } else {
 122    failString = @"Object does not conform to GTMUnitTestingImaging protocol";
 123  }
 124  if (error) {
 125    *error = failString;
 126  }
 127  return isGood;
 128}
 129
 130BOOL GTMIsObjectStateEqualToStateNamed(id object, 
 131                                       NSString* filename, 
 132                                       NSString **error) {
 133  NSString *failString = nil;
 134  if (error) {
 135    *error = nil;
 136  }
 137  BOOL isGood = [object conformsToProtocol:@protocol(GTMUnitTestingEncoding)];
 138  if (isGood) {
 139    NSString *aPath = [object gtm_pathForStateNamed:filename];
 140    isGood = aPath != nil;
 141    if (isGood) {
 142      isGood = [object gtm_compareWithStateAt:aPath];
 143    }
 144    if (!isGood) {
 145      if (aPath) {
 146        filename = [filename stringByAppendingString:@"_Failed"];
 147      }
 148      BOOL aSaved = [object gtm_saveToStateNamed:filename];
 149      NSString *fileNameWithExtension = [NSString stringWithFormat:@"%@.%@",
 150                                         filename, [object gtm_stateExtension]];
 151      NSString *fullSavePath = [object gtm_saveToPathForStateNamed:filename];
 152      if (NO == aSaved) {
 153        if (!aPath) {
 154          failString = [NSString stringWithFormat:@"File %@ did not exist in "
 155                        @"bundle. Tried to save as %@ and failed.", 
 156                        fileNameWithExtension, fullSavePath];
 157        } else {
 158          failString = [NSString stringWithFormat:@"Object state different "
 159                        @"than file %@. Tried to save as %@ and failed.", 
 160                        aPath, fullSavePath];
 161        }
 162      } else {
 163        if (!aPath) {
 164          failString = [NSString stringWithFormat:@"File %@ did not exist in "
 165                       @ "bundle. Saved to %@", fileNameWithExtension, 
 166                        fullSavePath];
 167        } else {
 168          failString = [NSString stringWithFormat:@"Object state different "
 169                        @"than file %@. Saved to %@", aPath, fullSavePath];
 170        }
 171      }
 172    }
 173  } else {
 174    failString = @"Object does not conform to GTMUnitTestingEncoding protocol"; 
 175  }
 176  if (error) {
 177    *error = failString;
 178  }
 179  return isGood;
 180}
 181
 182CGContextRef GTMCreateUnitTestBitmapContextOfSizeWithData(CGSize size,
 183                                                          unsigned char **data) {
 184  CGContextRef context = NULL;
 185  size_t height = size.height;
 186  size_t width = size.width;
 187  size_t bytesPerRow = width * 4;
 188  size_t bitsPerComponent = 8;
 189  CGColorSpaceRef cs = NULL;
 190#if GTM_IPHONE_SDK
 191  cs = CGColorSpaceCreateDeviceRGB();
 192#else
 193  cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
 194#endif
 195  _GTMDevAssert(cs, @"Couldn't create colorspace");
 196  CGBitmapInfo info 
 197    = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault;
 198  if (data) {
 199    *data = (unsigned char*)calloc(bytesPerRow, height);
 200    _GTMDevAssert(*data, @"Couldn't create bitmap");
 201  }
 202  context = CGBitmapContextCreate(data ? *data : NULL, width, height, 
 203                                  bitsPerComponent, bytesPerRow, cs, info);
 204  _GTMDevAssert(context, @"Couldn't create an context");
 205  if (!data) {
 206    CGContextClearRect(context, CGRectMake(0, 0, size.width, size.height));
 207  }
 208  CGContextSetRenderingIntent(context, kCGRenderingIntentRelativeColorimetric);
 209  CGContextSetInterpolationQuality(context, kCGInterpolationNone);
 210  CGContextSetShouldAntialias(context, NO);
 211  CGContextSetAllowsAntialiasing(context, NO);
 212  CGContextSetShouldSmoothFonts(context, NO);
 213  CGColorSpaceRelease(cs);
 214  return context;  
 215}
 216
 217@interface NSObject (GTMUnitTestingAdditionsPrivate)
 218///  Find the path for a file named name.extension in your bundle.
 219//  Searches for the following:
 220//  "name.extension", 
 221//  "name.arch.extension", 
 222//  "name.arch.OSVersionMajor.extension"
 223//  "name.arch.OSVersionMajor.OSVersionMinor.extension"
 224//  "name.arch.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.extension"
 225//  "name.arch.OSVersionMajor.extension"
 226//  "name.OSVersionMajor.arch.extension"
 227//  "name.OSVersionMajor.OSVersionMinor.arch.extension"
 228//  "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.arch.extension"
 229//  "name.OSVersionMajor.extension"
 230//  "name.OSVersionMajor.OSVersionMinor.extension"
 231//  "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.extension"
 232//  Do not include the ".extension" extension on your name.
 233//
 234//  Args:
 235//    name: The name for the file you would like to find.
 236//    extension: the extension for the file you would like to find
 237//
 238//  Returns:
 239//    the path if the file exists in your bundle
 240//    or nil if no file is found
 241//
 242- (NSString *)gtm_pathForFileNamed:(NSString*)name 
 243                         extension:(NSString*)extension;
 244- (NSString *)gtm_saveToPathForFileNamed:(NSString*)name 
 245                               extension:(NSString*)extension;
 246- (CGImageRef)gtm_unitTestImage;
 247// Returns nil if there is no override
 248- (NSString *)gtm_getOverrideDefaultUnitTestSaveToDirectory;
 249@end
 250
 251// This is a keyed coder for storing unit test state data. It is used only by
 252// the GTMUnitTestingAdditions category. Most of the work is done in
 253// encodeObject:forKey:.
 254@interface GTMUnitTestingKeyedCoder : NSCoder {
 255  NSMutableDictionary *dictionary_; // storage for data (STRONG)
 256}
 257
 258//  get the data stored in coder.
 259//
 260//  Returns:
 261//    NSDictionary with currently stored data.
 262- (NSDictionary*)dictionary; 
 263@end
 264
 265// Small utility function for checking to see if a is b +/- 1.
 266GTM_INLINE BOOL almostEqual(unsigned char a, unsigned char b) {
 267  unsigned char diff = a > b ? a - b : b - a;
 268  BOOL notEqual = diff < 2;
 269  return notEqual;
 270}
 271
 272@implementation GTMUnitTestingKeyedCoder
 273
 274//  Set up storage for coder. Stores type and version.
 275//  Version 1
 276// 
 277//  Returns:
 278//    self
 279- (id)init {
 280  self = [super init];
 281  if (self != nil) {
 282    dictionary_ = [[NSMutableDictionary alloc] initWithCapacity:2];
 283    [dictionary_ setObject:@"GTMUnitTestingArchive" forKey:@"$GTMArchive"];
 284    
 285    // Version number can be changed here.
 286    [dictionary_ setObject:[NSNumber numberWithInt:1] forKey:@"$GTMVersion"];
 287  }
 288  return self;
 289}
 290
 291// Standard dealloc
 292- (void)dealloc {
 293  [dictionary_ release];
 294  [super dealloc];
 295}
 296
 297// Utility function for checking for a key value. We don't want duplicate keys
 298// in any of our dictionaries as we may be writing over data stored by previous
 299// objects.
 300//
 301//  Arguments:
 302//    key - key to check for in dictionary
 303- (void)checkForKey:(NSString*)key {
 304  _GTMDevAssert(![dictionary_ objectForKey:key], 
 305                @"Key already exists for %@", key);
 306}
 307
 308// Key routine for the encoder. We store objects in our dictionary based on
 309// their key. As we encode objects we send out notifications to let other
 310// classes doing tests add their specific data to the base types. If we can't
 311// encode the object (it doesn't support gtm_unitTestEncodeState) and we don't 
 312// get any info back from the notifier, we attempt to store it's description.
 313//
 314//  Arguments:
 315//    objv - object to be encoded
 316//    key - key to encode it with
 317//
 318- (void)encodeObject:(id)objv forKey:(NSString *)key {
 319  // Sanity checks
 320  if (!objv) return;
 321  [self checkForKey:key];
 322  
 323  // Set up a new dictionary for the current object
 324  NSMutableDictionary *curDictionary = dictionary_;
 325  dictionary_ = [[NSMutableDictionary alloc] initWithCapacity:0];
 326  
 327  // If objv responds to gtm_unitTestEncodeState get it to record
 328  // its data.
 329  if ([objv respondsToSelector:@selector(gtm_unitTestEncodeState:)]) {
 330    [objv gtm_unitTestEncodeState:self];
 331  }
 332  
 333  // We then send out a notification to let other folks
 334  // add data for this object
 335  NSDictionary *notificationDict 
 336    = [NSDictionary dictionaryWithObject:self forKey:GTMUnitTestingEncoderKey];
 337  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
 338  [nc postNotificationName:GTMUnitTestingEncodedObjectNotification
 339                    object:objv
 340                  userInfo:notificationDict];
 341  
 342  // If we got anything from the object, or from the notification, store it in
 343  // our dictionary. Otherwise store the description.
 344  if ([dictionary_ count] > 0) {
 345    [curDictionary setObject:dictionary_ forKey:key];
 346  } else {
 347    NSString *description = [objv description];
 348    // If description has a pointer value in it, we don't want to store it
 349    // as the pointer value can change from run to run
 350    if (description && [description rangeOfString:@"0x"].length == 0) {
 351      [curDictionary setObject:description forKey:key];
 352    } else {
 353      _GTMDevAssert(NO, @"Unable to encode forKey: %@", key);  // COV_NF_LINE
 354    }
 355  }
 356  [dictionary_ release];
 357  dictionary_ = curDictionary;
 358}
 359
 360//  Basic encoding methods for POD types.
 361//
 362//  Arguments:
 363//    *v - value to encode
 364//    key - key to encode it in
 365
 366- (void)encodeBool:(BOOL)boolv forKey:(NSString *)key {
 367  [self checkForKey:key];
 368  [dictionary_ setObject:[NSNumber numberWithBool:boolv] forKey:key];
 369}
 370
 371- (void)encodeInt:(int)intv forKey:(NSString *)key {
 372  [self checkForKey:key];
 373  [dictionary_ setObject:[NSNumber numberWithInt:intv] forKey:key];
 374}
 375
 376- (void)encodeInt32:(int32_t)intv forKey:(NSString *)key {
 377  [self checkForKey:key];
 378  [dictionary_ setObject:[NSNumber numberWithLong:intv] forKey:key];
 379}
 380
 381- (void)encodeInt64:(int64_t)intv forKey:(NSString *)key {
 382  [self checkForKey:key];
 383  [dictionary_ setObject:[NSNumber numberWithLongLong:intv] forKey:key];
 384}
 385
 386- (void)encodeFloat:(float)realv forKey:(NSString *)key {
 387  [self checkForKey:key];
 388  [dictionary_ setObject:[NSNumber numberWithFloat:realv] forKey:key];
 389}
 390
 391- (void)encodeDouble:(double)realv forKey:(NSString *)key {
 392  [self checkForKey:key];
 393  [dictionary_ setObject:[NSNumber numberWithDouble:realv] forKey:key];
 394}
 395
 396- (void)encodeBytes:(const uint8_t *)bytesp 
 397             length:(unsigned)lenv 
 398             forKey:(NSString *)key {
 399  [self checkForKey:key];
 400  [dictionary_ setObject:[NSData dataWithBytes:bytesp 
 401                                        length:lenv] 
 402                  forKey:key];
 403}
 404
 405//  Get our storage back as an NSDictionary
 406//  
 407//  Returns:
 408//    NSDictionary containing our encoded info
 409-(NSDictionary*)dictionary {
 410  return [[dictionary_ retain] autorelease];
 411}
 412
 413@end
 414
 415static NSString *gGTMUnitTestSaveToDirectory = nil;
 416
 417@implementation NSObject (GTMUnitTestingAdditions)
 418
 419+ (void)gtm_setUnitTestSaveToDirectory:(NSString*)path {
 420  @synchronized([self class]) {
 421    [gGTMUnitTestSaveToDirectory autorelease];
 422    gGTMUnitTestSaveToDirectory = [path copy];
 423  }
 424}
 425
 426+ (NSString *)gtm_getUnitTestSaveToDirectory {
 427  NSString *result = nil;
 428  @synchronized([self class]) {
 429    if (!gGTMUnitTestSaveToDirectory) {
 430#if GTM_IPHONE_SDK
 431      // Developer build, use their home directory Desktop.
 432      gGTMUnitTestSaveToDirectory 
 433        = [[[[[NSHomeDirectory() stringByDeletingLastPathComponent] 
 434              stringByDeletingLastPathComponent] 
 435             stringByDeletingLastPathComponent] 
 436            stringByDeletingLastPathComponent] 
 437           stringByAppendingPathComponent:@"Desktop"];
 438#else
 439      NSArray *desktopDirs 
 440        = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, 
 441                                              NSUserDomainMask,
 442                                              YES);
 443      gGTMUnitTestSaveToDirectory = [desktopDirs objectAtIndex:0];
 444#endif    
 445      // Did we get overridden?
 446      NSString *override = [self gtm_getOverrideDefaultUnitTestSaveToDirectory];
 447      if (override) {
 448        gGTMUnitTestSaveToDirectory = override;
 449      }
 450      [gGTMUnitTestSaveToDirectory retain];
 451    }
 452    result = gGTMUnitTestSaveToDirectory;
 453  }
 454  
 455  return result;
 456}
 457
 458// Return nil if there is no override
 459- (NSString *)gtm_getOverrideDefaultUnitTestSaveToDirectory {
 460  NSString *result = nil;
 461
 462  // If we have an environment variable that ends in "BUILD_NUMBER" odds are
 463  // we're on an automated build system, so use the build products dir as an
 464  // override instead of writing on the desktop.
 465  NSDictionary *env = [[NSProcessInfo processInfo] environment];
 466  NSString *key;
 467  GTM_FOREACH_KEY(key, env) {
 468    if ([key hasSuffix:@"BUILD_NUMBER"]) {
 469      break;
 470    }
 471  }
 472  if (key) {
 473    result = [env objectForKey:@"BUILT_PRODUCTS_DIR"];
 474  }
 475  
 476  if (result && [result length] == 0) {
 477    result = nil;
 478  }
 479  return result;
 480}
 481
 482///  Find the path for a file named name.extension in your bundle.
 483//  Searches for the following:
 484//  "name.CompilerSDK.OSVersionMajor.OSVersionMinor.OSVersionBugFix.arch.extension"
 485//  "name.CompilerSDK.OSVersionMajor.OSVersionMinor.arch.extension"
 486//  "name.CompilerSDK.OSVersionMajor.arch.extension"
 487//  "name.CompilerSDK.arch.extension"
 488//  "name.CompilerSDK.OSVersionMajor.OSVersionMinor.OSVersionBugFix.extension"
 489//  "name.CompilerSDK.OSVersionMajor.OSVersionMinor.extension"
 490//  "name.CompilerSDK.OSVersionMajor.extension"
 491//  "name.CompilerSDK.extension"
 492//  "name.OSVersionMajor.OSVersionMinor.OSVersionBugFix.arch.extension"
 493//  "name.OSVersionMajor.OSVersionMinor.arch.extension"
 494//  "name.OSVersionMajor.arch.extension"
 495//  "name.arch.extension"
 496//  "name.OSVersionMajor.OSVersionMinor.OSVersionBugFix.extension"
 497//  "name.OSVersionMajor.OSVersionMinor.extension"
 498//  "name.OSVersionMajor.extension"
 499//  "name.extension"
 500//  Do not include the ".extension" extension on your name.
 501//
 502//  Args:
 503//    name: The name for the file you would like to find.
 504//    extension: the extension for the file you would like to find
 505//
 506//  Returns:
 507//    the path if the file exists in your bundle
 508//    or nil if no file is found
 509//
 510- (NSString *)gtm_pathForFileNamed:(NSString*)name 
 511                         extension:(NSString*)extension {
 512  NSString *thePath = nil;
 513  Class bundleClass = [GTMUnitTestingAdditionsBundleFinder class];
 514  NSBundle *myBundle = [NSBundle bundleForClass:bundleClass];
 515  _GTMDevAssert(myBundle, 
 516                @"Couldn't find bundle for class: %@ searching for file:%@.%@", 
 517                NSStringFromClass(bundleClass), name, extension);
 518  // System Version
 519  SInt32 major, minor, bugFix;
 520  [GTMSystemVersion getMajor:&major minor:&minor bugFix:&bugFix];
 521  NSString *systemVersions[4];
 522  systemVersions[0] = [NSString stringWithFormat:@".%d.%d.%d", 
 523                       major, minor, bugFix];
 524  systemVersions[1] = [NSString stringWithFormat:@".%d.%d", major, minor];
 525  systemVersions[2] = [NSString stringWithFormat:@".%d", major];
 526  systemVersions[3] = @"";
 527  // Architecture
 528  NSString *architecture[2];
 529  architecture[0] 
 530    = [NSString stringWithFormat:@".%@", 
 531       [GTMSystemVersion runtimeArchitecture]];
 532  architecture[1] = @"";
 533  // Compiler SDK
 534#if GTM_MACOS_SDK
 535  // Some times Apple changes how things work based on the SDK built against.
 536  NSString *sdks[2];
 537# if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6
 538  sdks[0] = @".10_6_SDK";
 539# elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
 540  sdks[0] = @".10_5_SDK";
 541# elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4
 542  sdks[0] = @".10_4_SDK";
 543# elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3
 544  sdks[0] = @".10_3_SDK";
 545# elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_2
 546  sdks[0] = @".10_2_SDK";
 547# elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_1
 548  sdks[0] = @".10_1_SDK";
 549# else
 550  sdks[0] = @".10_0_SDK";
 551# endif
 552  sdks[1] = @"";
 553#else  // !GTM_MACOS_SDK
 554  // iPhone doesn't current support SDK specific images (hopefully it won't
 555  // be needed.
 556  NSString *sdks[] = { @"" };
 557#endif  // GTM_MACOS_SDK
 558  
 559  // Note that we are searching for the most exact match first.
 560  for (size_t i = 0; 
 561       !thePath && i < sizeof(sdks) / sizeof(*sdks); 
 562       ++i) {
 563    for (size_t j = 0; 
 564         !thePath && j < sizeof(architecture) / sizeof(*architecture); 
 565         j++) {
 566      for (size_t k = 0;
 567           !thePath && k < sizeof(systemVersions) / sizeof(*systemVersions); 
 568           k++) {
 569        NSString *fullName = [NSString stringWithFormat:@"%@%@%@%@", 
 570                    name, sdks[i], systemVersions[k], architecture[j]];
 571        thePath = [myBundle pathForResource:fullName ofType:extension];
 572      }
 573    }
 574  }
 575  
 576  return thePath;
 577}  
 578
 579- (NSString *)gtm_saveToPathForFileNamed:(NSString*)name 
 580                               extension:(NSString*)extension {  
 581  NSString *systemArchitecture = [GTMSystemVersion runtimeArchitecture];
 582  
 583  SInt32 major, minor, bugFix;
 584  [GTMSystemVersion getMajor:&major minor:&minor bugFix:&bugFix];
 585  
 586  // We don't include the CompilerSDK in here because it is not something that
 587  // that is commonly needed.
 588  NSString *fullName = [NSString stringWithFormat:@"%@.%d.%d.%d.%@", 
 589                        name, major, minor, bugFix, systemArchitecture];
 590  
 591  NSString *basePath = [[self class] gtm_getUnitTestSaveToDirectory];
 592  return [[basePath stringByAppendingPathComponent:fullName]
 593          stringByAppendingPathExtension:extension];
 594}
 595
 596#pragma mark UnitTestImage
 597
 598// Checks to see that system settings are valid for doing an image comparison.
 599// To be overridden by subclasses.
 600// Returns:
 601//  YES if we can do image comparisons for this object type.
 602- (BOOL)gtm_areSystemSettingsValidForDoingImage {
 603  return YES;
 604}
 605
 606- (CFStringRef)gtm_imageUTI {
 607#if GTM_IPHONE_SDK
 608  return kUTTypePNG;
 609#else
 610  // Currently can't use PNG on Leopard. (10.5.2)
 611  // Radar:5844618 PNG importer/exporter in ImageIO is lossy
 612  return kUTTypeTIFF;
 613#endif
 614}
 615
 616// Return the extension to be used for saving unittest images
 617//
 618// Returns
 619//  An extension (e.g. "png")
 620- (NSString*)gtm_imageExtension {
 621  CFStringRef uti = [self gtm_imageUTI];
 622#if GTM_IPHONE_SDK
 623  if (CFEqual(uti, kUTTypePNG)) {
 624    return @"png";
 625  } else if (CFEqual(uti, kUTTypeJPEG)) {
 626    return @"jpg";
 627  } else {
 628    _GTMDevAssert(NO, @"Illegal UTI for iPhone");
 629  }
 630  return nil;
 631#else
 632  CFStringRef extension 
 633    = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
 634  _GTMDevAssert(extension, @"No extension for uti: %@", uti);
 635  
 636  return GTMCFAutorelease(extension);
 637#endif
 638}
 639
 640// Return image data in the format expected for gtm_imageExtension
 641// So for a "png" extension I would expect "png" data
 642//
 643// Returns
 644//  NSData for image
 645- (NSData*)gtm_imageDataForImage:(CGImageRef)image {
 646  NSData *data = nil;
 647#if GTM_IPHONE_SDK
 648  // iPhone support
 649  UIImage *uiImage = [UIImage imageWithCGImage:image];
 650  CFStringRef uti = [self gtm_imageUTI];
 651  if (CFEqual(uti, kUTTypePNG)) {
 652    data = UIImagePNGRepresentation(uiImage);
 653  } else if (CFEqual(uti, kUTTypeJPEG)) {
 654    data = UIImageJPEGRepresentation(uiImage, 1.0f);
 655  } else {
 656    _GTMDevAssert(NO, @"Illegal UTI for iPhone");
 657  }
 658#else
 659  data = [NSMutableData data];
 660  CGImageDestinationRef dest 
 661    = CGImageDestinationCreateWithData((CFMutableDataRef)data,
 662                                       [self gtm_imageUTI],
 663                                       1,
 664                                       NULL);
 665  // LZW Compression for TIFF
 666  NSDictionary *tiffDict 
 667    = [NSDictionary dictionaryWithObjectsAndKeys:
 668       [NSNumber gtm_numberWithUnsignedInteger:NSTIFFCompressionLZW],
 669       (const NSString*)kCGImagePropertyTIFFCompression,
 670       nil];
 671  NSDictionary *destProps 
 672    = [NSDictionary dictionaryWithObjectsAndKeys:
 673       [NSNumber numberWithFloat:1.0f], 
 674       (const NSString*)kCGImageDestinationLossyCompressionQuality,
 675       tiffDict,
 676       (const NSString*)kCGImagePropertyTIFFDictionary,
 677       nil];
 678  CGImageDestinationAddImage(dest, image, (CFDictionaryRef)destProps);
 679  CGImageDestinationFinalize(dest);
 680  CFRelease(dest);
 681#endif
 682  return data;
 683  
 684}
 685
 686// Save the unitTestImage to an image file with name |name| at
 687// ~/Desktop/|name|.extension. 
 688//
 689//  Note: When running under Pulse automation output is redirected to the
 690//  Pulse base directory.
 691//
 692//  Args:
 693//    name: The name for the image file you would like saved.
 694//
 695//  Returns:
 696//    YES if the file was successfully saved.
 697//
 698- (BOOL)gtm_saveToImageNamed:(NSString*)name {
 699  NSString *newPath = [self gtm_saveToPathForImageNamed:name];
 700  return [self gtm_saveToImageAt:newPath];
 701}
 702
 703//  Save unitTestImage of |self| to an image file at path |path|.
 704//
 705//  Args:
 706//    name: The name for the image file you would like saved.
 707//
 708//  Returns:
 709//    YES if the file was successfully saved.
 710//
 711- (BOOL)gtm_saveToImageAt:(NSString*)path {
 712  if (!path) return NO;
 713  NSData *data = [self gtm_imageRepresentation];
 714  return [data writeToFile:path atomically:YES];
 715}
 716
 717// Generates a CGImageRef from the image at |path|
 718// Args:
 719//  path: The path to the image.
 720//
 721// Returns:
 722//  A CGImageRef that you own, or nil if no image at path
 723- (CGImageRef)gtm_imageWithContentsOfFile:(NSString*)path {
 724  CGImageRef imageRef = nil;
 725#if GTM_IPHONE_SDK
 726  UIImage *image = [UIImage imageWithContentsOfFile:path];
 727  if (image) {
 728    imageRef = CGImageRetain(image.CGImage);
 729  }
 730#else
 731  CFURLRef url = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)path, 
 732                                               kCFURLPOSIXPathStyle, NO);
 733  if (url) {
 734    CGImageSourceRef imageSource = CGImageSourceCreateWithURL(url, NULL);
 735    CFRelease(url);
 736    if (imageSource) {
 737      imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
 738      CFRelease(imageSource);
 739    }
 740  }
 741#endif
 742  return (CGImageRef)GTMCFAutorelease(imageRef);
 743}
 744
 745///  Compares unitTestImage of |self| to the image located at |path|
 746//
 747//  Args:
 748//    path: the path to the image file you want to compare against.
 749//    If diff is non-nil, it will contain an auto-released diff of the images.
 750//
 751//  Returns:
 752//    YES if they are equal, NO is they are not
 753//    If diff is non-nil, it will contain an auto-released diff of the images.
 754//
 755- (BOOL)gtm_compareWithImageAt:(NSString*)path diffImage:(CGImageRef*)diff {
 756  BOOL answer = NO;
 757  if (diff) {
 758    *diff = nil;
 759  }
 760  CGImageRef fileRep = [self gtm_imageWithContentsOfFile:path];
 761  _GTMDevAssert(fileRep, @"Unable to create imagerep from %@", path);
 762  
 763  CGImageRef imageRep = [self gtm_unitTestImage];
 764  _GTMDevAssert(imageRep, @"Unable to create imagerep for %@", self);
 765
 766  size_t fileHeight = CGImageGetHeight(fileRep);
 767  size_t fileWidth = CGImageGetWidth(fileRep);
 768  size_t imageHeight = CGImageGetHeight(imageRep);
 769  size_t imageWidth = CGImageGetWidth(imageRep);
 770  if (fileHeight == imageHeight && fileWidth == imageWidth) {
 771    // if all the sizes are equal, run through the bytes and compare
 772    // them for equality.
 773    // Do an initial fast check, if this fails and the caller wants a 
 774    // diff, we'll do the slow path and create the diff. The diff path
 775    // could be optimized, but probably not necessary at this point.
 776    answer = YES;
 777    
 778    CGSize imageSize = CGSizeMake(fileWidth, fileHeight);
 779    CGRect imageRect = CGRectMake(0, 0, fileWidth, fileHeight);
 780    unsigned char *fileData;
 781    unsigned char *imageData;
 782    CGContextRef fileContext 
 783      = GTMCreateUnitTestBitmapContextOfSizeWithData(imageSize, &fileData);
 784    _GTMDevAssert(fileContext, @"Unable to create filecontext");
 785    CGContextDrawImage(fileContext, imageRect, fileRep);
 786    CGContextRef imageContext
 787      = GTMCreateUnitTestBitmapContextOfSizeWithData(imageSize, &imageData);
 788    _GTMDevAssert(imageContext, @"Unable to create imageContext");
 789    CGContextDrawImage(imageContext, imageRect, imageRep);
 790    
 791    size_t fileBytesPerRow = CGBitmapContextGetBytesPerRow(fileContext);
 792    size_t imageBytesPerRow = CGBitmapContextGetBytesPerRow(imageContext);
 793    size_t row, col;
 794    
 795    _GTMDevAssert(imageWidth * 4 <= imageBytesPerRow, 
 796                  @"We expect image data to be 32bit RGBA");
 797    
 798    for (row = 0; row < fileHeight && answer; row++) {
 799      answer = memcmp(fileData + fileBytesPerRow * row,
 800                      imageData + imageBytesPerRow * row,
 801                      imageWidth * 4) == 0;
 802    }
 803    if (!answer && diff) {
 804      answer = YES;
 805      unsigned char *diffData;
 806      CGContextRef diffContext 
 807        = GTMCreateUnitTestBitmapContextOfSizeWithData(imageSize, &diffData);
 808      _GTMDevAssert(diffContext, @"Can't make diff context");
 809      size_t diffRowBytes = CGBitmapContextGetBytesPerRow(diffContext);
 810      for (row = 0; row < imageHeight; row++) {
 811        uint32_t *imageRow = (uint32_t*)(imageData + imageBytesPerRow * row);
 812        uint32_t *fileRow = (uint32_t*)(fileData + fileBytesPerRow * row);
 813        uint32_t* diffRow = (uint32_t*)(diffData + diffRowBytes * row);
 814        for (col = 0; col < imageWidth; col++) {
 815          uint32_t imageColor = imageRow[col];
 816          uint32_t fileColor = fileRow[col];
 817          
 818          unsigned char imageAlpha = imageColor & 0xF;
 819          unsigned char imageBlue = imageColor >> 8 & 0xF;
 820          unsigned char imageGreen = imageColor >> 16 & 0xF;
 821          unsigned char imageRed = imageColor >> 24 & 0xF;
 822          unsigned char fileAlpha = fileColor & 0xF;
 823          unsigned char fileBlue = fileColor >> 8 & 0xF;
 824          unsigned char fileGreen = fileColor >> 16 & 0xF;
 825          unsigned char fileRed = fileColor >> 24 & 0xF;
 826          
 827          // Check to see if color is almost right.
 828          // No matter how hard I've tried, I've still gotten occasionally
 829          // screwed over by colorspaces not mapping correctly, and small
 830          // sampling errors coming in. This appears to work for most cases.
 831          // Almost equal is defined to check within 1% on all components.
 832          BOOL equal = almostEqual(imageRed, fileRed) &&
 833          almostEqual(imageGreen, fileGreen) &&
 834          almostEqual(imageBlue, fileBlue) &&
 835          almostEqual(imageAlpha, fileAlpha);
 836          answer &= equal;
 837          if (diff) {
 838            uint32_t newColor;
 839            if (equal) {
 840              newColor = (((uint32_t)imageRed) << 24) + 
 841              (((uint32_t)imageGreen) << 16) + 
 842              (((uint32_t)imageBlue) << 8) + 
 843              (((uint32_t)imageAlpha) / 2);
 844            } else {
 845              newColor = 0xFF0000FF;
 846            }
 847            diffRow[col] = newColor;
 848          }
 849        }
 850      }
 851      *diff = CGBitmapContextCreateImage(diffContext);
 852      free(diffData);
 853      CFRelease(diffContext);
 854    }       
 855    free(fileData);
 856    CFRelease(fileContext);
 857    free(imageData);
 858    CFRelease(imageContext);
 859  }
 860  return answer;
 861}
 862
 863//  Find the path for an image by name in your bundle.
 864//  Do not include the extension on your name.
 865//
 866//  Args:
 867//    name: The name for the image file you would like to find.
 868//
 869//  Returns:
 870//    the path if the image exists in your bundle
 871//    or nil if no image to be found
 872//
 873- (NSString *)gtm_pathForImageNamed:(NSString*)name {
 874  return [self gtm_pathForFileNamed:name 
 875                          extension:[self gtm_imageExtension]];
 876}
 877
 878- (NSString *)gtm_saveToPathForImageNamed:(NSString*)name {
 879  return [self gtm_saveToPathForFileNamed:name 
 880                                extension:[self gtm_imageExtension]];
 881}
 882
 883//  Gives us a representation of unitTestImage of |self|.
 884//
 885//  Returns:
 886//    a representation of image if successful
 887//    nil if failed
 888//
 889- (NSData *)gtm_imageRepresentation {
 890  CGImageRef imageRep = [self gtm_unitTestImage];
 891  NSData *data = [self gtm_imageDataForImage:imageRep];
 892  _GTMDevAssert(data, @"unable to create %@ from %@", 
 893                [self gtm_imageExtension], self);
 894  return data;
 895}
 896
 897#pragma mark UnitTestState
 898
 899// Return the extension to be used for saving unittest states
 900//
 901// Returns
 902//  An extension (e.g. "gtmUTState")
 903- (NSString*)gtm_stateExtension {
 904  return @"gtmUTState";
 905}
 906
 907//  Save the encoded unit test state to a state file with name |name| at
 908//  ~/Desktop/|name|.extension.
 909//
 910//  Note: When running under Pulse automation output is redirected to the
 911//  Pulse base directory.
 912//
 913//  Args:
 914//    name: The name for the state file you would like saved.
 915//
 916//  Returns:
 917//    YES if the file was successfully saved.
 918//
 919- (BOOL)gtm_saveToStateNamed:(NSString*)name {
 920  NSString *newPath = [self gtm_saveToPathForStateNamed:name];
 921  return [self gtm_saveToStateAt:newPath];
 922}
 923
 924//  Save encoded unit test state of |self| to a state file at path |path|.
 925//
 926//  Args:
 927//    name: The name for the state file you would like saved.
 928//
 929//  Returns:
 930//    YES if the file was successfully saved.
 931//
 932- (BOOL)gtm_saveToStateAt:(NSString*)path {
 933  if (!path) return NO;
 934  NSDictionary *dictionary = [self gtm_stateRepresentation];
 935  return [dictionary writeToFile:path atomically:YES];
 936}
 937
 938//  Compares encoded unit test state of |self| to the state file located at
 939//  |path|
 940//
 941//  Args:
 942//    path: the path to the state file you want to compare against.
 943//
 944//  Returns:
 945//    YES if they are equal, NO is they are not
 946//
 947- (BOOL)gtm_compareWithStateAt:(NSString*)path {
 948  NSDictionary *masterDict = [NSDictionary dictionaryWithContentsOfFile:path];
 949  _GTMDevAssert(masterDict, @"Unable to create dictionary from %@", path);
 950  NSDictionary *selfDict = [self gtm_stateRepresentation];
 951  return [selfDict isEqual: masterDict];
 952}
 953
 954//  Find the path for a state by name in your bundle.
 955//  Do not include the extension.
 956//
 957//  Args:
 958//    name: The name for the state file you would like to find.
 959//
 960//  Returns:
 961//    the path if the state exists in your bundle
 962//    or nil if no state to be found
 963//
 964- (NSString *)gtm_pathForStateNamed:(NSString*)name {
 965  return [self gtm_pathForFileNamed:name extension:[self gtm_stateExtension]];
 966}
 967
 968- (NSString *)gtm_saveToPathForStateNamed:(NSString*)name {
 969  return [self gtm_saveToPathForFileNamed:name 
 970                                extension:[self gtm_stateExtension]];
 971}
 972
 973//  Gives us the encoded unit test state |self|
 974//
 975//  Returns:
 976//    the encoded state if successful
 977//    nil if failed
 978//
 979- (NSDictionary *)gtm_stateRepresentation {
 980  NSDictionary *dictionary = nil;
 981  if ([self conformsToProtocol:@protocol(GTMUnitTestingEncoding)]) {
 982    id<GTMUnitTestingEncoding> encoder = (id<GTMUnitTestingEncoding>)self;
 983    GTMUnitTestingKeyedCoder *archiver;
 984    archiver = [[[GTMUnitTestingKeyedCoder alloc] init] autorelease];
 985    [encoder gtm_unitTestEncodeState:archiver];
 986    dictionary = [archiver dictionary];
 987  }
 988  return dictionary;
 989}
 990
 991//  Encodes the state of an object in a manner suitable for comparing
 992//  against a master state file so we can determine whether the
 993//  object is in a suitable state. Encode data in the coder in the same
 994//  manner that you would encode data in any other Keyed NSCoder subclass.
 995//
 996//  Arguments:
 997//    inCoder - the coder to encode our state into
 998- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder {
 999  // All impls of gtm_unitTestEncodeState
1000  // should be calling [super gtm_unitTestEncodeState] as their first action.
1001  _GTMDevAssert([inCoder isKindOfClass:[GTMUnitTestingKeyedCoder class]], 
1002                @"Coder must be of kind GTMUnitTestingKeyedCoder");
1003  
1004  // If the object has a delegate, give it a chance to respond
1005  if ([self respondsToSelector:@selector(delegate)]) {
1006    id delegate = [self performSelector:@selector(delegate)];
1007    if (delegate && 
1008        [delegate respondsToSelector:@selector(gtm_unitTestEncoderWillEncode:inCoder:)]) {
1009      [delegate gtm_unitTestEncoderWillEncode:self inCoder:inCoder];
1010    }
1011  }
1012}
1013
1014@end