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 && !GTM_IPHONE_USE_SENTEST
30#import <stdarg.h>
31
32@interface NSException (GTMSenTestPrivateAdditions)
33+ (NSException *)failureInFile:(NSString *)filename
34                        atLine:(int)lineNumber
35                        reason:(NSString *)reason;
36@end
37
38@implementation NSException (GTMSenTestPrivateAdditions)
39+ (NSException *)failureInFile:(NSString *)filename
40                        atLine:(int)lineNumber
41                        reason:(NSString *)reason {
42  NSDictionary *userInfo =
43    [NSDictionary dictionaryWithObjectsAndKeys:
44     [NSNumber numberWithInteger:lineNumber], SenTestLineNumberKey,
45     filename, SenTestFilenameKey,
46     nil];
47
48  return [self exceptionWithName:SenTestFailureException
49                          reason:reason
50                        userInfo:userInfo];
51}
52@end
53
54@implementation NSException (GTMSenTestAdditions)
55
56+ (NSException *)failureInFile:(NSString *)filename
57                        atLine:(int)lineNumber
58               withDescription:(NSString *)formatString, ... {
59
60  NSString *testDescription = @"";
61  if (formatString) {
62    va_list vl;
63    va_start(vl, formatString);
64    testDescription =
65      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
66    va_end(vl);
67  }
68
69  NSString *reason = testDescription;
70
71  return [self failureInFile:filename atLine:lineNumber reason:reason];
72}
73
74+ (NSException *)failureInCondition:(NSString *)condition
75                             isTrue:(BOOL)isTrue
76                             inFile:(NSString *)filename
77                             atLine:(int)lineNumber
78                    withDescription:(NSString *)formatString, ... {
79
80  NSString *testDescription = @"";
81  if (formatString) {
82    va_list vl;
83    va_start(vl, formatString);
84    testDescription =
85      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
86    va_end(vl);
87  }
88
89  NSString *reason = [NSString stringWithFormat:@"'%@' should be %s. %@",
90                      condition, isTrue ? "false" : "true", testDescription];
91
92  return [self failureInFile:filename atLine:lineNumber reason:reason];
93}
94
95+ (NSException *)failureInEqualityBetweenObject:(id)left
96                                      andObject:(id)right
97                                         inFile:(NSString *)filename
98                                         atLine:(int)lineNumber
99                                withDescription:(NSString *)formatString, ... {
100
101  NSString *testDescription = @"";
102  if (formatString) {
103    va_list vl;
104    va_start(vl, formatString);
105    testDescription =
106      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
107    va_end(vl);
108  }
109
110  NSString *reason =
111    [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
112     [left description], [right description], testDescription];
113
114  return [self failureInFile:filename atLine:lineNumber reason:reason];
115}
116
117+ (NSException *)failureInEqualityBetweenValue:(NSValue *)left
118                                      andValue:(NSValue *)right
119                                  withAccuracy:(NSValue *)accuracy
120                                        inFile:(NSString *)filename
121                                        atLine:(int)lineNumber
122                               withDescription:(NSString *)formatString, ... {
123
124  NSString *testDescription = @"";
125  if (formatString) {
126    va_list vl;
127    va_start(vl, formatString);
128    testDescription =
129      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
130    va_end(vl);
131  }
132
133  NSString *reason;
134  if (accuracy) {
135    reason =
136      [NSString stringWithFormat:@"'%@' should be equal to '%@'. %@",
137       left, right, testDescription];
138  } else {
139    reason =
140      [NSString stringWithFormat:@"'%@' should be equal to '%@' +/-'%@'. %@",
141       left, right, accuracy, testDescription];
142  }
143
144  return [self failureInFile:filename atLine:lineNumber reason:reason];
145}
146
147+ (NSException *)failureInRaise:(NSString *)expression
148                         inFile:(NSString *)filename
149                         atLine:(int)lineNumber
150                withDescription:(NSString *)formatString, ... {
151
152  NSString *testDescription = @"";
153  if (formatString) {
154    va_list vl;
155    va_start(vl, formatString);
156    testDescription =
157      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
158    va_end(vl);
159  }
160
161  NSString *reason = [NSString stringWithFormat:@"'%@' should raise. %@",
162                      expression, testDescription];
163
164  return [self failureInFile:filename atLine:lineNumber reason:reason];
165}
166
167+ (NSException *)failureInRaise:(NSString *)expression
168                      exception:(NSException *)exception
169                         inFile:(NSString *)filename
170                         atLine:(int)lineNumber
171                withDescription:(NSString *)formatString, ... {
172
173  NSString *testDescription = @"";
174  if (formatString) {
175    va_list vl;
176    va_start(vl, formatString);
177    testDescription =
178      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
179    va_end(vl);
180  }
181
182  NSString *reason;
183  if ([[exception name] isEqualToString:SenTestFailureException]) {
184    // it's our exception, assume it has the right description on it.
185    reason = [exception reason];
186  } else {
187    // not one of our exception, use the exceptions reason and our description
188    reason = [NSString stringWithFormat:@"'%@' raised '%@'. %@",
189              expression, [exception reason], testDescription];
190  }
191
192  return [self failureInFile:filename atLine:lineNumber reason:reason];
193}
194
195@end
196
197NSString *STComposeString(NSString *formatString, ...) {
198  NSString *reason = @"";
199  if (formatString) {
200    va_list vl;
201    va_start(vl, formatString);
202    reason =
203      [[[NSString alloc] initWithFormat:formatString arguments:vl] autorelease];
204    va_end(vl);
205  }
206  return reason;
207}
208
209NSString *const SenTestFailureException = @"SenTestFailureException";
210NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
211NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey";
212
213@interface SenTestCase (SenTestCasePrivate)
214// our method of logging errors
215+ (void)printException:(NSException *)exception fromTestName:(NSString *)name;
216@end
217
218@implementation SenTestCase
219+ (id)testCaseWithInvocation:(NSInvocation *)anInvocation {
220  return [[[self alloc] initWithInvocation:anInvocation] autorelease];
221}
222
223- (id)initWithInvocation:(NSInvocation *)anInvocation {
224  if ((self = [super init])) {
225    invocation_ = [anInvocation retain];
226  }
227  return self;
228}
229
230- (void)dealloc {
231  [invocation_ release];
232  [super dealloc];
233}
234
235- (void)failWithException:(NSException*)exception {
236  [exception raise];
237}
238
239- (void)setUp {
240}
241
242- (void)performTest {
243  @try {
244    [self invokeTest];
245  } @catch (NSException *exception) {
246    [[self class] printException:exception
247                    fromTestName:NSStringFromSelector([self selector])];
248    [exception raise];
249  }
250}
251
252- (NSInvocation *)invocation {
253  return invocation_;
254}
255
256- (SEL)selector {
257  return [invocation_ selector];
258}
259
260+ (void)printException:(NSException *)exception fromTestName:(NSString *)name {
261  NSDictionary *userInfo = [exception userInfo];
262  NSString *filename = [userInfo objectForKey:SenTestFilenameKey];
263  NSNumber *lineNumber = [userInfo objectForKey:SenTestLineNumberKey];
264  NSString *className = NSStringFromClass([self class]);
265  if ([filename length] == 0) {
266    filename = @"Unknown.m";
267  }
268  fprintf(stderr, "%s:%ld: error: -[%s %s] : %s\n",
269          [filename UTF8String],
270          (long)[lineNumber integerValue],
271          [className UTF8String],
272          [name UTF8String],
273          [[exception reason] UTF8String]);
274  fflush(stderr);
275}
276
277- (void)invokeTest {
278  NSException *e = nil;
279  @try {
280    // Wrap things in autorelease pools because they may
281    // have an STMacro in their dealloc which may get called
282    // when the pool is cleaned up
283    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
284    // We don't log exceptions here, instead we let the person that called
285    // this log the exception.  This ensures they are only logged once but the
286    // outer layers get the exceptions to report counts, etc.
287    @try {
288      [self setUp];
289      @try {
290        NSInvocation *invocation = [self invocation];
291#if GTM_IPHONE_SIMULATOR
292        // We don't call [invocation invokeWithTarget:self]; because of
293        // Radar 8081169: NSInvalidArgumentException can't be caught
294        // It turns out that on iOS4 (and 3.2) exceptions thrown inside an
295        // [invocation invoke] on the simulator cannot be caught.
296        // http://openradar.appspot.com/8081169
297        objc_msgSend(self, [invocation selector]);
298#else
299        [invocation invokeWithTarget:self];
300#endif
301      } @catch (NSException *exception) {
302        e = [exception retain];
303      }
304      [self tearDown];
305    } @catch (NSException *exception) {
306      e = [exception retain];
307    }
308    [pool release];
309  } @catch (NSException *exception) {
310    e = [exception retain];
311  }
312  if (e) {
313    [e autorelease];
314    [e raise];
315  }
316}
317
318- (void)tearDown {
319}
320
321- (NSString *)description {
322  // This matches the description OCUnit would return to you
323  return [NSString stringWithFormat:@"-[%@ %@]", [self class],
324          NSStringFromSelector([self selector])];
325}
326
327// Used for sorting methods below
328static int MethodSort(id a, id b, void *context) {
329  NSInvocation *invocationA = a;
330  NSInvocation *invocationB = b;
331  const char *nameA = sel_getName([invocationA selector]);
332  const char *nameB = sel_getName([invocationB selector]);
333  return strcmp(nameA, nameB);
334}
335
336
337+ (NSArray *)testInvocations {
338  NSMutableArray *invocations = nil;
339  // Need to walk all the way up the parent classes collecting methods (in case
340  // a test is a subclass of another test).
341  Class senTestCaseClass = [SenTestCase class];
342  for (Class currentClass = self;
343       currentClass && (currentClass != senTestCaseClass);
344       currentClass = class_getSuperclass(currentClass)) {
345    unsigned int methodCount;
346    Method *methods = class_copyMethodList(currentClass, &methodCount);
347    if (methods) {
348      // This handles disposing of methods for us even if an exception should fly.
349      [NSData dataWithBytesNoCopy:methods
350                           length:sizeof(Method) * methodCount];
351      if (!invocations) {
352        invocations = [NSMutableArray arrayWithCapacity:methodCount];
353      }
354      for (size_t i = 0; i < methodCount; ++i) {
355        Method currMethod = methods[i];
356        SEL sel = method_getName(currMethod);
357        char *returnType = NULL;
358        const char *name = sel_getName(sel);
359        // If it starts with test, takes 2 args (target and sel) and returns
360        // void run it.
361        if (strstr(name, "test") == name) {
362          returnType = method_copyReturnType(currMethod);
363          if (returnType) {
364            // This handles disposing of returnType for us even if an
365            // exception should fly. Length +1 for the terminator, not that
366            // the length really matters here, as we never reference inside
367            // the data block.
368            [NSData dataWithBytesNoCopy:returnType
369                                 length:strlen(returnType) + 1];
370          }
371        }
372        // TODO: If a test class is a subclass of another, and they reuse the
373        // same selector name (ie-subclass overrides it), this current loop
374        // and test here will cause cause it to get invoked twice.  To fix this
375        // the selector would have to be checked against all the ones already
376        // added, so it only gets done once.
377        if (returnType  // True if name starts with "test"
378            && strcmp(returnType, @encode(void)) == 0
379            && method_getNumberOfArguments(currMethod) == 2) {
380          NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel];
381          NSInvocation *invocation
382            = [NSInvocation invocationWithMethodSignature:sig];
383          [invocation setSelector:sel];
384          [invocations addObject:invocation];
385        }
386      }
387    }
388  }
389  // Match SenTestKit and run everything in alphbetical order.
390  [invocations sortUsingFunction:MethodSort context:nil];
391  return invocations;
392}
393
394@end
395
396#endif  // GTM_IPHONE_SDK && !GTM_IPHONE_USE_SENTEST
397
398@implementation GTMTestCase : SenTestCase
399- (void)invokeTest {
400  NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init];
401  Class devLogClass = NSClassFromString(@"GTMUnitTestDevLog");
402  if (devLogClass) {
403    [devLogClass performSelector:@selector(enableTracking)];
404    [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
405
406  }
407  [super invokeTest];
408  if (devLogClass) {
409    [devLogClass performSelector:@selector(verifyNoMoreLogsExpected)];
410    [devLogClass performSelector:@selector(disableTracking)];
411  }
412  [localPool drain];
413}
414
415+ (BOOL)isAbstractTestCase {
416  NSString *name = NSStringFromClass(self);
417  return [name rangeOfString:@"AbstractTest"].location != NSNotFound;
418}
419
420+ (NSArray *)testInvocations {
421  NSArray *invocations = nil;
422  if (![self isAbstractTestCase]) {
423    invocations = [super testInvocations];
424  }
425  return invocations;
426}
427
428@end
429