]> git.wincent.com - WOTest.git/blob - WOTestClass.m
Fix object-to-pointer comparisons on Leopard
[WOTest.git] / WOTestClass.m
1 //
2 //  WOTestClass.m
3 //  WOTest
4 //
5 //  Created by Wincent Colaiuta on 12 October 2004.
6 //
7 //  Copyright 2004-2007 Wincent Colaiuta.
8 //  This program is free software: you can redistribute it and/or modify
9 //  it under the terms of the GNU General Public License as published by
10 //  the Free Software Foundation, either version 3 of the License, or
11 //  (at your option) any later version.
12 //
13 //  This program is distributed in the hope that it will be useful,
14 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 //  GNU General Public License for more details.
17 //
18 //  You should have received a copy of the GNU General Public License
19 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 //
21
22 // class header
23 #import "WOTestClass.h"
24
25 // system headers
26 #import <objc/objc-runtime.h>
27 #import <sys/types.h>               /* write() */
28 #import <sys/uio.h>                 /* write() */
29 #import <unistd.h>                  /* write(), _exit() */
30 #import <mach/mach.h>
31 #import <pthread.h>
32
33 // framework headers
34 #import "WOTest.h"
35 #import "exc.h"                     /* generated by MiG */
36
37 // make what(1) produce meaningful output
38 #import "WOTest_Version.h"
39
40 #define WO_UNICODE_PLUS_MINUS_SIGN 0x00b1
41 #define WO_UNICODE_ELLIPSIS        0x2026
42
43 // convenience macros, call method to truncate description to 64 characters
44 #define WO_TRUNCATE_INDEX   64
45
46 // return truncated description of \p object, expects a BOOL variable of the format objectTruncated to be defined within the same scope
47 #define WO_DESC(object)         [self description:object truncatedAt:WO_TRUNCATE_INDEX didTruncate:&object ## Truncated]
48
49 // return untruncated description of object
50 #define WO_LONG_DESC(object)    [self description:object truncatedAt:0 didTruncate:NULL]
51
52 #define WO_UNCAUGHT_EXCEPTION_ERROR @"uncaught exception"
53
54 // convenience macro to throw exception when NSString type checking fails
55 #define WO_EXPECTED_STRING_EXCEPTION_REASON(object)                         \
56 [NSString stringWithFormat:@"Expected NSString object but got object of class \"%@\"", NSStringFromClass([object class])]
57
58 // convenience macro to throw exception when NSArray type checking fails
59 #define WO_EXPECTED_ARRAY_EXCEPTION_REASON(object)                          \
60 [NSString stringWithFormat:@"Expected NSArray object but got object of class \"%@\"", NSStringFromClass([object class])]
61
62 // convenience macro to throw exception when NSDictionary type check fails
63 #define WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(object)                     \
64 [NSString stringWithFormat:@"Expected NSDictionary object but got object of class \"%@\"", NSStringFromClass([object class])]
65
66 #define WO_NIL_PARAMETER_EXCEPTION_REASON @"A test which does not accept nil parameters was passed a nil parameter"
67
68 // Return a random offset between 0 and WO_RANDOMIZATION_RANGE inclusive.
69 #define WO_RANDOM_OFFSET        (random() % (WO_RANDOMIZATION_RANGE - 1))
70
71 // Return +1 or -1 randomly.
72 #define WO_RANDOM_SIGN          ((BOOL)(random() % 2) ? 1 : -1)
73
74 #pragma mark -
75 #pragma mark Class variables
76
77 static WOTest                   *WOTestSharedInstance       = nil;
78 static volatile BOOL            WOTestCanJump               = NO;
79 static volatile ExceptionKind   WOLastLowLevelException;
80 static volatile sig_atomic_t    WOTestExceptionTriggered    = 0;
81 static ExceptionHandlerUPP      WOOldLowLevelExceptionHandler;
82
83 // for the timebeing, only store/restore a limited number of registers; may remove the unused ones from the struct at a later time
84 typedef struct WOJumpBuffer {
85     unsigned long eax;      // store/restore
86     unsigned long ebx;      // store/restore
87     unsigned long ecx; 
88     unsigned long edx;
89     unsigned long edi;      // store/restore
90     unsigned long esi;      // store/restore
91     unsigned long ebp;      // store/restore
92     unsigned long esp;      // store/restore
93     unsigned long ss; 
94     unsigned long eflags;  
95     unsigned long cs; 
96     unsigned long ds; 
97     unsigned long es; 
98     unsigned long fs; 
99     unsigned long gs;
100 } WOJumpBuffer;
101 static volatile WOJumpBuffer WOLowLevelExceptionJumpBuffer;
102
103 #ifdef __i386__
104 static unsigned long    WOProgramCounter;
105 #elif defined (__ppc__)
106 static UnsignedWide     WOProgramCounter;
107 #else
108 #error Unsupported architecture
109 #endif
110
111 #pragma mark -
112 #pragma mark Functions
113
114 OSStatus WOLowLevelExceptionHandler(ExceptionInformation *theException)
115 {
116     if (!WOTestCanJump)         // unexpected exception
117     {
118         fprintf(stderr, "error: WOTest internal error (unexpected exception in WOLowLevelExceptionHandler)\n");
119         fprintf(stderr, "Exception type: %lu\n", (unsigned long)(theException->theKind));
120         fflush(NULL);
121         
122         // forwarding to old exception handler doesn't seem to work (get into infinite loop)
123         //return InvokeExceptionHandlerUPP(theException, WOOldLowLevelExceptionHandler);
124         _exit(EXIT_FAILURE);
125     }
126     
127     WOLastLowLevelException     = theException->theKind;
128     WOTestCanJump               = NO;
129
130     // set flag to indicate that an exception was triggerd
131     WOTestExceptionTriggered    = 1;
132     
133     // will resume execution at previously marked "safe place": longjmp would be fine here
134 #ifdef __i386__
135     // set only the registers that setjmp() saves and longjmp() restores
136     theException->machineState->EIP     = WOProgramCounter;
137     theException->registerImage->EBP    = WOLowLevelExceptionJumpBuffer.ebp;
138     theException->registerImage->EAX    = WOLowLevelExceptionJumpBuffer.eax;
139     theException->registerImage->EBX    = WOLowLevelExceptionJumpBuffer.ebx;
140     theException->registerImage->EDI    = WOLowLevelExceptionJumpBuffer.edi;
141     theException->registerImage->ESI    = WOLowLevelExceptionJumpBuffer.esi;
142     theException->registerImage->ESP    = WOLowLevelExceptionJumpBuffer.esp;
143     
144     // clear out exception state (probably not necessary)
145     theException->info.memoryInfo = NULL;
146     
147 #elif defined (__ppc__)
148     theException->machineState->PC  = WOProgramCounter;
149     // TODO: must restore more state here
150 #else
151 #error Unsupported architecture
152 #endif
153     return noErr;
154 }
155
156 @interface WOTest (WOPrivate)
157
158 - (void)installLowLevelExceptionHandler;
159 - (void)removeLowLevelExceptionHandler;
160
161 /*! Check to see that the start date has been recorded. If it has not, record it. */
162 - (void)checkStartDate;
163
164 - (void)setStartDate:(NSDate *)aStartDate;
165
166 /*! Helper method for optionally trimming path names before printing them to the console. */
167 - (NSString *)trimmedPath:(char *)path;
168
169 @end
170
171 @implementation WOTest
172
173 #pragma mark -
174 #pragma mark Singleton pattern enforcement methods
175
176 + (WOTest *)sharedInstance;
177 {
178     // speed less of a concern here than robustness so always lock (instead of using double-checked locking plus memory barriers)
179     volatile id instance = nil;
180     @synchronized (WOTestSharedInstance)
181     {
182         if (WOTestSharedInstance)
183             instance = WOTestSharedInstance;
184         else
185             instance = [[self alloc] init];
186     }
187     return instance;
188 }
189
190 - (id)init
191 {
192     @synchronized (WOTestSharedInstance)
193     {
194         if (!WOTestSharedInstance)          // first time here
195         {
196             if ((self = [super init]))
197             {
198                 // once-off initialization and setting of defaults:
199                 self->handlesLowLevelExceptions = YES; 
200                 self->warnsAboutSignComparisons = YES;
201             }
202             WOTestSharedInstance = self;
203         }
204         else
205             NSDeallocateObject(self);       // were racing, but lost the race
206     }
207     return WOTestSharedInstance;
208 }
209
210 // overriding allocWithZone also effectively overrides alloc
211 + (id)allocWithZone:(NSZone *)aZone
212 {
213     volatile id instance = nil;
214     @synchronized (WOTestSharedInstance)
215     {
216         if (WOTestSharedInstance)
217             instance = WOTestSharedInstance;
218         else
219             instance = NSAllocateObject([self class], 0, aZone);
220     }
221     return instance;
222 }
223
224 - (oneway void)release
225 {
226     return;
227 }
228
229 - (void)dealloc
230 {
231     [self doesNotRecognizeSelector:_cmd];   // being a singleton, officially does not support dealloc
232     [super dealloc];                        // this line necessary to suppress compiler warning
233 }
234
235 - (unsigned)retainCount
236 {
237     return UINT_MAX;
238 }
239
240 - (id)autorelease
241 {
242     return self;
243 }
244
245 - (id)retain
246 {
247     return self;
248 }
249
250 // overriding this also overrides copy
251 - (id)copyWithZone:(NSZone *)zone
252 {
253     return self;
254 }
255
256 // overriding this also overrides mutableCopy
257 - (id)mutableCopyWithZone:(NSZone *)zone
258 {
259     return self;
260 }
261
262 #pragma mark -
263 #pragma mark Utility methods
264
265 - (NSString *)description:(id)anObject truncatedAt:(unsigned)index didTruncate:(BOOL *)didTruncate
266 {
267     if (didTruncate)    *didTruncate    = NO;
268     NSString            *description    = nil;
269     if (!anObject) 
270         description = @"(nil)";
271     else
272     {
273         @try
274         {
275             description = [NSObject WOTest_descriptionForObject:anObject];
276             unsigned int originalLength = [description length];
277             if (index > 0)  // a value of 0 would indicate that no truncation is to be performed
278             {
279                 description = [description WOTest_stringByCollapsingWhitespace];
280                 if ([description length] > index)
281                     description = [[description substringToIndex:index] WOTest_stringByAppendingCharacter:WO_UNICODE_ELLIPSIS];
282                 if (([description length] != originalLength) && (didTruncate))
283                     *didTruncate = YES;
284             }
285         }
286         @catch (id e)
287         {
288             description = @"(exception caught trying to get object description)";
289         }
290     }
291     return description;
292 }
293
294 - (void)seedRandomNumberGenerator
295 {
296     srandom(time(NULL));
297 }
298
299 - (void)seedRandomNumberGenerator:(unsigned long)seed
300 {
301     srandom(seed);
302 }
303
304 - (BOOL)isClassMethod:(NSString *)method
305 {
306     return (method && [method hasPrefix:@"+"]);
307 }
308
309 - (BOOL)isInstanceMethod:(NSString *)method
310 {
311     return (method && [method hasPrefix:@"-"]);
312 }
313
314 - (SEL)selectorFromMethod:(NSString *)method
315 {
316     NSParameterAssert(method != nil);
317     NSParameterAssert([method length] > 1);
318     NSString *selectorName = [method substringFromIndex:1];
319     return NSSelectorFromString(selectorName);
320 }
321
322 #pragma mark -
323 #pragma mark Test-running methods
324
325 - (void)checkStartDate
326 {
327     @synchronized (self)
328     {
329         if ([self startDate] == nil)
330             [self setStartDate:[NSDate date]];
331     }
332 }
333
334 - (BOOL)runAllTests
335 {
336     int failures = 0;
337     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
338     WO_ENUMERATE([self testableClasses], class)
339         [self runTestsForClassName:class] ? : failures++;
340     [self printTestResultsSummary];
341     [pool release];
342     return (failures > 0) ? NO : YES;
343 }
344
345 - (BOOL)runTestsForClassName:(NSString *)className
346 {
347     NSParameterAssert(className != nil);
348     return [self runTestsForClass:NSClassFromString(className)];
349 }
350
351 // all other test-running methods ultimately get funnelled through this method
352 - (BOOL)runTestsForClass:(Class)aClass
353 {
354     NSParameterAssert(aClass != nil);
355     [self checkStartDate];
356     BOOL    noTestFailed    = YES;
357     NSDate  *startClass     = [NSDate date];
358     @try
359     {
360         _WOLog(@"Running tests for class %@", NSStringFromClass(aClass));
361         if ([NSObject WOTest_instancesOfClass:aClass conformToProtocol:@protocol(WOTest)])
362         {
363             NSArray         *methods    = [self testableMethodsFrom:aClass];
364             WO_ENUMERATE(methods, method)
365             {
366                 NSAutoreleasePool   *pool           = [[NSAutoreleasePool alloc] init];
367                 NSDate              *startMethod    = [NSDate date];
368                 SEL                 preflight       = @selector(preflight);
369                 SEL                 postflight      = @selector(postflight);
370                 
371                 _WOLog(@"Running test method %@", method);
372                 @try
373                 {
374                     // minimize time spent with exception handlers in place
375                     [self installLowLevelExceptionHandler];
376                     
377                     // record program counter and some other registers right now
378 #ifdef __i386__
379                     // LowLevelABI.pdf says "EDI, ESI, EBX, EBP" are the preserved registers (across function calls)                    
380                     // ebp is the "saved frame pointer": "the base address of the caller's stack frame"
381                     // eax is used to return pointer and integral results to callers: "The called function places integral or pointer results in EAX"
382                     
383                     // info on inline assembly: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
384                     __asm__ volatile("movl %%eax, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.eax));
385                     __asm__ volatile("movl %%ebx, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.ebx));
386                     __asm__ volatile("movl %%edi, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.edi));
387                     __asm__ volatile("movl %%esi, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.esi));
388                     __asm__ volatile("movl %%esp, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.esp));
389                     __asm__ volatile("movl %%ebp, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.ebp));
390
391                     // done this way in Linux (see acpi_save_register_state function)
392                     WOProgramCounter = (unsigned long)&&jump_point;
393 #elif defined (__ppc__)
394                     // equivalent to (psuedo code) "WOProgramCounter = current contents of PC register"
395                     unsigned long counter;
396                     __asm__ volatile("mflr %0" : "=r" (counter));
397                     WOProgramCounter.lo = counter & 0xffffffff;
398                     WOProgramCounter.hi = (counter & 0xffffffff00000000) >> 32;
399 #else
400 #error Unsupported architecture
401 #endif
402                     WOTestCanJump = YES;
403                     
404                     goto jump_point; // necessary to silence compiler warning about unused label
405 jump_point:
406                     // if flag set, that means we crashed: throw an exception
407                     if (WOTestExceptionTriggered)
408                     {
409                         WOTestExceptionTriggered = 0;                        
410                         @throw [WOTestLowLevelException exceptionWithType:WOLastLowLevelException];
411                     }
412                     
413                     if ([self isClassMethod:method])
414                     {
415                         if ([NSObject WOTest_class:aClass respondsToSelector:preflight])
416                             objc_msgSend(aClass, preflight);
417                         objc_msgSend(aClass, [self selectorFromMethod:method]);
418                         if ([NSObject WOTest_class:aClass respondsToSelector:postflight])
419                             objc_msgSend(aClass, postflight);
420                     }
421                     else if ([self isInstanceMethod:method])
422                     {
423                         // class must implement alloc, init and release
424                         if ([NSObject WOTest_object:aClass respondsToSelector:@selector(alloc)] &&
425                             [NSObject WOTest_instancesOfClass:aClass respondToSelector:@selector(init)] &&
426                             [NSObject WOTest_instancesOfClass:aClass respondToSelector:@selector(release)])
427                         {
428                             id instance = [[aClass alloc] init];
429                             if ([NSObject WOTest_instancesOfClass:aClass respondToSelector:preflight])
430                                 objc_msgSend(instance, preflight);
431                             objc_msgSend(instance, [self selectorFromMethod:method]);
432                             if ([NSObject WOTest_instancesOfClass:aClass respondToSelector:postflight])
433                                 objc_msgSend(instance, postflight);
434                             [instance release];
435                         }
436                         else
437                         {
438                             [self writeError:@"Class %@ must respond to the alloc, init and release selectors", 
439                                 NSStringFromClass(aClass)];
440                             [self writeLastKnownLocation];
441                         }
442                     }
443                     else    // should never get here
444                         [self writeError:@"WOTest internal error"];
445                 }
446                 @catch (WOTestLowLevelException *lowLevelException)
447                 {
448                     if ([self expectLowLevelExceptions])
449                     {
450                         [self writeStatus:[lowLevelException reason]];    // expected low-level exceptions are not an error
451                         lowLevelExceptionsExpected++;
452                     }
453                     else
454                     {
455                         [self writeError:[lowLevelException reason]];     // unexpected low-level exceptions are an error
456                         [self writeLastKnownLocation];
457                         noTestFailed = NO;
458                         lowLevelExceptionsUnexpected++;
459                     }
460                 }
461                 @catch (id e)
462                 {
463                     [self writeError:@"uncaught exception (%@) in test method %@", [NSException WOTest_descriptionForException:e], 
464                         method];
465                     [self writeLastKnownLocation];
466                     noTestFailed = NO;
467                     uncaughtExceptions++;
468                 }
469                 @finally
470                 {
471                     if (lowLevelExceptionHandlerInstalled)
472                         [self removeLowLevelExceptionHandler];
473                     _WOLog(@"Finished test method %@ (%.4f seconds)", method, -[startMethod timeIntervalSinceNow]);
474                     [pool release];
475                 }
476             }
477         }
478     }
479     @catch (id e)
480     {
481         [self writeError:@"uncaught exception (%@) testing class %@", [NSException WOTest_descriptionForException:e], 
482             NSStringFromClass(aClass)];
483         [self writeLastKnownLocation];
484         noTestFailed = NO;
485         uncaughtExceptions++;
486     }
487     @finally
488     {
489         _WOLog(@"Finished tests for class %@ (%.4f seconds)", NSStringFromClass(aClass), -[startClass timeIntervalSinceNow]);
490     }
491     return noTestFailed;
492 }
493
494 - (NSArray *)testableClasses
495 {
496     // return an array of class names
497     NSMutableArray *testableClasses = [NSMutableArray array];
498     
499     unsigned    classCount                 = 0; // the total number of classes
500     unsigned    conformingClassCount       = 0; // classes conforming to WOTest
501     unsigned    nonconformingClassCount    = 0; // unconforming classes
502     unsigned    excludedClassCount         = 0; // excluded classes
503     unsigned    exceptionCount             = 0; // classes provoking exceptions    
504     
505     int         numClasses                  = 0;
506     int         newNumClasses               = objc_getClassList(NULL, 0);
507     Class       *classes                    = NULL;
508     
509     // get a list of all classes on the system
510     while (numClasses < newNumClasses)
511     {
512         numClasses          = newNumClasses;
513         size_t bufferSize   = sizeof(Class) * numClasses;
514         classes             = realloc(classes, bufferSize);
515         NSAssert1((classes != NULL), @"realloc() failed (size %d)", bufferSize);
516         newNumClasses       = objc_getClassList(classes, numClasses);
517     }
518     
519     @try 
520     {
521         if (classes)
522         {   
523             // skip over some classes because they not only cause exceptions but also spew out ugly console messages
524             SInt32 systemVersion;
525             Gestalt(gestaltSystemVersion, &systemVersion);
526             systemVersion = systemVersion & 0x0000ffff; // Apple instructs to ignore the high-order word    
527
528             NSArray *excludedClasses = (systemVersion < 0x00001040) ?
529                 [NSArray arrayWithObjects: @"Protocol", @"List", @"Object", @"_NSZombie", nil] :                        // 10.3
530                 [NSArray arrayWithObjects: @"Protocol", @"List", @"Object", @"_NSZombie", @"NSATSGlyphGenerator", nil]; // 10.4
531             
532             if ([self verbosity] > 1)
533                 _WOLog(@"Examining classes for WOTest protocol compliance");
534             
535             for (int i = 0; i < newNumClasses; i++)
536             {
537                 classCount++;
538                 
539                 Class       aClass      = classes[i];
540                 NSString    *className  = NSStringFromClass(aClass);
541                 
542                 @try
543                 {
544                     if ([excludedClasses containsObject:className])
545                     {
546                         excludedClassCount++;
547                         if ([self verbosity] > 1)
548                             _WOLog(@"Skipping class %@ (appears in exclusion list)", className);
549                     }
550                     else if ([NSObject WOTest_instancesOfClass:aClass conformToProtocol:@protocol(WOTest)])
551                     {
552                         conformingClassCount++;
553                         [testableClasses addObject:className];
554                         if ([self verbosity] > 0)
555                             _WOLog(@"Class %@ complies with the WOTest protocol", className);
556                     }
557                     else
558                     {
559                         nonconformingClassCount++;
560                         if ([self verbosity] > 1)
561                             _WOLog(@"Class %@ does not comply with the WOTest protocol", className);
562                     }
563                 }
564                 @catch (id exception)
565                 {
566                     exceptionCount++;
567                     // a number of classes are known to provoke exceptions:
568                     if ([self verbosity] > 1)
569                         _WOLog(@"Cannot test protocol compliance for class %@ (caught exception)", className);
570                     continue;
571                 }
572             }
573             free(classes);
574         }
575         
576     }
577     @catch (id e)
578     {
579         _WOLog(@"Uncaught exception...");
580     }
581         
582     _WOLog(@"Runtime Summary:\n"
583            @"Total classes:                                         %d\n"
584            @"Classes which conform to the WOTest protocol:          %d\n"
585            @"Classes which do not conform to the protocol:          %d\n"
586            @"Classes excluded from scanning:                        %d\n"
587            @"Classes that could not be scanned due to exceptions:   %d",
588            classCount,
589            conformingClassCount,
590            nonconformingClassCount,
591            excludedClassCount,
592            exceptionCount);
593     
594     return [testableClasses sortedArrayUsingSelector:@selector(compare:)];
595 }
596
597 - (NSArray *)testableClassesFrom:(NSBundle *)aBundle
598 {
599     NSArray         *allClasses = [self testableClasses];
600     NSMutableArray  *classNames = [NSMutableArray array];
601     
602     if (aBundle)    // only search if actually passed a non-nil bundle
603     {
604         // add only classes that match the passed bundle and conform to WOTest
605         WO_ENUMERATE(allClasses, className)
606         {
607             Class       aClass          = NSClassFromString(className);
608             NSBundle    *classBundle    = [NSBundle bundleForClass:aClass];
609             if ([classBundle isEqualTo:aBundle])
610                 [classNames addObject:className];
611         }
612     }
613     
614     // return autoreleased, immutable NSArray
615     return [NSArray arrayWithArray:classNames];
616 }
617
618 - (NSArray *)testableMethodsFrom:(Class)aClass
619 {
620     // catch crashes caused by passing an "id" instead of a "Class"
621     NSParameterAssert([NSObject WOTest_isRegisteredClass:aClass] || [NSObject WOTest_isMetaClass:aClass]);
622     
623     NSMutableArray *methodNames = [NSMutableArray array];
624     @try 
625     {
626         NSString *prefix = @"-";            // default prefix (instance methods)
627         if (class_isMetaClass(aClass))
628             prefix = @"+";                  // special prefix (class methods)
629         else                                // this is not a metaclass
630         {
631             // get the metaclass; could also use object_getClass
632             Class   metaClass       = object_getClass(aClass);
633             NSArray *classMethods   = [self testableMethodsFrom:metaClass];
634             [methodNames addObjectsFromArray:classMethods];
635         }        
636         
637                 unsigned int count;
638                 Method *methods = class_copyMethodList(aClass, &count);
639                 if (methods)
640                 {
641                         for (unsigned int i = 0, max = count; i < max; i++)
642                         {
643                                 SEL                     aSelector       = method_getName(methods[i]);
644                                 NSString        *name           = NSStringFromSelector(aSelector);
645                                 if (name && [name hasPrefix:@"test"])
646                                         [methodNames addObject:[NSString stringWithFormat:@"%@%@", prefix, name]];
647                         }
648                         free(methods);
649                 }
650     }
651     @catch (id e)
652     {
653         NSString *error = [NSString stringWithFormat:@"exception caught trying to identify testable methods in class %@",
654             NSStringFromClass(aClass)];
655         [self writeError:error];
656     }
657     
658     return [methodNames sortedArrayUsingSelector:@selector(compare:)];
659 }
660
661 - (void)printTestResultsSummary;
662 {
663     [self checkStartDate];  // just in case no tests were run, make sure that startDate is non-nil
664     double      successRate = 0.0;
665     double      failureRate = 0.0;
666     if (testsRun > 0)   // watch out for divide-by-zero if no tests run
667     {
668         successRate = ((double)(testsPassed + testsFailedExpected)      / (double)testsRun) * 100.0;
669         failureRate = ((double)(testsFailed + testsPassedUnexpected)    / (double)testsRun) * 100.0;
670     }
671     _WOLog(@"Run summary:\n"
672            @"Tests run:                         %d\n"
673            @"Tests passed:                      %d + %d expected failures (%.2f%% success rate)\n"
674            @"Tests failed:                      %d + %d unexpected passes (%.2f%% failure rate)\n"
675            @"Uncaught exceptions:               %d\n"
676            @"Low-level exceptions (crashers):   %d + %d expected\n"
677            @"Total run time:                    %.2f seconds\n",
678            testsRun,
679            testsPassed,    testsFailedExpected,    successRate,
680            testsFailed,    testsPassedUnexpected,  failureRate,
681            uncaughtExceptions,
682            lowLevelExceptionsUnexpected, lowLevelExceptionsExpected, 
683            -[[self startDate] timeIntervalSinceNow]);
684     
685     if (testsRun == 0)
686         _WOLog(@"warning: no tests were run\n");
687     
688     // TODO: make Growl notifications optional
689     // TODO: include information about project being tested in Growl notification title
690     // TODO: add options for showing coalesced growl notifications showing individual test failures (with path and line info)
691     // TODO: make clicking on notification bring Xcode to the front, or open the file with the last failure in it etc
692     NSString *status = [NSString stringWithFormat:@"%d tests passed, %d tests failed", 
693         testsPassed + testsFailedExpected, testsFailed + testsPassedUnexpected];
694         
695     if ([self testsWereSuccessful])
696         [self growlNotifyTitle:@"WOTest run successful" message:status isWarning:NO sticky:NO];
697     else
698     {
699         _WOLog(@"error: testing did not complete without errors\n");    
700         [self growlNotifyTitle:@"WOTest run failed" message:status isWarning:YES sticky:YES];
701         
702     }
703     
704     // reset start date
705     [self setStartDate:nil];
706 }
707
708 - (BOOL)testsWereSuccessful
709 {
710     return ((testsFailed + testsPassedUnexpected + uncaughtExceptions + lowLevelExceptionsUnexpected) == 0);
711 }
712
713 #pragma mark -
714 #pragma mark Low-level exception handling
715
716 - (void)installLowLevelExceptionHandler
717 {
718     if (!lowLevelExceptionHandlerInstalled)
719     {
720         WOOldLowLevelExceptionHandler = InstallExceptionHandler(NewExceptionHandlerUPP(WOLowLevelExceptionHandler));
721         lowLevelExceptionHandlerInstalled = YES;
722     }
723 }
724
725 - (void)removeLowLevelExceptionHandler
726 {        
727     if (lowLevelExceptionHandlerInstalled)
728     {
729         DisposeExceptionHandlerUPP(InstallExceptionHandler(WOOldLowLevelExceptionHandler));
730         lowLevelExceptionHandlerInstalled = NO;
731     }
732 }
733
734 #pragma mark -
735 #pragma mark Growl support
736
737 - (void)growlNotifyTitle:(NSString *)title message:(NSString *)message isWarning:(BOOL)isWarning sticky:(BOOL)sticky
738 {
739     NSParameterAssert(title != nil);
740     NSParameterAssert(message != nil);
741     
742     // clean up enviroment a bit (hides possible warnings caused if these set for WOTestRunner)
743     NSMutableDictionary *environment = [NSMutableDictionary dictionaryWithDictionary:[[NSProcessInfo processInfo] environment]];
744     [environment removeObjectForKey:@"DYLD_INSERT_LIBRARIES"];
745     [environment removeObjectForKey:@"WOTestBundleInjector"];
746     
747     NSTask *task = [[[NSTask alloc] init] autorelease];
748     [task setLaunchPath:@"/usr/bin/env"];   // use env so as to pick up PATH, if set
749     [task setEnvironment:environment];
750     NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"growlnotify",
751         @"--name",      @"com.wincent.WOTest", 
752         @"--appIcon",   @"Xcode", 
753         @"--priority",  (isWarning ? @"2" : @"0"),
754         @"--message",   message,
755         title,          nil];
756     if (sticky) [arguments insertObject:@"--sticky" atIndex:0];
757     [task setArguments:arguments];
758     @try
759     {
760         [task launch];
761         [task waitUntilExit];
762     }
763     @catch (id e)
764     {
765         // ignore: growlnotify may not be installed or the PATH environment variable may not be correctly set
766     }
767 }
768
769 #pragma mark -
770 #pragma mark Logging methods
771
772 - (NSString *)trimmedPath:(char *)path
773 {
774     NSParameterAssert(path != NULL);
775     NSString *pathString = [NSString stringWithUTF8String:path];
776     
777     unsigned trim = [self trimInitialPathComponents];
778     if (trim == 0) return pathString;
779     if (![pathString isAbsolutePath]) return pathString;    // only trim absolute paths
780     NSArray *components = [pathString pathComponents];      // note: Cocoa returns "/" here as an additional first component
781     NSAssert(components != nil, @"components != nil");
782     unsigned count = [components count];
783     if (count < trim + 2) return pathString;                // only trim if there will be at least one component left over
784     return [NSString pathWithComponents:[components subarrayWithRange:NSMakeRange(trim + 1, count - trim - 1)]];
785 }
786
787 - (void)writePassed:(BOOL)passed 
788              inFile:(char *)path 
789              atLine:(int)line
790             message:(NSString *)message, ...
791 {
792     testsRun++;
793     va_list args;
794     va_start(args, message);
795     NSString *string = [NSString WOTest_stringWithFormat:message arguments:args];
796     va_end(args);
797     if ([self expectFailures])  // invert sense of tests (ie. failure is good)
798     {
799         if (passed) // passed: bad
800         {
801             [self writeErrorInFile:path atLine:line message:[NSString stringWithFormat:@"Passed (unexpected pass): %@", string]];
802             testsPassedUnexpected++;
803         }
804         else        // failed: good
805         {
806             [self writeStatusInFile:path atLine:line message:[NSString stringWithFormat:@"Failed (expected failure): %@", string]];
807             testsFailedExpected++;
808         }
809     }
810     else    // normal handling (ie. passing is good, failing is bad)
811     {
812         if (passed) // passed: good
813         {
814             [self writeStatusInFile:path atLine:line message:[NSString stringWithFormat:@"Passed: %@", string]];
815             testsPassed++;
816         }
817         else        // failed: bad
818         {
819             [self writeErrorInFile:path atLine:line message:[NSString stringWithFormat:@"Failed: %@", string]];
820             testsFailed++;
821         }    
822     }
823 }
824
825 - (void)cacheFile:(char *)path line:(int)line
826 {
827     [self setLastReportedFile:[self trimmedPath:path]];
828     lastReportedLine = line;
829 }
830
831 - (void)writeLastKnownLocation
832 {
833     NSString *path = [self lastReportedFile];
834     if (path)
835         _WOLog(@"%@:%d: last known location was %@:%d", path, lastReportedLine, path, lastReportedLine);
836 }
837
838 - (void)writeErrorInFile:(char *)path atLine:(int)line message:(NSString *)message, ...
839 {
840     va_list args;
841     va_start(args, message);
842     NSString *error = [NSString WOTest_stringWithFormat:message arguments:args];
843     _WOLog(@"%@:%d: error: %@", [self trimmedPath:path], line, error);
844     [self cacheFile:path line:line];
845     va_end(args);
846 }
847
848 - (void)writeWarningInFile:(char *)path atLine:(int)line message:(NSString *)message, ... 
849 {
850     va_list args;
851     va_start(args, message);
852     NSString *warning = [NSString WOTest_stringWithFormat:message arguments:args];
853     _WOLog(@"%@:%d: warning: %@", [self trimmedPath:path], line, warning);
854     [self cacheFile:path line:line];
855     va_end(args);
856 }
857
858 - (void)writeUncaughtException:(NSString *)info inFile:(char *)path atLine:(int)line
859 {
860     _WOLog(@"%@:%d: error: uncaught exception during test execution: %@", [self trimmedPath:path], line, info);
861     uncaughtExceptions++;
862 }
863
864 - (void)writeStatusInFile:(char *)path atLine:(int)line message:(NSString *)message, ... 
865 {
866     va_list args;
867     va_start(args, message);
868     NSString *status = [NSString WOTest_stringWithFormat:message arguments:args];
869     _WOLog(@"%@:%d %@", [self trimmedPath:path], line, status); // omit colin after line number or Xcode will show this as an error
870     [self cacheFile:path line:line];
871     va_end(args);
872 }
873
874 - (void)writeStatus:(NSString *)message, ...
875 {
876     va_list args;
877     va_start(args, message);
878     NSString *status = [NSString WOTest_stringWithFormat:message arguments:args];
879     _WOLog(@"%@", status);
880     va_end(args);    
881 }
882
883 - (void)writeWarning:(NSString *)message, ...
884 {
885     va_list args;
886     va_start(args, message);
887     NSString *warning = [NSString WOTest_stringWithFormat:message arguments:args];
888     _WOLog(@"warning: %@", warning); // older versions of Xcode required initial colons "::" to show this as a warning
889     va_end(args);    
890 }
891
892 - (void)writeError:(NSString *)message, ...
893 {
894     va_list args;
895     va_start(args, message);
896     NSString *error = [NSString WOTest_stringWithFormat:message arguments:args];
897     _WOLog(@"error: %@", error); // older versions of Xcode required initial colons "::" to show this as an error
898     va_end(args);
899 }
900
901 #pragma mark - 
902 #pragma mark Empty (do-nothing) test methods
903
904 - (void)passTestInFile:(char *)path atLine:(int)line
905 {
906     [self writePassed:YES inFile:path atLine:line message:@"(always passes)"];
907 }
908
909 - (void)failTestInFile:(char *)path atLine:(int)line
910 {
911     [self writePassed:NO inFile:path atLine:line message:@"(always fails)"];
912 }
913
914 #pragma mark - 
915 #pragma mark Boolean test methods
916
917 - (void)testTrue:(BOOL)expr inFile:(char *)path atLine:(int)line
918 {
919     [self writePassed:expr inFile:path atLine:line message:@"expected YES, got %@", (expr ? @"YES" : @"NO")];
920 }
921
922 - (void)testFalse:(BOOL)expr inFile:(char *)path atLine:(int)line
923 {
924     [self writePassed:!expr inFile:path atLine:line message:@"expected NO, got %@", (expr ? @"YES" : @"NO")];
925 }
926
927 #pragma mark -
928 #pragma mark NSValue-based tests
929
930 - (void)testValue:(NSValue *)actual isEqualTo:(NSValue *)expected inFile:(char *)path atLine:(int)line
931 {
932     NSParameterAssert(actual);
933     NSParameterAssert(expected);
934     BOOL equal = NO;
935         
936     // NSValue category will throw an exception for invalid input(s)
937     @try {
938         equal = [actual WOTest_testIsEqualToValue:expected];
939     }
940     @catch (id e) {
941         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
942         uncaughtExceptions++;
943     }
944     BOOL expectedTruncated, actualTruncated;
945     [self writePassed:equal inFile:path atLine:line message:@"expected %@, got %@", WO_DESC(expected), WO_DESC(actual)];
946     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
947     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
948 }
949
950 - (void)testValue:(NSValue *)actual isNotEqualTo:(NSValue *)expected inFile:(char *)path atLine:(int)line
951 {
952     NSParameterAssert(actual);
953     NSParameterAssert(expected);
954     BOOL equal = NO;
955     
956     // NSValue category will throw an exception for invalid input(s)
957     @try {
958         equal = [actual WOTest_testIsEqualToValue:expected];
959     }
960     @catch (id e) {
961         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
962         uncaughtExceptions++;
963     }
964     BOOL expectedTruncated, actualTruncated;
965     [self writePassed:(!equal) inFile:path atLine:line message:@"expected (not) %@, got %@", WO_DESC(expected), WO_DESC(actual)];
966     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
967     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
968 }
969
970 - (void)testValue:(NSValue *)actual greaterThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
971 {
972     NSParameterAssert(actual);
973     NSParameterAssert(expected);
974     BOOL greaterThan = NO;
975     
976     // NSValue category will throw an exception for invalid input(s)
977     @try {
978         greaterThan = [actual WOTest_testIsGreaterThanValue:expected];
979     }
980     @catch (id e) {
981         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
982         uncaughtExceptions++;
983     }
984     BOOL expectedTruncated, actualTruncated;
985     [self writePassed:greaterThan inFile:path atLine:line message:@"expected > %@, got %@", WO_DESC(expected), WO_DESC(actual)];
986     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
987     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
988 }
989
990 - (void)testValue:(NSValue *)actual notGreaterThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
991 {
992     NSParameterAssert(actual);
993     NSParameterAssert(expected);
994     BOOL notGreaterThan = NO;
995     
996     // NSValue category will throw an exception for invalid input(s)
997     @try {
998         notGreaterThan = [actual WOTest_testIsNotGreaterThanValue:expected];
999     }
1000     @catch (id e) {
1001         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
1002         uncaughtExceptions++;
1003     }
1004     BOOL expectedTruncated, actualTruncated;
1005     [self writePassed:notGreaterThan inFile:path atLine:line message:@"expected <= %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1006     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1007     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1008 }
1009
1010 - (void)testValue:(NSValue *)actual lessThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
1011 {
1012     NSParameterAssert(actual);
1013     NSParameterAssert(expected);
1014     BOOL lessThan = NO;
1015     
1016     // NSValue category will throw an exception for invalid input(s)
1017     @try {
1018         lessThan = [actual WOTest_testIsLessThanValue:expected];
1019     }
1020     @catch (id e) {
1021         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
1022         uncaughtExceptions++;
1023     }
1024     BOOL expectedTruncated, actualTruncated;
1025     [self writePassed:lessThan inFile:path atLine:line message:@"expected < %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1026     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1027     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1028 }
1029
1030 - (void)testValue:(NSValue *)actual notLessThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
1031 {
1032     NSParameterAssert(actual);
1033     NSParameterAssert(expected);
1034     BOOL notLessThan = NO;
1035     
1036     // NSValue category will throw an exception for invalid input(s)
1037     @try {
1038         notLessThan = [actual WOTest_testIsNotLessThanValue:expected];
1039     }
1040     @catch (id e) {
1041         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
1042         uncaughtExceptions++;
1043     }
1044     BOOL expectedTruncated, actualTruncated;
1045     [self writePassed:notLessThan inFile:path atLine:line message:@"expected >= %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1046     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1047     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1048 }
1049
1050 #pragma mark -
1051 #pragma mark Pointer to void test methods
1052
1053 - (void)testNil:(void *)pointer inFile:(char *)path atLine:(int)line
1054 {
1055     BOOL result = (pointer ? NO : YES);
1056     [self writePassed:result inFile:path atLine:line message:@"expected nil, got %@", 
1057         (result ? @"nil" : [NSString stringWithFormat:@"%x", pointer])];
1058 }
1059
1060 - (void)testNotNil:(void *)pointer inFile:(char *)path atLine:(int)line
1061 {
1062     BOOL result = (pointer ? YES : NO);
1063     [self writePassed:result inFile:path atLine:line message:@"expected (not) nil, got %@",
1064         (result ? [NSString stringWithFormat:@"%x", pointer] : @"nil")];
1065 }
1066
1067 - (void)testPointer:(void *)actual isEqualTo:(void *)expected inFile:(char *)path atLine:(int)line
1068 {
1069     BOOL result = (actual == expected);
1070     [self writePassed:result inFile:path atLine:line message:@"expected %x, got %x", expected, actual];
1071 }
1072
1073 - (void)testPointer:(void *)actual isNotEqualTo:(void *)expected inFile:(char *)path atLine:(int)line
1074 {
1075     BOOL result = (actual != expected);
1076     [self writePassed:result inFile:path atLine:line message:@"expected (not) %x, got %x", expected, actual];
1077 }
1078
1079 #pragma mark -
1080 #pragma mark int test methods
1081
1082 - (void)testIsInt:(char *)type inFile:(char *)path atLine:(int)line 
1083 {
1084     BOOL result = (strcmp(type, "i") == 0); 
1085     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"i\", got \"%s\"", type]]; 
1086 }
1087
1088 - (void)testIsNotInt:(char *)type inFile:(char *)path atLine:(int)line
1089 {
1090     BOOL result = (strcmp(type, "i") != 0);
1091     [self writePassed:result
1092                inFile:path
1093                atLine:line
1094               message:[NSString stringWithFormat:@"expected type (not) \"i\", got \"%s\"", type]];
1095 }
1096
1097 - (void)testIntPositive:(int)aInt inFile:(char *)path atLine:(int)line
1098 {
1099     [self testInt:aInt greaterThan:(int)0 inFile:path atLine:line];
1100 }
1101
1102 - (void)testIntNegative:(int)aInt inFile:(char *)path atLine:(int)line
1103 {
1104     [self testInt:aInt lessThan:(int)0 inFile:path atLine:line];
1105 }
1106
1107 - (void)testIntZero:(int)aInt inFile:(char *)path atLine:(int)line
1108 {
1109     [self testInt:aInt isEqualTo:(int)0 inFile:path atLine:line];
1110 }
1111
1112 - (void)testIntNotZero:(int)aInt inFile:(char *)path atLine:(int)line
1113 {
1114     [self testInt:aInt isNotEqualTo:(int)0 inFile:path atLine:line];
1115 }
1116
1117 - (void)testInt:(int)actual isEqualTo:(int)expected inFile:(char *)path atLine:(int)line 
1118 {
1119     BOOL result = (actual == expected); 
1120     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat:@"expected %d, got %d", expected, actual]]; 
1121 }
1122
1123 - (void)testInt:(int)actual isNotEqualTo:(int)expected inFile:(char *)path atLine:(int)line 
1124 {
1125     BOOL result = (actual != expected); 
1126     [self writePassed:result 
1127                inFile:path 
1128                atLine:line 
1129               message:[NSString stringWithFormat:@"expected (not) %d, got %d", expected, actual]]; 
1130 }
1131
1132 - (void)testInt:(int)actual greaterThan:(int)expected inFile:(char *)path atLine:(int)line 
1133 {
1134     BOOL result = (actual > expected); 
1135     [self writePassed:result 
1136                inFile:path 
1137                atLine:line 
1138               message:[NSString stringWithFormat:@"expected > %d, got %d", expected, actual]]; 
1139 }
1140
1141 - (void)testInt:(int)actual notGreaterThan:(int)expected inFile:(char *)path atLine:(int)line 
1142 {
1143     BOOL result = (actual <= expected); 
1144     [self writePassed:result 
1145                inFile:path 
1146                atLine:line 
1147               message:[NSString stringWithFormat:@"expected <= %d, got %d", expected, actual]]; 
1148 }
1149
1150 - (void)testInt:(int)actual lessThan:(int)expected inFile:(char *)path atLine:(int)line 
1151 {
1152     BOOL result = (actual < expected); 
1153     [self writePassed:result 
1154                inFile:path 
1155                atLine:line 
1156               message:[NSString stringWithFormat:@"expected < %d, got %d", expected, actual]]; 
1157 }
1158
1159 - (void)testInt:(int)actual notLessThan:(int)expected inFile:(char *)path atLine:(int)line 
1160 {
1161     BOOL result = (actual >= expected); 
1162     [self writePassed:result 
1163                inFile:path 
1164                atLine:line 
1165               message:[NSString stringWithFormat:@"expected >= %d, got %d", expected, actual]]; 
1166 }
1167
1168 #pragma mark -
1169 #pragma mark unsigned test methods
1170
1171 - (void)testIsUnsigned:(char *)type inFile:(char *)path atLine:(int)line 
1172 {
1173     BOOL result = (strcmp(type, "I") == 0); 
1174     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"I\", got \"%s\"", type]]; 
1175 }
1176
1177 - (void)testIsNotUnsigned:(char *)type inFile:(char *)path atLine:(int)line
1178 {
1179     BOOL result = (strcmp(type, "I") != 0);
1180     [self writePassed:result
1181                inFile:path
1182                atLine:line
1183               message:[NSString stringWithFormat:@"expected type (not) \"I\", got \"%s\"", type]];
1184 }
1185
1186 - (void)testUnsignedZero:(unsigned)aUnsigned inFile:(char *)path atLine:(int)line 
1187 {
1188     return [self testUnsigned:aUnsigned isEqualTo:(unsigned)0 inFile:path atLine:line]; 
1189 }
1190
1191 - (void)testUnsignedNotZero:(unsigned)aUnsigned inFile:(char *)path atLine:(int)line 
1192 {
1193     return [self testUnsigned:aUnsigned isNotEqualTo:(unsigned)0 inFile:path atLine:line]; 
1194 }
1195
1196 - (void)testUnsigned:(unsigned)actual isEqualTo:(unsigned)expected inFile:(char *)path atLine:(int)line
1197 {
1198     BOOL result = (actual == expected);    
1199     [self writePassed:result 
1200                inFile:path 
1201                atLine:line
1202               message:[NSString stringWithFormat:@"expected %u, got %u", expected, actual]];
1203 }
1204
1205 - (void)testUnsigned:(unsigned)actual isNotEqualTo:(unsigned)expected inFile:(char *)path atLine:(int)line 
1206 {
1207     BOOL result = (actual != expected);    
1208     [self writePassed:result 
1209                inFile:path 
1210                atLine:line
1211               message:[NSString stringWithFormat:@"expected (not) %u, got %u", expected, actual]];
1212 }
1213
1214 - (void)testUnsigned:(unsigned)actual greaterThan:(unsigned)expected inFile:(char *)path atLine:(int)line 
1215 {
1216     BOOL result = (actual > expected);    
1217     [self writePassed:result 
1218                inFile:path 
1219                atLine:line
1220               message:[NSString stringWithFormat:@"expected > %u, got %u", expected, actual]];
1221 }
1222
1223 - (void)testUnsigned:(unsigned)actual notGreaterThan:(unsigned)expected inFile:(char *)path atLine:(int)line 
1224 {
1225     BOOL result = (actual <= expected); 
1226     [self writePassed:result 
1227                inFile:path 
1228                atLine:line 
1229               message:[NSString stringWithFormat:@"expected <= %u, got %u", expected, actual]]; 
1230 }
1231
1232 - (void)testUnsigned:(unsigned)actual lessThan:(unsigned)expected inFile:(char *)path atLine:(int)line 
1233 {
1234     BOOL result = (actual < expected); 
1235     [self writePassed:result 
1236                inFile:path 
1237                atLine:line 
1238               message:[NSString stringWithFormat:@"expected < %u, got %u", expected, actual]]; 
1239 }
1240
1241 - (void)testUnsigned:(unsigned)actual notLessThan:(unsigned)expected inFile:(char *)path atLine:(int)line 
1242 {
1243     BOOL result = (actual >= expected); 
1244     [self writePassed:result 
1245                inFile:path 
1246                atLine:line 
1247               message:[NSString stringWithFormat:@"expected >= %u, got %u", expected, actual]]; 
1248 }
1249
1250 #pragma mark -
1251 #pragma mark float test methods without error margins
1252
1253 - (void)testIsFloat:(char *)type inFile:(char *)path atLine:(int)line
1254 {
1255     BOOL result = (strcmp(type, "f") == 0);
1256     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"f\", got \"%s\"", type]]; 
1257 }
1258
1259 - (void)testIsNotFloat:(char *)type inFile:(char *)path atLine:(int)line
1260 {
1261     BOOL result = (strcmp(type, "f") != 0);
1262     [self writePassed:result
1263                inFile:path
1264                atLine:line
1265               message:[NSString stringWithFormat:@"expected type (not) \"f\", got \"%s\"", type]];
1266 }
1267
1268 - (void)testFloatPositive:(float)aFloat inFile:(char *)path atLine:(int)line 
1269 {
1270     return [self testFloat:aFloat greaterThan:(float)0.0 withinError:(float)0.0 inFile:path atLine:line]; 
1271 }
1272
1273 - (void)testFloatNegative:(float)aFloat inFile:(char *)path atLine:(int)line 
1274 {
1275     return [self testFloat:aFloat lessThan:(float)0.0 withinError:(float)0.0 inFile:path atLine:line]; 
1276 }
1277
1278 - (void)testFloatZero:(float)aFloat inFile:(char *)path atLine:(int)line 
1279 {
1280     return [self testFloat:aFloat isEqualTo:(float)0.0 withinError:(float)0.0 inFile:path atLine:line]; 
1281 }
1282
1283 - (void)testFloatNotZero:(float)aFloat inFile:(char *)path atLine:(int)line 
1284 {
1285     return [self testFloat:aFloat isNotEqualTo:(float)0.0 withinError:(float)0.0 inFile:path atLine:line]; 
1286 }
1287
1288 - (void)testFloat:(float)actual isEqualTo:(float)expected inFile:(char *)path atLine:(int)line 
1289 {
1290     return [self testFloat:actual isEqualTo:expected withinError:(float)0.0 inFile:path atLine:line]; 
1291 }
1292
1293 - (void)testFloat:(float)actual isNotEqualTo:(float)expected inFile:(char *)path atLine:(int)line 
1294 {
1295     return [self testFloat:actual isNotEqualTo:expected withinError:(float)0.0 inFile:path atLine:line]; 
1296 }
1297
1298 - (void)testFloat:(float)actual greaterThan:(float)expected inFile:(char *)path atLine:(int)line 
1299 {
1300     return [self testFloat:actual greaterThan:expected withinError:(float)0.0 inFile:path atLine:line]; 
1301 }
1302
1303 - (void)testFloat:(float)actual notGreaterThan:(float)expected inFile:(char *)path atLine:(int)line 
1304 {
1305     return [self testFloat:actual notGreaterThan:expected withinError:(float)0.0 inFile:path atLine:line]; 
1306 }
1307
1308 - (void)testFloat:(float)actual lessThan:(float)expected inFile:(char *)path atLine:(int)line 
1309 {
1310     return [self testFloat:actual lessThan:expected withinError:(float)0.0 inFile:path atLine:line]; 
1311 }
1312
1313 - (void)testFloat:(float)actual notLessThan:(float)expected inFile:(char *)path atLine:(int)line 
1314 {
1315     return [self testFloat:actual notLessThan:expected withinError:(float)0.0 inFile:path atLine:line]; 
1316 }
1317
1318 #pragma mark -
1319 #pragma mark float test methods with error margins
1320
1321 - (void)testFloatPositive:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line 
1322 {
1323     return [self testFloat:aFloat greaterThan:(float)0.0 withinError:error inFile:path atLine:line]; 
1324 }
1325
1326 - (void)testFloatNegative:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line 
1327 {
1328     return [self testFloat:aFloat lessThan:(float)0.0 withinError:error inFile:path atLine:line]; 
1329 }
1330
1331 - (void)testFloatZero:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line 
1332 {
1333     return [self testFloat:aFloat isEqualTo:(float)0.0 withinError:error inFile:path atLine:line]; 
1334 }
1335
1336 - (void)testFloatNotZero:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line 
1337 {
1338     return [self testFloat:aFloat isNotEqualTo:(float)0.0 withinError:error inFile:path atLine:line]; 
1339 }
1340
1341 - (void)testFloat:(float)actual isEqualTo:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line 
1342 {
1343     BOOL result = (fabsf(actual - expected) <= error);   
1344     [self writePassed:result 
1345                inFile:path 
1346                atLine:line
1347               message:
1348         [NSString stringWithFormat:@"expected %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1349 }
1350
1351 - (void)testFloat:(float)actual isNotEqualTo:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line 
1352 {
1353     BOOL result = (fabsf(actual - expected) > -error);   
1354     [self writePassed:result 
1355                inFile:path 
1356                atLine:line
1357               message:
1358         [NSString stringWithFormat:@"expected (not) %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1359 }
1360
1361 - (void)testFloat:(float)actual greaterThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1362 {
1363     BOOL result = ((actual - expected) > -error);
1364     [self writePassed:result 
1365                inFile:path 
1366                atLine:line
1367               message:
1368         [NSString stringWithFormat:@"expected > %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1369 }
1370
1371 - (void)testFloat:(float)actual notGreaterThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1372 {
1373     BOOL result = ((actual - expected) <= error);
1374     [self writePassed:result 
1375                inFile:path 
1376                atLine:line
1377               message:
1378         [NSString stringWithFormat:@"expected <= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1379 }
1380
1381 - (void)testFloat:(float)actual lessThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line 
1382 {
1383     BOOL result = ((actual - expected) < -error);
1384     [self writePassed:result 
1385                inFile:path 
1386                atLine:line
1387               message:
1388         [NSString stringWithFormat:@"expected < %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1389 }
1390
1391 - (void)testFloat:(float)actual notLessThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line 
1392 {
1393     BOOL result = ((actual - expected) >= error);
1394     [self writePassed:result 
1395                inFile:path 
1396                atLine:line
1397               message:
1398         [NSString stringWithFormat:@"expected >= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1399 }
1400
1401 #pragma mark -
1402 #pragma mark double test methods without error margins
1403
1404 - (void)testIsDouble:(char *)type inFile:(char *)path atLine:(int)line
1405 {
1406     BOOL result = (strcmp(type, "d") == 0);
1407     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"d\", got \"%s\"", type]]; 
1408 }
1409
1410 - (void)testIsNotDouble:(char *)type inFile:(char *)path atLine:(int)line
1411 {
1412     BOOL result = (strcmp(type, "d") != 0);
1413     [self writePassed:result
1414                inFile:path
1415                atLine:line
1416               message:[NSString stringWithFormat:@"expected type (not) \"d\", got \"%s\"", type]];
1417 }
1418
1419 - (void)testDoublePositive:(double)aDouble inFile:(char *)path atLine:(int)line 
1420 {
1421     return [self testDouble:aDouble greaterThan:(double)0.0 withinError:(double)0.0 inFile:path atLine:line]; 
1422 }
1423
1424 - (void)testDoubleNegative:(double)aDouble inFile:(char *)path atLine:(int)line 
1425 {
1426     return [self testDouble:aDouble lessThan:(double)0.0 withinError:(double)0.0 inFile:path atLine:line]; 
1427 }
1428
1429 - (void)testDoubleZero:(double)aDouble inFile:(char *)path atLine:(int)line 
1430 {
1431     return [self testDouble:aDouble isEqualTo:(double)0.0 withinError:(double)0.0 inFile:path atLine:line]; 
1432 }
1433
1434 - (void)testDoubleNotZero:(double)aDouble inFile:(char *)path atLine:(int)line 
1435 {
1436     return [self testDouble:aDouble isNotEqualTo:(double)0.0 withinError:(double)0.0 inFile:path atLine:line]; 
1437 }
1438
1439 - (void)testDouble:(double)actual isEqualTo:(double)expected inFile:(char *)path atLine:(int)line 
1440 {
1441     return [self testDouble:actual isEqualTo:expected withinError:(double)0.0 inFile:path atLine:line]; 
1442 }
1443
1444 - (void)testDouble:(double)actual isNotEqualTo:(double)expected inFile:(char *)path atLine:(int)line 
1445 {
1446     return [self testDouble:actual isNotEqualTo:expected withinError:(double)0.0 inFile:path atLine:line]; 
1447 }
1448
1449 - (void)testDouble:(double)actual greaterThan:(double)expected inFile:(char *)path atLine:(int)line 
1450 {
1451     return [self testDouble:actual greaterThan:expected withinError:(double)0.0 inFile:path atLine:line]; 
1452 }
1453
1454 - (void)testDouble:(double)actual notGreaterThan:(double)expected inFile:(char *)path atLine:(int)line 
1455 {
1456     return [self testDouble:actual notGreaterThan:expected withinError:(double)0.0 inFile:path atLine:line]; 
1457 }
1458
1459 - (void)testDouble:(double)actual lessThan:(double)expected inFile:(char *)path atLine:(int)line 
1460 {
1461     return [self testDouble:actual lessThan:expected withinError:(double)0.0 inFile:path atLine:line]; 
1462 }
1463
1464 - (void)testDouble:(double)actual notLessThan:(double)expected inFile:(char *)path atLine:(int)line 
1465 {
1466     return [self testDouble:actual notLessThan:expected withinError:(double)0.0 inFile:path atLine:line]; 
1467 }
1468
1469 #pragma mark -
1470 #pragma mark double test methods with error margins
1471
1472 - (void)testDoublePositive:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line 
1473 {
1474     return [self testDouble:aDouble greaterThan:(double)0.0 withinError:error inFile:path atLine:line]; 
1475 }
1476
1477 - (void)testDoubleNegative:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line 
1478 {
1479     return [self testDouble:aDouble lessThan:(double)0.0 withinError:error inFile:path atLine:line]; 
1480 }
1481
1482 - (void)testDoubleZero:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line 
1483 {
1484     return [self testDouble:aDouble isEqualTo:(double)0.0 withinError:error inFile:path atLine:line]; 
1485 }
1486
1487 - (void)testDoubleNotZero:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line 
1488 {
1489     return [self testDouble:aDouble isNotEqualTo:(double)0.0 withinError:error inFile:path atLine:line]; 
1490 }
1491
1492 - (void)testDouble:(double)actual isEqualTo:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line 
1493 {
1494     BOOL result = (fabs(actual - expected) <= error);   
1495     [self writePassed:result 
1496                inFile:path 
1497                atLine:line
1498               message:
1499         [NSString stringWithFormat:@"expected %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1500 }
1501
1502 - (void)testDouble:(double)actual isNotEqualTo:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line 
1503 {
1504     BOOL result = (fabs(actual - expected) > -error);   
1505     [self writePassed:result 
1506                inFile:path 
1507                atLine:line
1508               message:
1509         [NSString stringWithFormat:@"expected (not) %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1510 }
1511
1512 - (void)testDouble:(double)actual greaterThan:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line
1513 {
1514     BOOL result = ((actual - expected) > -error);
1515     [self writePassed:result
1516                inFile:path
1517                atLine:line
1518               message:
1519         [NSString stringWithFormat:@"expected > %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1520 }
1521
1522 - (void)testDouble:(double)actual notGreaterThan:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line 
1523 {
1524     BOOL result = ((actual - expected) <= error);
1525     [self writePassed:result
1526                inFile:path
1527                atLine:line
1528               message:
1529         [NSString stringWithFormat:@"expected <= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1530 }
1531
1532 - (void)testDouble:(double)actual lessThan:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line 
1533 {
1534     BOOL result = ((actual - expected) < -error);
1535     [self writePassed:result
1536                inFile:path
1537                atLine:line
1538               message:
1539         [NSString stringWithFormat:@"expected < %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1540 }
1541
1542 - (void)testDouble:(double)actual notLessThan:(double)expected withinError:(float)error inFile:(char *)path atLine:(int)line 
1543 {
1544     BOOL result = ((actual - expected) >= error);
1545     [self writePassed:result
1546                inFile:path
1547                atLine:line
1548               message:
1549         [NSString stringWithFormat:@"expected >= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1550 }
1551
1552 #pragma mark -
1553 #pragma mark object test methods
1554
1555 - (void)testObject:(id)actual isEqualTo:(id)expected inFile:(char *)path atLine:(int)line
1556 {
1557     BOOL equal = NO;
1558     if (!actual && !expected) equal = YES; // equal (both nil)
1559     else if (actual) equal = [actual isEqual:expected];
1560     BOOL expectedTruncated, actualTruncated;
1561     [self writePassed:equal inFile:path atLine:line message:@"expected \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1562     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1563     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1564 }
1565
1566 - (void)testObject:(id)actual isNotEqualTo:(id)expected inFile:(char *)path atLine:(int)line 
1567 {
1568     BOOL equal = NO;
1569     if (!actual && !expected) equal = YES; // equal (both nil)
1570     else if (actual) equal = [actual isEqual:expected];
1571     BOOL expectedTruncated, actualTruncated;
1572     [self writePassed:(!equal)
1573                inFile:path
1574                atLine:line
1575               message:@"expected (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1576     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1577     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1578 }
1579
1580 #pragma mark -
1581 #pragma mark NSString test methods
1582
1583 - (void)testString:(NSString *)actual isEqualTo:(NSString *)expected inFile:(char *)path atLine:(int)line 
1584 {
1585     if (actual && ![actual isKindOfClass:[NSString class]])
1586         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1587                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1588                            inFile:path
1589                            atLine:line];
1590     if (expected && ![expected isKindOfClass:[NSString class]])
1591         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1592                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1593                            inFile:path
1594                            atLine:line];
1595     BOOL equal = NO;
1596     if (!actual && !expected) equal = YES; // equal (both nil) 
1597     else if (actual) equal = [actual isEqualToString:expected];    
1598     BOOL expectedTruncated, actualTruncated;
1599     [self writePassed:equal inFile:path atLine:line message:@"expected \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)]; 
1600     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1601     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1602 }
1603
1604 - (void)testString:(NSString *)actual isNotEqualTo:(NSString *)expected inFile:(char *)path atLine:(int)line
1605 {
1606     if (actual && ![actual isKindOfClass:[NSString class]])
1607         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1608                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1609                            inFile:path
1610                            atLine:line];
1611     if (expected && ![expected isKindOfClass:[NSString class]])
1612         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1613                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1614                            inFile:path
1615                            atLine:line];
1616     BOOL equal = NO;
1617     if (!actual && !expected) equal = YES; // equal (both nil) 
1618     else if (actual) equal = [actual isEqualToString:expected];
1619     BOOL expectedTruncated, actualTruncated;
1620     [self writePassed:equal
1621                inFile:path
1622                atLine:line
1623               message:@"expected (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1624     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1625     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1626 }
1627
1628 - (void)testString:(NSString *)actual hasPrefix:(NSString *)expected inFile:(char *)path atLine:(int)line 
1629 {
1630     if (!expected)
1631         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1632                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1633                            inFile:path
1634                            atLine:line];
1635     if (actual && ![actual isKindOfClass:[NSString class]])
1636         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1637                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1638                            inFile:path
1639                            atLine:line];
1640     if (![expected isKindOfClass:[NSString class]])
1641         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1642                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1643                            inFile:path
1644                            atLine:line];
1645     BOOL result = actual ? NO : [actual hasPrefix:expected]; 
1646     BOOL expectedTruncated, actualTruncated;
1647     [self writePassed:result
1648                inFile:path
1649                atLine:line
1650               message:@"expected prefix \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1651     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1652     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1653 }
1654
1655 - (void)testString:(NSString *)actual doesNotHavePrefix:(NSString *)expected inFile:(char *)path atLine:(int)line 
1656 {
1657     if (!expected)
1658         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1659                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1660                            inFile:path
1661                            atLine:line];
1662     if (actual && ![actual isKindOfClass:[NSString class]])
1663         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1664                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1665                            inFile:path
1666                            atLine:line];
1667     if (![expected isKindOfClass:[NSString class]])
1668         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1669                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1670                            inFile:path
1671                            atLine:line];
1672     BOOL result = actual ? (![actual hasPrefix:expected]) : NO; 
1673     BOOL expectedTruncated, actualTruncated;
1674     [self writePassed:result
1675                inFile:path
1676                atLine:line
1677               message:@"expected prefix (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1678     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1679     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1680 }
1681
1682 - (void)testString:(NSString *)actual hasSuffix:(NSString *)expected inFile:(char *)path atLine:(int)line 
1683 {
1684     if (!expected)
1685         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1686                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1687                            inFile:path
1688                            atLine:line];
1689     if (actual && ![actual isKindOfClass:[NSString class]])
1690         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1691                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1692                            inFile:path
1693                            atLine:line];
1694     if (![expected isKindOfClass:[NSString class]])
1695         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1696                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1697                            inFile:path
1698                            atLine:line];
1699     BOOL result = actual ? NO : [actual hasSuffix:expected]; 
1700     BOOL expectedTruncated, actualTruncated;
1701     [self writePassed:result
1702                inFile:path
1703                atLine:line
1704               message:@"expected suffix \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1705     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1706     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1707 }
1708
1709 - (void)testString:(NSString *)actual doesNotHaveSuffix:(NSString *)expected inFile:(char *)path atLine:(int)line 
1710 {
1711     if (!expected)
1712         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1713                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1714                            inFile:path
1715                            atLine:line];
1716     if (actual && ![actual isKindOfClass:[NSString class]])
1717         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1718                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1719                            inFile:path
1720                            atLine:line];
1721     if (![expected isKindOfClass:[NSString class]])
1722         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1723                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1724                            inFile:path
1725                            atLine:line];
1726     BOOL result = actual ? (![actual hasSuffix:expected]) : NO; 
1727     BOOL expectedTruncated, actualTruncated;
1728     [self writePassed:result
1729                inFile:path
1730                atLine:line
1731               message:@"expected suffix (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1732     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1733     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1734 }
1735
1736 - (void)testString:(NSString *)actual contains:(NSString *)expected inFile:(char *)path atLine:(int)line 
1737 {
1738     if (!expected)
1739         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1740                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1741                            inFile:path
1742                            atLine:line];
1743     if (actual && ![actual isKindOfClass:[NSString class]])
1744         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1745                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1746                            inFile:path
1747                            atLine:line];
1748     if (![expected isKindOfClass:[NSString class]])
1749         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1750                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1751                            inFile:path
1752                            atLine:line];
1753     BOOL result = actual ? NO : (!NSEqualRanges([actual rangeOfString:expected], NSMakeRange(NSNotFound, 0))); 
1754     BOOL expectedTruncated, actualTruncated;
1755     [self writePassed:result
1756                inFile:path
1757                atLine:line
1758               message:@"expected contains \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1759     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1760     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1761 }
1762
1763 - (void)testString:(NSString *)actual doesNotContain:(NSString *)expected inFile:(char *)path atLine:(int)line 
1764 {
1765     if (!expected)
1766         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1767                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1768                            inFile:path
1769                            atLine:line];
1770     if (actual && ![actual isKindOfClass:[NSString class]])
1771         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1772                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1773                            inFile:path
1774                            atLine:line];
1775     if (![expected isKindOfClass:[NSString class]])
1776         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1777                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1778                            inFile:path
1779                            atLine:line];
1780     BOOL result = actual ? YES : (NSEqualRanges([actual rangeOfString:expected], NSMakeRange(NSNotFound, 0))); 
1781     BOOL expectedTruncated, actualTruncated;
1782     [self writePassed:result
1783                inFile:path
1784                atLine:line
1785               message:@"expected contains (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1786     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1787     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1788 }
1789
1790 #pragma mark -
1791 #pragma mark NSArray test methods
1792
1793 - (void)testArray:(NSArray *)actual isEqualTo:(NSArray *)expected inFile:(char *)path atLine:(int)line 
1794 {
1795     if (actual && ![actual isKindOfClass:[NSArray class]])
1796         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1797                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(actual)
1798                             inFile:path
1799                             atLine:line];
1800     if (expected && ![expected isKindOfClass:[NSArray class]])
1801         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1802                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(expected)
1803                             inFile:path
1804                             atLine:line];
1805     BOOL equal = NO;
1806     if (!actual && !expected) equal = YES; // equal (both nil) 
1807     else if (actual) equal = [actual isEqualToArray:expected]; 
1808     BOOL expectedTruncated, actualTruncated;
1809     [self writePassed:equal inFile:path atLine:line message:@"expected %@, got %@", WO_DESC(expected), WO_DESC(actual)]; 
1810     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1811     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1812 }
1813
1814 - (void)testArray:(NSArray *)actual isNotEqualTo:(NSArray *)expected inFile:(char *)path atLine:(int)line 
1815 {
1816     if (actual && ![actual isKindOfClass:[NSArray class]])
1817         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1818                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(actual)
1819                             inFile:path
1820                             atLine:line];
1821     if (expected && ![expected isKindOfClass:[NSArray class]])
1822         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1823                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(expected)
1824                             inFile:path
1825                             atLine:line];
1826     BOOL equal = NO;
1827     if (!actual && !expected) equal = YES; // equal (both nil)
1828     else if (actual) equal = [actual isEqualToArray:expected];
1829     BOOL expectedTruncated, actualTruncated;
1830     [self writePassed:(!equal) inFile:path atLine:line message:@"expected (not) %@, got %@", WO_DESC(expected), WO_DESC(actual)]; 
1831     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1832     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1833 }
1834
1835 #pragma mark -
1836 #pragma mark NSDictionary test methods
1837
1838 - (void)testDictionary:(NSDictionary *)actual isEqualTo:(NSDictionary *)expected inFile:(char *)path atLine:(int)line 
1839 {
1840     if (actual && ![actual isKindOfClass:[NSArray class]])
1841         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1842                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(actual)
1843                             inFile:path
1844                             atLine:line];
1845     if (expected && ![expected isKindOfClass:[NSArray class]])
1846         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1847                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(expected)
1848                             inFile:path
1849                             atLine:line];
1850     BOOL equal = NO;
1851     if (!actual && !expected) equal = YES; // equal (both nil) 
1852     else if (actual) equal = [actual isEqualToDictionary:expected]; 
1853     BOOL expectedTruncated, actualTruncated;
1854     [self writePassed:equal inFile:path atLine:line message:@"expected %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1855     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1856     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1857 }
1858
1859 - (void)testDictionary:(NSDictionary *)actual isNotEqualTo:(NSDictionary *)expected inFile:(char *)path atLine:(int)line 
1860 {
1861     if (actual && ![actual isKindOfClass:[NSDictionary class]])
1862         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1863                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(actual)
1864                             inFile:path
1865                             atLine:line];
1866     if (expected && ![expected isKindOfClass:[NSDictionary class]])
1867         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1868                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(expected)
1869                             inFile:path
1870                             atLine:line];
1871     BOOL equal = NO;
1872     if (!actual && !expected) equal = YES; // equal (both nil) 
1873     else if (actual) equal = [actual isEqualToDictionary:expected]; 
1874     BOOL expectedTruncated, actualTruncated;
1875     [self writePassed:(!equal) inFile:path atLine:line message:@"expected (not) %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1876     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1877     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1878 }
1879
1880 #pragma mark -
1881 #pragma mark Exception test methods
1882
1883 - (void)testThrowsException:(id)exception inFile:(char *)path atLine:(int)line 
1884 {
1885     BOOL result = (exception ? YES : NO); 
1886     [self writePassed:result
1887                inFile:path
1888                atLine:line
1889               message:[NSString stringWithFormat:@"expected exception, got %@", [NSException WOTest_nameForException:exception]]];
1890 }
1891
1892 - (void)testDoesNotThrowException:(id)exception inFile:(char *)path atLine:(int)line 
1893 {
1894     BOOL result = (exception ? NO : YES); 
1895     [self writePassed:result
1896                inFile:path
1897                atLine:line
1898               message:
1899         [NSString stringWithFormat:@"expected no exception, got %@", [NSException WOTest_nameForException:exception]]];
1900 }
1901
1902 - (void)testThrowsException:(id)exception named:(NSString *)name inFile:(char *)path atLine:(int)line 
1903 {
1904     if (name && ![name isKindOfClass:[NSString class]]) 
1905     {
1906         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1907                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(name)
1908                            inFile:path
1909                            atLine:line];
1910     }
1911     
1912     BOOL        result      = NO; 
1913     NSString    *actualName = [NSException WOTest_nameForException:exception]; 
1914     
1915     if (exception && [actualName isEqualToString:name]) result = YES; 
1916     
1917     [self writePassed:result
1918                inFile:path
1919                atLine:line
1920               message:[NSString stringWithFormat:@"expected %@, got %@", name, actualName]];
1921 }
1922
1923 - (void)testDoesNotThrowException:(id)exception named:(NSString *)name inFile:(char *)path atLine:(int)line 
1924 {
1925     if (name && ![name isKindOfClass:[NSString class]]) 
1926     {
1927         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1928                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(name)
1929                            inFile:path
1930                            atLine:line];
1931     }
1932
1933     BOOL        result      = YES; 
1934     NSString    *actualName = [NSException WOTest_nameForException:exception]; 
1935     
1936     if (exception && [actualName isEqualToString:name]) result = NO; 
1937     
1938     [self writePassed:result inFile:path atLine:line message:@"expected (not) %@, got %@", name, actualName]; 
1939 }
1940
1941 #pragma mark -
1942 #pragma mark Random value generator methods
1943
1944 // TODO: move these into a category, these are more "test helpers" rather than actual "testing methods"
1945
1946 - (int)anInt
1947 {
1948     return (int)(WO_RANDOM_SIGN * (WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
1949 }
1950
1951 - (int)aPositiveInt
1952 {
1953     return (int)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1954 }
1955
1956 - (int)aNegativeInt
1957 {
1958     return (int)-(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1959 }
1960
1961 - (int)aZeroInt
1962 {
1963     return 0;
1964 }
1965
1966 - (int)aBigInt
1967 {
1968     return (int)(WO_RANDOM_SIGN * (WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
1969 }
1970
1971 - (int)aBigPositiveInt
1972 {
1973     return (int)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1974 }
1975
1976 - (int)aBigNegativeInt
1977 {
1978     return (int)-(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1979 }
1980
1981 - (int)aSmallInt
1982 {
1983     return (int)(WO_RANDOM_SIGN * (WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
1984 }
1985
1986 - (int)aSmallPositiveInt
1987 {
1988     return (int)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1989 }
1990
1991 - (int)aSmallNegativeInt
1992 {
1993     return (int)-(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1994 }
1995
1996 - (unsigned)anUnsigned
1997 {
1998     return (unsigned)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1999 }
2000
2001 - (unsigned)aZeroUnsigned
2002 {
2003     return 0;
2004 }
2005
2006 - (unsigned)aBigUnsigned
2007 {
2008     return (unsigned)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2009 }
2010
2011 - (unsigned)aSmallUnsigned
2012 {
2013     return (unsigned)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2014 }
2015
2016 - (float)aFloat
2017 {
2018     return (float)(WO_RANDOM_SIGN * (WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2019 }
2020
2021 - (float)aPositiveFloat
2022 {
2023     return (float)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2024 }
2025
2026 - (float)aNegativeFloat
2027 {
2028     return (float)-(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2029 }
2030
2031 - (float)aZeroFloat
2032 {
2033     return 0.0;
2034 }
2035
2036 - (float)aBigFloat
2037 {
2038     return (float)(WO_RANDOM_SIGN * (WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2039 }
2040
2041 - (float)aBigPositiveFloat
2042 {
2043     return (float)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2044 }
2045
2046 - (float)aBigNegativeFloat
2047 {
2048     return (float)-(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2049 }
2050
2051 - (float)aSmallFloat
2052 {
2053     return (float)(WO_RANDOM_SIGN * (WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2054 }
2055
2056 - (float)aSmallPositiveFloat
2057 {
2058     return (float)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2059 }
2060
2061 - (float)aSmallNegativeFloat
2062 {
2063     return (float)-(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2064 }
2065
2066 - (double)aDouble
2067 {
2068     return (double)(WO_RANDOM_SIGN * (WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2069 }
2070
2071 - (double)aPositiveDouble
2072 {
2073     return (double)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2074 }
2075
2076 - (double)aNegativeDouble
2077 {
2078     return (double)-(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2079 }
2080
2081 - (double)aZeroDouble
2082 {
2083     return 0.0;
2084 }
2085
2086 - (double)aBigDouble
2087 {
2088     return (double)(WO_RANDOM_SIGN * (WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2089 }
2090
2091 - (double)aBigPositiveDouble
2092 {
2093     return (double)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2094 }
2095
2096 - (double)aBigNegativeDouble
2097 {
2098     return (double)-(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2099 }
2100
2101 - (double)aSmallDouble
2102 {
2103     return (double)(WO_RANDOM_SIGN * (WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2104 }
2105
2106 - (double)aSmallPositiveDouble
2107 {
2108     return (double)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2109 }
2110
2111 - (double)aSmallNegativeDouble
2112 {
2113     return (double)-(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2114 }
2115
2116 #pragma mark - 
2117 #pragma mark Accessors
2118
2119 - (NSDate *)startDate
2120 {
2121     return [[startDate retain] autorelease]; 
2122 }
2123
2124 - (void)setStartDate:(NSDate *)aStartDate
2125 {
2126     [aStartDate retain];
2127     [startDate release];
2128     startDate = aStartDate;
2129 }
2130
2131 - (unsigned)classesWithTests
2132 {
2133     return classesWithTests;
2134 }
2135
2136 - (unsigned)classesWithoutTests
2137 {
2138     return classesWithoutTests;
2139 }
2140
2141 - (unsigned)methodsWithTests
2142 {
2143     return methodsWithTests;
2144 }
2145
2146 - (unsigned)testsRun
2147 {
2148     return testsRun;
2149 }
2150
2151 - (unsigned)testsPassed
2152 {
2153     return testsPassed;
2154 }
2155
2156 - (unsigned)testsFailed
2157 {
2158     return testsFailed;
2159 }
2160
2161 - (unsigned)uncaughtExceptions
2162 {
2163     return uncaughtExceptions;
2164 }
2165
2166 - (unsigned)testsFailedExpected
2167 {
2168     return testsFailedExpected;
2169 }
2170
2171 - (unsigned)testsPassedUnexpected
2172 {
2173     return testsPassedUnexpected;
2174 }
2175
2176 - (BOOL)expectFailures
2177 {
2178     return expectFailures;
2179 }
2180
2181 - (void)setExpectFailures:(BOOL)aValue
2182 {
2183     expectFailures = aValue;
2184 }
2185
2186 - (unsigned)lowLevelExceptionsExpected
2187 {
2188     return lowLevelExceptionsExpected;
2189 }
2190
2191 - (unsigned)lowLevelExceptionsUnexpected
2192 {
2193     return lowLevelExceptionsUnexpected;
2194 }
2195
2196 - (BOOL)expectLowLevelExceptions
2197 {
2198     return expectLowLevelExceptions;
2199 }
2200
2201 - (void)setExpectLowLevelExceptions:(BOOL)aValue
2202 {
2203     expectLowLevelExceptions = aValue;
2204 }
2205
2206 - (BOOL)handlesLowLevelExceptions
2207 {
2208     return handlesLowLevelExceptions;
2209 }
2210
2211 - (void)setHandlesLowLevelExceptions:(BOOL)aValue
2212 {
2213     handlesLowLevelExceptions = aValue;
2214 }
2215
2216 - (unsigned)verbosity
2217 {
2218     return verbosity;
2219 }
2220
2221 - (void)setVerbosity:(unsigned)aVerbosity
2222 {
2223     verbosity = aVerbosity;
2224 }
2225
2226 - (unsigned)trimInitialPathComponents
2227 {
2228     return trimInitialPathComponents;
2229 }
2230
2231 - (void)setTrimInitialPathComponents:(unsigned)aTrimInitialPathComponents
2232 {
2233     trimInitialPathComponents = aTrimInitialPathComponents;
2234 }
2235
2236 - (NSString *)lastReportedFile
2237 {
2238     return [[lastReportedFile retain] autorelease]; 
2239 }
2240
2241 - (void)setLastReportedFile:(NSString *)aLastReportedFile
2242 {
2243     if (lastReportedFile != aLastReportedFile)
2244     {
2245         [aLastReportedFile retain];
2246         [lastReportedFile release];
2247         lastReportedFile = aLastReportedFile;
2248     }
2249 }
2250
2251 - (BOOL)warnsAboutSignComparisons
2252 {
2253     return warnsAboutSignComparisons;
2254 }
2255
2256 - (void)setWarnsAboutSignComparisons:(BOOL)aWarnsAboutSignComparisons
2257 {
2258     warnsAboutSignComparisons = aWarnsAboutSignComparisons;
2259 }
2260
2261 @end