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