/ src / common / mac / testing / GTMSenTestCase.m
GTMSenTestCase.m
  1  //
  2  //  GTMSenTestCase.m
  3  //
  4  //  Copyright 2007-2008 Google LLC
  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  
197  NSString *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  
209  NSString *const SenTestFailureException = @"SenTestFailureException";
210  NSString *const SenTestFilenameKey = @"SenTestFilenameKey";
211  NSString *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
328  static 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