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#import <unistd.h>
21
22#if !GTM_IPHONE_SDK
23#import "GTMGarbageCollection.h"
24#endif  // !GTM_IPHONE_SDK
25
26#if GTM_IPHONE_SDK
27#import <stdarg.h>
28
29@interface NSException (GTMSenTestPrivateAdditions)
30+ (NSException *)failureInFile:(NSString *)filename
31                        atLine:(int)lineNumber
32                        reason:(NSString *)reason;
33@end
34
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
45  return [self exceptionWithName:SenTestFailureException
46                          reason:reason
47                        userInfo:userInfo];
48}
49@end
50
51@implementation NSException (GTMSenTestAdditions)
52
53+ (NSException *)failureInFile:(NSString *)filename
54                        atLine:(int)lineNumber
55               withDescription:(NSString *)formatString, ... {
56
57  NSString *testDescription = @"";
58  if (formatString) {
59    va_list vl;
60    va_start(vl, formatString);
61    testDescription =
62      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
63    va_end(vl);
64  }
65
66  NSString *reason = testDescription;
67
68  return [self failureInFile:filename atLine:lineNumber reason:reason];
69}
70
71+ (NSException *)failureInCondition:(NSString *)condition
72                             isTrue:(BOOL)isTrue
73                             inFile:(NSString *)filename
74                             atLine:(int)lineNumber
75                    withDescription:(NSString *)formatString, ... {
76
77  NSString *testDescription = @"";
78  if (formatString) {
79    va_list vl;
80    va_start(vl, formatString);
81    testDescription =
82      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
83    va_end(vl);
84  }
85
86  NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@",
87                      condition, isTrue ? "TRUE" : "FALSE", testDescription];
88
89  return [self failureInFile:filename atLine:lineNumber reason:reason];
90}
91
92+ (NSException *)failureInEqualityBetweenObject:(id)left
93                                      andObject:(id)right
94                                         inFile:(NSString *)filename
95                                         atLine:(int)lineNumber
96                                withDescription:(NSString *)formatString, ... {
97
98  NSString *testDescription = @"";
99  if (formatString) {
100    va_list vl;
101    va_start(vl, formatString);
102    testDescription =
103      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
104    va_end(vl);
105  }
106
107  NSString *reason =
108    [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
109     [left description], [right description], testDescription];
110
111  return [self failureInFile:filename atLine:lineNumber reason:reason];
112}
113
114+ (NSException *)failureInEqualityBetweenValue:(NSValue *)left
115                                      andValue:(NSValue *)right
116                                  withAccuracy:(NSValue *)accuracy
117                                        inFile:(NSString *)filename
118                                        atLine:(int)lineNumber
119                               withDescription:(NSString *)formatString, ... {
120
121  NSString *testDescription = @"";
122  if (formatString) {
123    va_list vl;
124    va_start(vl, formatString);
125    testDescription =
126      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
127    va_end(vl);
128  }
129
130  NSString *reason;
131  if (accuracy) {
132    reason =
133      [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
134       left, right, testDescription];
135  } else {
136    reason =
137      [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@",
138       left, right, accuracy, testDescription];
139  }
140
141  return [self failureInFile:filename atLine:lineNumber reason:reason];
142}
143
144+ (NSException *)failureInRaise:(NSString *)expression
145                         inFile:(NSString *)filename
146                         atLine:(int)lineNumber
147                withDescription:(NSString *)formatString, ... {
148
149  NSString *testDescription = @"";
150  if (formatString) {
151    va_list vl;
152    va_start(vl, formatString);
153    testDescription =
154      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
155    va_end(vl);
156  }
157
158  NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@",
159                      expression, testDescription];
160
161  return [self failureInFile:filename atLine:lineNumber reason:reason];
162}
163
164+ (NSException *)failureInRaise:(NSString *)expression
165                      exception:(NSException *)exception
166                         inFile:(NSString *)filename
167                         atLine:(int)lineNumber
168                withDescription:(NSString *)formatString, ... {
169
170  NSString *testDescription = @"";
171  if (formatString) {
172    va_list vl;
173    va_start(vl, formatString);
174    testDescription =
175      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
176    va_end(vl);
177  }
178
179  NSString *reason;
180  if ([[exception name] isEqualToString:SenTestFailureException]) {
181    // it's our exception, assume it has the right description on it.
182    reason = [exception reason];
183  } else {
184    // not one of our exception, use the exceptions reason and our description
185    reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@",
186              expression, [exception reason], testDescription];
187  }
188
189  return [self failureInFile:filename atLine:lineNumber reason:reason];
190}
191
192@end
193
194NSString *STComposeString(NSString *formatString, ...) {
195  NSString *reason = @"";
196  if (formatString) {
197    va_list vl;
198    va_start(vl, formatString);
199    reason =
200      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
201    va_end(vl);
202  }
203  return reason;
204}
205
206NSString *const SenTestFailureException = @"SenTestFailureException";
207NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
208NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey";
209
210@interface SenTestCase (SenTestCasePrivate)
211// our method of logging errors
212+ (void)printException:(NSException *)exception fromTestName:(NSString *)name;
213@end
214
215@implementation SenTestCase
216- (void)failWithException:(NSException*)exception {
217  [exception raise];
218}
219
220- (void)setUp {
221}
222
223- (void)performTest:(SEL)sel {
224  currentSelector_ = sel;
225  @try {
226    [self invokeTest];
227  } @catch (NSException *exception) {
228    [[self class] printException:exception
229                    fromTestName:NSStringFromSelector(sel)];
230    [exception raise];
231  }
232}
233
234+ (void)printException:(NSException *)exception fromTestName:(NSString *)name {
235  NSDictionary *userInfo = [exception userInfo];
236  NSString *filename = [userInfo objectForKey:SenTestFilenameKey];
237  NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey];
238  NSString *className = NSStringFromClass([self class]);
239  if ([filename length] == 0) {
240    filename = @"Unknown.m";
241  }
242  fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n",
243          [filename UTF8String],
244          (long)[lineNumber integerValue],
245          [className UTF8String],
246          [name UTF8String],
247          [[exception reason] UTF8String]);
248  fflush(stderr);
249}
250
251- (void)invokeTest {
252  NSException *e = nil;
253  @try {
254    // Wrap things in autorelease pools because they may
255    // have an STMacro in their dealloc which may get called
256    // when the pool is cleaned up
257    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
258    // We don't log exceptions here, instead we let the person that called
259    // this log the exception.  This ensures they are only logged once but the
260    // outer layers get the exceptions to report counts, etc.
261    @try {
262      [self setUp];
263      @try {
264        [self performSelector:currentSelector_];
265      } @catch (NSException *exception) {
266        e = [exception retain];
267      }
268      [self tearDown];
269    } @catch (NSException *exception) {
270      e = [exception retain];
271    }
272    [pool release];
273  } @catch (NSException *exception) {
274    e = [exception retain];
275  }
276  if (e) {
277    [e autorelease];
278    [e raise];
279  }
280}
281
282- (void)tearDown {
283}
284
285- (NSString *)description {
286  // This matches the description OCUnit would return to you
287  return [NSString stringWithFormat:@"-[%@ %@]", [self class],
288          NSStringFromSelector(currentSelector_)];
289}
290@end
291
292#endif  // GTM_IPHONE_SDK
293
294@implementation GTMTestCase : SenTestCase
295- (void)invokeTest {
296  Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog");
297  if (devLogClass) {
298    [devLogClass performSelector:@selector(enableTracking)];
299    [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
300
301  }
302  [super invokeTest];
303  if (devLogClass) {
304    [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
305    [devLogClass performSelector:@selector(disableTracking)];
306  }
307}
308@end
309
310// Leak detection
311#if !GTM_IPHONE_DEVICE
312// Don't want to get leaks on the iPhone Device as the device doesn't
313// have 'leaks'. The simulator does though.
314
315// COV_NF_START
316// We don't have leak checking on by default, so this won't be hit.
317static void _GTMRunLeaks(void) {
318  // This is an atexit handler. It runs leaks for us to check if we are
319  // leaking anything in our tests.
320  const char* cExclusionsEnv = getenv("GTM_LEAKS_SYMBOLS_TO_IGNORE");
321  NSMutableString *exclusions = [NSMutableString string];
322  if (cExclusionsEnv) {
323    NSString *exclusionsEnv = [NSString stringWithUTF8String:cExclusionsEnv];
324    NSArray *exclusionsArray = [exclusionsEnv componentsSeparatedByString:@","];
325    NSString *exclusion;
326    NSCharacterSet *wcSet = [NSCharacterSet whitespaceCharacterSet];
327    GTM_FOREACH_OBJECT(exclusion, exclusionsArray) {
328      exclusion = [exclusion stringByTrimmingCharactersInSet:wcSet];
329      [exclusions appendFormat:@"-exclude \"%@\" ", exclusion];
330    }
331  }
332  NSString *string
333    = [NSString stringWithFormat:@"/usr/bin/leaks %@%d"
334       @"| /usr/bin/sed -e 's/Leak: /Leaks:0: warning: Leak /'",
335       exclusions, getpid()];
336  int ret = system([string UTF8String]);
337  if (ret) {
338    fprintf(stderr, "%s:%d: Error: Unable to run leaks. 'system' returned: %d",
339            __FILE__, __LINE__, ret);
340    fflush(stderr);
341  }
342}
343// COV_NF_END
344
345static __attribute__((constructor)) void _GTMInstallLeaks(void) {
346  BOOL checkLeaks = YES;
347#if !GTM_IPHONE_SDK
348  checkLeaks = GTMIsGarbageCollectionEnabled() ? NO : YES;
349#endif  // !GTM_IPHONE_SDK
350  if (checkLeaks) {
351    checkLeaks = getenv("GTM_ENABLE_LEAKS") ? YES : NO;
352    if (checkLeaks) {
353      // COV_NF_START
354      // We don't have leak checking on by default, so this won't be hit.
355      fprintf(stderr, "Leak Checking Enabled\n");
356      fflush(stderr);
357      int ret = atexit(&_GTMRunLeaks);
358      _GTMDevAssert(ret == 0,
359                    @"Unable to install _GTMRunLeaks as an atexit handler (%d)",
360                    errno);
361      // COV_NF_END
362    }
363  }
364}
365
366#endif   // !GTM_IPHONE_DEVICE
367