Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 //
19 #import "GTMSenTestCase.h"
21 #import <unistd.h>
22 #if GTM_IPHONE_SIMULATOR
23 #import <objc/message.h>
24 #endif
26 #import "GTMObjC2Runtime.h"
27 #import "GTMUnitTestDevLog.h"
29 #if !GTM_IPHONE_SDK
30 #import "GTMGarbageCollection.h"
31 #endif // !GTM_IPHONE_SDK
33 #if GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
34 #import <stdarg.h>
36 @interface NSException (GTMSenTestPrivateAdditions)
37 + (NSException *)failureInFile:(NSString *)filename
38 atLine:(int)lineNumber
39 reason:(NSString *)reason;
40 @end
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];
52 return [self exceptionWithName:SenTestFailureException
53 reason:reason
54 userInfo:userInfo];
55 }
56 @end
58 @implementation NSException (GTMSenTestAdditions)
60 + (NSException *)failureInFile:(NSString *)filename
61 atLine:(int)lineNumber
62 withDescription:(NSString *)formatString, ... {
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 }
73 NSString *reason = testDescription;
75 return [self failureInFile:filename atLine:lineNumber reason:reason];
76 }
78 + (NSException *)failureInCondition:(NSString *)condition
79 isTrue:(BOOL)isTrue
80 inFile:(NSString *)filename
81 atLine:(int)lineNumber
82 withDescription:(NSString *)formatString, ... {
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 }
93 NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@",
94 condition, isTrue ? "false" : "true", testDescription];
96 return [self failureInFile:filename atLine:lineNumber reason:reason];
97 }
99 + (NSException *)failureInEqualityBetweenObject:(id)left
100 andObject:(id)right
101 inFile:(NSString *)filename
102 atLine:(int)lineNumber
103 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 }
114 NSString *reason =
115 [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
116 [left description], [right description], testDescription];
118 return [self failureInFile:filename atLine:lineNumber reason:reason];
119 }
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, ... {
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 }
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 }
148 return [self failureInFile:filename atLine:lineNumber reason:reason];
149 }
151 + (NSException *)failureInRaise:(NSString *)expression
152 inFile:(NSString *)filename
153 atLine:(int)lineNumber
154 withDescription:(NSString *)formatString, ... {
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 }
165 NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@",
166 expression, testDescription];
168 return [self failureInFile:filename atLine:lineNumber reason:reason];
169 }
171 + (NSException *)failureInRaise:(NSString *)expression
172 exception:(NSException *)exception
173 inFile:(NSString *)filename
174 atLine:(int)lineNumber
175 withDescription:(NSString *)formatString, ... {
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 }
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 }
196 return [self failureInFile:filename atLine:lineNumber reason:reason];
197 }
199 @end
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 }
213 NSString *const SenTestFailureException = @"SenTestFailureException";
214 NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
215 NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey";
217 @interface SenTestCase (SenTestCasePrivate)
218 // our method of logging errors
219 + (void)printException:(NSException *)exception fromTestName:(NSString *)name;
220 @end
222 @implementation SenTestCase
223 + (id)testCaseWithInvocation:(NSInvocation *)anInvocation {
224 return [[[self alloc] initWithInvocation:anInvocation] autorelease];
225 }
227 - (id)initWithInvocation:(NSInvocation *)anInvocation {
228 if ((self = [super init])) {
229 invocation_ = [anInvocation retain];
230 }
231 return self;
232 }
234 - (void)dealloc {
235 [invocation_ release];
236 [super dealloc];
237 }
239 - (void)failWithException:(NSException*)exception {
240 [exception raise];
241 }
243 - (void)setUp {
244 }
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 }
256 - (NSInvocation *)invocation {
257 return invocation_;
258 }
260 - (SEL)selector {
261 return [invocation_ selector];
262 }
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 }
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 }
322 - (void)tearDown {
323 }
325 - (NSString *)description {
326 // This matches the description OCUnit would return to you
327 return [NSString stringWithFormat:@"-[%@ %@]", [self class],
328 NSStringFromSelector([self selector])];
329 }
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 }
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 }
398 @end
400 #endif // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
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)];
410 }
411 [super invokeTest];
412 if (devLogClass) {
413 [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
414 [devLogClass performSelector:@selector(disableTracking)];
415 }
416 [localPool drain];
417 }
419 + (BOOL)isAbstractTestCase {
420 NSString *name = NSStringFromClass(self);
421 return [name rangeOfString:@"AbstractTest"].location != NSNotFound;
422 }
424 + (NSArray *)testInvocations {
425 NSArray *invocations = nil;
426 if (![self isAbstractTestCase]) {
427 invocations = [super testInvocations];
428 }
429 return invocations;
430 }
432 @end
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.
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
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 }
500 #endif // !GTM_IPHONE_DEVICE && !GTM_SUPPRESS_RUN_LEAKS_HOOK