/core/externals/google-toolbox-for-mac/UnitTesting/GTMSenTestCase.m

http://macfuse.googlecode.com/ · Objective C · 505 lines · 374 code · 65 blank · 66 comment · 39 complexity · 2855ced15c9e5b1daf5db5e92d998216 MD5 · raw file

  1. //
  2. // GTMSenTestCase.m
  3. //
  4. // Copyright 2007-2008 Google Inc.
  5. //
  6. // Licensed under the Apache License, Version 2.0 (the "License"); you may not
  7. // use this file except in compliance with the License. You may obtain a copy
  8. // of the License at
  9. //
  10. // http://www.apache.org/licenses/LICENSE-2.0
  11. //
  12. // Unless required by applicable law or agreed to in writing, software
  13. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  14. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  15. // License for the specific language governing permissions and limitations under
  16. // the License.
  17. //
  18. #import "GTMSenTestCase.h"
  19. #import <unistd.h>
  20. #if GTM_IPHONE_SIMULATOR
  21. #import <objc/message.h>
  22. #endif
  23. #import "GTMObjC2Runtime.h"
  24. #import "GTMUnitTestDevLog.h"
  25. #if GTM_IPHONE_SDK
  26. #import <UIKit/UIKit.h>
  27. #endif // GTM_IPHONE_SDK
  28. #if GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
  29. #import <stdarg.h>
  30. @interface NSException (GTMSenTestPrivateAdditions)
  31. + (NSException *)failureInFile:(NSString *)filename
  32. atLine:(int)lineNumber
  33. reason:(NSString *)reason;
  34. @end
  35. @implementation NSException (GTMSenTestPrivateAdditions)
  36. + (NSException *)failureInFile:(NSString *)filename
  37. atLine:(int)lineNumber
  38. reason:(NSString *)reason {
  39. NSDictionary *userInfo =
  40. [NSDictionary dictionaryWithObjectsAndKeys:
  41. [NSNumber numberWithInteger:lineNumber], SenTestLineNumberKey,
  42. filename, SenTestFilenameKey,
  43. nil];
  44. return [self exceptionWithName:SenTestFailureException
  45. reason:reason
  46. userInfo:userInfo];
  47. }
  48. @end
  49. @implementation NSException (GTMSenTestAdditions)
  50. + (NSException *)failureInFile:(NSString *)filename
  51. atLine:(int)lineNumber
  52. withDescription:(NSString *)formatString, ... {
  53. NSString *testDescription = @"";
  54. if (formatString) {
  55. va_list vl;
  56. va_start(vl, formatString);
  57. testDescription =
  58. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  59. va_end(vl);
  60. }
  61. NSString *reason = testDescription;
  62. return [self failureInFile:filename atLine:lineNumber reason:reason];
  63. }
  64. + (NSException *)failureInCondition:(NSString *)condition
  65. isTrue:(BOOL)isTrue
  66. inFile:(NSString *)filename
  67. atLine:(int)lineNumber
  68. withDescription:(NSString *)formatString, ... {
  69. NSString *testDescription = @"";
  70. if (formatString) {
  71. va_list vl;
  72. va_start(vl, formatString);
  73. testDescription =
  74. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  75. va_end(vl);
  76. }
  77. NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@",
  78. condition, isTrue ? "false" : "true", testDescription];
  79. return [self failureInFile:filename atLine:lineNumber reason:reason];
  80. }
  81. + (NSException *)failureInEqualityBetweenObject:(id)left
  82. andObject:(id)right
  83. inFile:(NSString *)filename
  84. atLine:(int)lineNumber
  85. withDescription:(NSString *)formatString, ... {
  86. NSString *testDescription = @"";
  87. if (formatString) {
  88. va_list vl;
  89. va_start(vl, formatString);
  90. testDescription =
  91. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  92. va_end(vl);
  93. }
  94. NSString *reason =
  95. [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
  96. [left description], [right description], testDescription];
  97. return [self failureInFile:filename atLine:lineNumber reason:reason];
  98. }
  99. + (NSException *)failureInEqualityBetweenValue:(NSValue *)left
  100. andValue:(NSValue *)right
  101. withAccuracy:(NSValue *)accuracy
  102. inFile:(NSString *)filename
  103. atLine:(int)lineNumber
  104. withDescription:(NSString *)formatString, ... {
  105. NSString *testDescription = @"";
  106. if (formatString) {
  107. va_list vl;
  108. va_start(vl, formatString);
  109. testDescription =
  110. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  111. va_end(vl);
  112. }
  113. NSString *reason;
  114. if (accuracy) {
  115. reason =
  116. [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
  117. left, right, testDescription];
  118. } else {
  119. reason =
  120. [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@",
  121. left, right, accuracy, testDescription];
  122. }
  123. return [self failureInFile:filename atLine:lineNumber reason:reason];
  124. }
  125. + (NSException *)failureInRaise:(NSString *)expression
  126. inFile:(NSString *)filename
  127. atLine:(int)lineNumber
  128. withDescription:(NSString *)formatString, ... {
  129. NSString *testDescription = @"";
  130. if (formatString) {
  131. va_list vl;
  132. va_start(vl, formatString);
  133. testDescription =
  134. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  135. va_end(vl);
  136. }
  137. NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@",
  138. expression, testDescription];
  139. return [self failureInFile:filename atLine:lineNumber reason:reason];
  140. }
  141. + (NSException *)failureInRaise:(NSString *)expression
  142. exception:(NSException *)exception
  143. inFile:(NSString *)filename
  144. atLine:(int)lineNumber
  145. withDescription:(NSString *)formatString, ... {
  146. NSString *testDescription = @"";
  147. if (formatString) {
  148. va_list vl;
  149. va_start(vl, formatString);
  150. testDescription =
  151. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  152. va_end(vl);
  153. }
  154. NSString *reason;
  155. if ([[exception name] isEqualToString:SenTestFailureException]) {
  156. // it's our exception, assume it has the right description on it.
  157. reason = [exception reason];
  158. } else {
  159. // not one of our exception, use the exceptions reason and our description
  160. reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@",
  161. expression, [exception reason], testDescription];
  162. }
  163. return [self failureInFile:filename atLine:lineNumber reason:reason];
  164. }
  165. @end
  166. NSString *STComposeString(NSString *formatString, ...) {
  167. NSString *reason = @"";
  168. if (formatString) {
  169. va_list vl;
  170. va_start(vl, formatString);
  171. reason =
  172. [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
  173. va_end(vl);
  174. }
  175. return reason;
  176. }
  177. NSString *const SenTestFailureException = @"SenTestFailureException";
  178. NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
  179. NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey";
  180. @interface SenTestCase (SenTestCasePrivate)
  181. // our method of logging errors
  182. + (void)printException:(NSException *)exception fromTestName:(NSString *)name;
  183. @end
  184. @implementation SenTestCase
  185. + (id)testCaseWithInvocation:(NSInvocation *)anInvocation {
  186. return [[[self alloc] initWithInvocation:anInvocation] autorelease];
  187. }
  188. - (id)initWithInvocation:(NSInvocation *)anInvocation {
  189. if ((self = [super init])) {
  190. invocation_ = [anInvocation retain];
  191. }
  192. return self;
  193. }
  194. - (void)dealloc {
  195. [invocation_ release];
  196. [super dealloc];
  197. }
  198. - (void)failWithException:(NSException*)exception {
  199. [exception raise];
  200. }
  201. - (void)setUp {
  202. }
  203. - (void)performTest {
  204. @try {
  205. [self invokeTest];
  206. } @catch (NSException *exception) {
  207. [[self class] printException:exception
  208. fromTestName:NSStringFromSelector([self selector])];
  209. [exception raise];
  210. }
  211. }
  212. - (NSInvocation *)invocation {
  213. return invocation_;
  214. }
  215. - (SEL)selector {
  216. return [invocation_ selector];
  217. }
  218. + (void)printException:(NSException *)exception fromTestName:(NSString *)name {
  219. NSDictionary *userInfo = [exception userInfo];
  220. NSString *filename = [userInfo objectForKey:SenTestFilenameKey];
  221. NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey];
  222. NSString *className = NSStringFromClass([self class]);
  223. if ([filename length] == 0) {
  224. filename = @"Unknown.m";
  225. }
  226. fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n",
  227. [filename UTF8String],
  228. (long)[lineNumber integerValue],
  229. [className UTF8String],
  230. [name UTF8String],
  231. [[exception reason] UTF8String]);
  232. fflush(stderr);
  233. }
  234. - (void)invokeTest {
  235. NSException *e = nil;
  236. @try {
  237. // Wrap things in autorelease pools because they may
  238. // have an STMacro in their dealloc which may get called
  239. // when the pool is cleaned up
  240. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  241. // We don't log exceptions here, instead we let the person that called
  242. // this log the exception. This ensures they are only logged once but the
  243. // outer layers get the exceptions to report counts, etc.
  244. @try {
  245. [self setUp];
  246. @try {
  247. NSInvocation *invocation = [self invocation];
  248. #if GTM_IPHONE_SIMULATOR
  249. // We don't call [invocation invokeWithTarget:self]; because of
  250. // Radar 8081169: NSInvalidArgumentException can't be caught
  251. // It turns out that on iOS4 (and 3.2) exceptions thrown inside an
  252. // [invocation invoke] on the simulator cannot be caught.
  253. // http://openradar.appspot.com/8081169
  254. objc_msgSend(self, [invocation selector]);
  255. #else
  256. [invocation invokeWithTarget:self];
  257. #endif
  258. } @catch (NSException *exception) {
  259. e = [exception retain];
  260. }
  261. [self tearDown];
  262. } @catch (NSException *exception) {
  263. e = [exception retain];
  264. }
  265. [pool release];
  266. } @catch (NSException *exception) {
  267. e = [exception retain];
  268. }
  269. if (e) {
  270. [e autorelease];
  271. [e raise];
  272. }
  273. }
  274. - (void)tearDown {
  275. }
  276. - (NSString *)description {
  277. // This matches the description OCUnit would return to you
  278. return [NSString stringWithFormat:@"-[%@ %@]", [self class],
  279. NSStringFromSelector([self selector])];
  280. }
  281. // Used for sorting methods below
  282. static NSInteger MethodSort(id a, id b, void *context) {
  283. NSInvocation *invocationA = a;
  284. NSInvocation *invocationB = b;
  285. const char *nameA = sel_getName([invocationA selector]);
  286. const char *nameB = sel_getName([invocationB selector]);
  287. return strcmp(nameA, nameB);
  288. }
  289. + (NSArray *)testInvocations {
  290. NSMutableArray *invocations = nil;
  291. // Need to walk all the way up the parent classes collecting methods (in case
  292. // a test is a subclass of another test).
  293. Class senTestCaseClass = [SenTestCase class];
  294. for (Class currentClass = self;
  295. currentClass && (currentClass != senTestCaseClass);
  296. currentClass = class_getSuperclass(currentClass)) {
  297. unsigned int methodCount;
  298. Method *methods = class_copyMethodList(currentClass, &methodCount);
  299. if (methods) {
  300. // This handles disposing of methods for us even if an exception should fly.
  301. [NSData dataWithBytesNoCopy:methods
  302. length:sizeof(Method) * methodCount];
  303. if (!invocations) {
  304. invocations = [NSMutableArray arrayWithCapacity:methodCount];
  305. }
  306. for (size_t i = 0; i < methodCount; ++i) {
  307. Method currMethod = methods[i];
  308. SEL sel = method_getName(currMethod);
  309. char *returnType = NULL;
  310. const char *name = sel_getName(sel);
  311. // If it starts with test, takes 2 args (target and sel) and returns
  312. // void run it.
  313. if (strstr(name, "test") == name) {
  314. returnType = method_copyReturnType(currMethod);
  315. if (returnType) {
  316. // This handles disposing of returnType for us even if an
  317. // exception should fly. Length +1 for the terminator, not that
  318. // the length really matters here, as we never reference inside
  319. // the data block.
  320. [NSData dataWithBytesNoCopy:returnType
  321. length:strlen(returnType) + 1];
  322. }
  323. }
  324. // TODO: If a test class is a subclass of another, and they reuse the
  325. // same selector name (ie-subclass overrides it), this current loop
  326. // and test here will cause cause it to get invoked twice. To fix this
  327. // the selector would have to be checked against all the ones already
  328. // added, so it only gets done once.
  329. if (returnType // True if name starts with "test"
  330. && strcmp(returnType, @encode(void)) == 0
  331. && method_getNumberOfArguments(currMethod) == 2) {
  332. NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel];
  333. NSInvocation *invocation
  334. = [NSInvocation invocationWithMethodSignature:sig];
  335. [invocation setSelector:sel];
  336. [invocations addObject:invocation];
  337. }
  338. }
  339. }
  340. }
  341. // Match SenTestKit and run everything in alphbetical order.
  342. [invocations sortUsingFunction:&MethodSort context:nil];
  343. return invocations;
  344. }
  345. @end
  346. #endif // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
  347. @implementation GTMTestCase
  348. - (void)invokeTest {
  349. NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init];
  350. Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog");
  351. if (devLogClass) {
  352. [devLogClass performSelector:@selector(enableTracking)];
  353. [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
  354. }
  355. [super invokeTest];
  356. if (devLogClass) {
  357. [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
  358. [devLogClass performSelector:@selector(disableTracking)];
  359. }
  360. [localPool drain];
  361. }
  362. + (BOOL)isAbstractTestCase {
  363. NSString *name = NSStringFromClass(self);
  364. return [name rangeOfString:@"AbstractTest"].location != NSNotFound;
  365. }
  366. #if GTM_IPHONE_SDK
  367. - (UIImage *)imageFromResource:(NSString *)resource {
  368. NSBundle *bundle = [NSBundle bundleForClass:[self class]];
  369. NSString *path = [bundle pathForResource:resource ofType:nil];
  370. UIImage *image = [UIImage imageWithContentsOfFile:path];
  371. STAssertNotNil(image, @"Could not load image from resource: %@", path);
  372. return image;
  373. }
  374. #endif
  375. + (NSArray *)testInvocations {
  376. NSArray *invocations = nil;
  377. if (![self isAbstractTestCase]) {
  378. invocations = [super testInvocations];
  379. }
  380. return invocations;
  381. }
  382. @end
  383. // Leak detection
  384. #if !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK
  385. // Don't want to get leaks on the iPhone Device as the device doesn't
  386. // have 'leaks'. The simulator does though.
  387. // COV_NF_START
  388. // We don't have leak checking on by default, so this won't be hit.
  389. static void _GTMRunLeaks(void) {
  390. // This is an atexit handler. It runs leaks for us to check if we are
  391. // leaking anything in our tests.
  392. const char* cExclusionsEnv = getenv("GTM_LEAKS_SYMBOLS_TO_IGNORE");
  393. NSMutableString *exclusions = [NSMutableString string];
  394. if (cExclusionsEnv) {
  395. NSString *exclusionsEnv = [NSString stringWithUTF8String:cExclusionsEnv];
  396. NSArray *exclusionsArray = [exclusionsEnv componentsSeparatedByString:@","];
  397. NSString *exclusion;
  398. NSCharacterSet *wcSet = [NSCharacterSet whitespaceCharacterSet];
  399. GTM_FOREACH_OBJECT(exclusion, exclusionsArray) {
  400. exclusion = [exclusion stringByTrimmingCharactersInSet:wcSet];
  401. [exclusions appendFormat:@"-exclude \"%@\" ", exclusion];
  402. }
  403. }
  404. // Clearing out DYLD_ROOT_PATH because iPhone Simulator framework libraries
  405. // are different from regular OS X libraries and leaks will fail to run
  406. // because of missing symbols. Also capturing the output of leaks and then
  407. // pipe rather than a direct pipe, because otherwise if leaks failed,
  408. // the system() call will still be successful. Bug:
  409. // http://code.google.com/p/google-toolbox-for-mac/issues/detail?id=56
  410. NSString *string
  411. = [NSString stringWithFormat:
  412. @"LeakOut=`DYLD_ROOT_PATH='' /usr/bin/leaks %@%d` &&"
  413. @"echo \"$LeakOut\"|/usr/bin/sed -e 's/Leak: /Leaks:0: warning: Leak /'",
  414. exclusions, getpid()];
  415. int ret = system([string UTF8String]);
  416. if (ret) {
  417. fprintf(stderr,
  418. "%s:%d: Error: Unable to run leaks. 'system' returned: %d\n",
  419. __FILE__, __LINE__, ret);
  420. fflush(stderr);
  421. }
  422. }
  423. // COV_NF_END
  424. static __attribute__((constructor)) void _GTMInstallLeaks(void) {
  425. BOOL checkLeaks = getenv("GTM_ENABLE_LEAKS") ? YES : NO;
  426. if (checkLeaks) {
  427. // COV_NF_START
  428. // We don't have leak checking on by default, so this won't be hit.
  429. fprintf(stderr, "Leak Checking Enabled\n");
  430. fflush(stderr);
  431. int ret = atexit(&_GTMRunLeaks);
  432. // To avoid unused variable warning when _GTMDevAssert is stripped.
  433. (void)ret;
  434. _GTMDevAssert(ret == 0,
  435. @"Unable to install _GTMRunLeaks as an atexit handler (%d)",
  436. errno);
  437. // COV_NF_END
  438. }
  439. }
  440. #endif // !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK