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