PageRenderTime 189ms CodeModel.GetById 20ms app.highlight 158ms RepoModel.GetById 1ms app.codeStats 1ms

/core/externals/google-toolbox-for-mac/UnitTesting/GTMNSObject+UnitTesting.m

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