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
201NSString *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
213NSString *const SenTestFailureException = @"SenTestFailureException";
214NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
215NSString *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
332static 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.
441static 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
477static __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
501