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