]> git.wincent.com - WOTest.git/blob - WOTestClass.m
4dc64131883bf84b5a4cfa0cd8f0b317dd3ab526
[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     for (NSString *class in [self testableClasses])
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             for (NSString *method in [self testableMethodsFrom:aClass])
364             {
365                 NSAutoreleasePool   *pool           = [[NSAutoreleasePool alloc] init];
366                 NSDate              *startMethod    = [NSDate date];
367                 SEL                 preflight       = @selector(preflight);
368                 SEL                 postflight      = @selector(postflight);
369
370                 _WOLog(@"Running test method %@", method);
371                 @try
372                 {
373                     // minimize time spent with exception handlers in place
374                     [self installLowLevelExceptionHandler];
375
376                     // record program counter and some other registers right now
377 #ifdef __i386__
378                     // LowLevelABI.pdf says "EDI, ESI, EBX, EBP" are the preserved registers (across function calls)
379                     // ebp is the "saved frame pointer": "the base address of the caller's stack frame"
380                     // eax is used to return pointer and integral results to callers: "The called function places integral or pointer results in EAX"
381
382                     // info on inline assembly: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
383                     __asm__ volatile("movl %%eax, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.eax));
384                     __asm__ volatile("movl %%ebx, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.ebx));
385                     __asm__ volatile("movl %%edi, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.edi));
386                     __asm__ volatile("movl %%esi, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.esi));
387                     __asm__ volatile("movl %%esp, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.esp));
388                     __asm__ volatile("movl %%ebp, %0\n" : "=m" (WOLowLevelExceptionJumpBuffer.ebp));
389
390                     // done this way in Linux (see acpi_save_register_state function)
391                     WOProgramCounter = (unsigned long)&&jump_point;
392 #elif defined (__ppc__)
393                     // equivalent to (psuedo code) "WOProgramCounter = current contents of PC register"
394                     unsigned long counter;
395                     __asm__ volatile("mflr %0" : "=r" (counter));
396                     WOProgramCounter.lo = counter & 0xffffffff;
397                     WOProgramCounter.hi = (counter & 0xffffffff00000000) >> 32;
398 #else
399 #error Unsupported architecture
400 #endif
401                     WOTestCanJump = YES;
402
403                     goto jump_point; // necessary to silence compiler warning about unused label
404 jump_point:
405                     // if flag set, that means we crashed: throw an exception
406                     if (WOTestExceptionTriggered)
407                     {
408                         WOTestExceptionTriggered = 0;
409                         @throw [WOTestLowLevelException exceptionWithType:WOLastLowLevelException];
410                     }
411
412                     if ([self isClassMethod:method])
413                     {
414                         if ([NSObject WOTest_class:aClass respondsToSelector:preflight])
415                             objc_msgSend(aClass, preflight);
416                         objc_msgSend(aClass, [self selectorFromMethod:method]);
417                         if ([NSObject WOTest_class:aClass respondsToSelector:postflight])
418                             objc_msgSend(aClass, postflight);
419                     }
420                     else if ([self isInstanceMethod:method])
421                     {
422                         // class must implement alloc, init and release
423                         if ([NSObject WOTest_object:aClass respondsToSelector:@selector(alloc)] &&
424                             [NSObject WOTest_instancesOfClass:aClass respondToSelector:@selector(init)] &&
425                             [NSObject WOTest_instancesOfClass:aClass respondToSelector:@selector(release)])
426                         {
427                             id instance = [[aClass alloc] init];
428                             if ([NSObject WOTest_instancesOfClass:aClass respondToSelector:preflight])
429                                 objc_msgSend(instance, preflight);
430                             objc_msgSend(instance, [self selectorFromMethod:method]);
431                             if ([NSObject WOTest_instancesOfClass:aClass respondToSelector:postflight])
432                                 objc_msgSend(instance, postflight);
433                             [instance release];
434                         }
435                         else
436                         {
437                             [self writeError:@"Class %@ must respond to the alloc, init and release selectors",
438                                 NSStringFromClass(aClass)];
439                             [self writeLastKnownLocation];
440                         }
441                     }
442                     else    // should never get here
443                         [self writeError:@"WOTest internal error"];
444                 }
445                 @catch (WOTestLowLevelException *lowLevelException)
446                 {
447                     if ([self expectLowLevelExceptions])
448                     {
449                         [self writeStatus:[lowLevelException reason]];    // expected low-level exceptions are not an error
450                         lowLevelExceptionsExpected++;
451                     }
452                     else
453                     {
454                         [self writeError:[lowLevelException reason]];     // unexpected low-level exceptions are an error
455                         [self writeLastKnownLocation];
456                         noTestFailed = NO;
457                         lowLevelExceptionsUnexpected++;
458                     }
459                 }
460                 @catch (id e)
461                 {
462                     [self writeError:@"uncaught exception (%@) in test method %@", [NSException WOTest_descriptionForException:e],
463                         method];
464                     [self writeLastKnownLocation];
465                     noTestFailed = NO;
466                     uncaughtExceptions++;
467                 }
468                 @finally
469                 {
470                     if (lowLevelExceptionHandlerInstalled)
471                         [self removeLowLevelExceptionHandler];
472                     _WOLog(@"Finished test method %@ (%.4f seconds)", method, -[startMethod timeIntervalSinceNow]);
473                     [pool release];
474                 }
475             }
476         }
477     }
478     @catch (id e)
479     {
480         [self writeError:@"uncaught exception (%@) testing class %@", [NSException WOTest_descriptionForException:e],
481             NSStringFromClass(aClass)];
482         [self writeLastKnownLocation];
483         noTestFailed = NO;
484         uncaughtExceptions++;
485     }
486     @finally
487     {
488         _WOLog(@"Finished tests for class %@ (%.4f seconds)", NSStringFromClass(aClass), -[startClass timeIntervalSinceNow]);
489     }
490     return noTestFailed;
491 }
492
493 - (NSArray *)testableClasses
494 {
495     // return an array of class names
496     NSMutableArray *testableClasses = [NSMutableArray array];
497
498     unsigned    classCount                 = 0; // the total number of classes
499     unsigned    conformingClassCount       = 0; // classes conforming to WOTest
500     unsigned    nonconformingClassCount    = 0; // unconforming classes
501     unsigned    excludedClassCount         = 0; // excluded classes
502     unsigned    exceptionCount             = 0; // classes provoking exceptions
503
504     int         numClasses                  = 0;
505     int         newNumClasses               = objc_getClassList(NULL, 0);
506     Class       *classes                    = NULL;
507
508     // get a list of all classes on the system
509     while (numClasses < newNumClasses)
510     {
511         numClasses          = newNumClasses;
512         size_t bufferSize   = sizeof(Class) * numClasses;
513         classes             = realloc(classes, bufferSize);
514         NSAssert1((classes != NULL), @"realloc() failed (size %d)", bufferSize);
515         newNumClasses       = objc_getClassList(classes, numClasses);
516     }
517
518     @try
519     {
520         if (classes)
521         {
522             // skip over some classes because they not only cause exceptions but also spew out ugly console messages
523             SInt32 systemVersion;
524             Gestalt(gestaltSystemVersion, &systemVersion);
525             systemVersion = systemVersion & 0x0000ffff; // Apple instructs to ignore the high-order word
526
527             NSArray *excludedClasses = (systemVersion < 0x00001040) ?
528                 [NSArray arrayWithObjects: @"Protocol", @"List", @"Object", @"_NSZombie", nil] :                        // 10.3
529                 [NSArray arrayWithObjects: @"Protocol", @"List", @"Object", @"_NSZombie", @"NSATSGlyphGenerator", nil]; // 10.4
530
531             if ([self verbosity] > 1)
532                 _WOLog(@"Examining classes for WOTest protocol compliance");
533
534             for (int i = 0; i < newNumClasses; i++)
535             {
536                 classCount++;
537
538                 Class       aClass      = classes[i];
539                 NSString    *className  = NSStringFromClass(aClass);
540
541                 @try
542                 {
543                     if ([excludedClasses containsObject:className])
544                     {
545                         excludedClassCount++;
546                         if ([self verbosity] > 1)
547                             _WOLog(@"Skipping class %@ (appears in exclusion list)", className);
548                     }
549                     else if ([NSObject WOTest_instancesOfClass:aClass conformToProtocol:@protocol(WOTest)])
550                     {
551                         conformingClassCount++;
552                         [testableClasses addObject:className];
553                         if ([self verbosity] > 0)
554                             _WOLog(@"Class %@ complies with the WOTest protocol", className);
555                     }
556                     else
557                     {
558                         nonconformingClassCount++;
559                         if ([self verbosity] > 1)
560                             _WOLog(@"Class %@ does not comply with the WOTest protocol", className);
561                     }
562                 }
563                 @catch (id exception)
564                 {
565                     exceptionCount++;
566                     // a number of classes are known to provoke exceptions:
567                     if ([self verbosity] > 1)
568                         _WOLog(@"Cannot test protocol compliance for class %@ (caught exception)", className);
569                     continue;
570                 }
571             }
572             free(classes);
573         }
574
575     }
576     @catch (id e)
577     {
578         _WOLog(@"Uncaught exception...");
579     }
580
581     _WOLog(@"Runtime Summary:\n"
582            @"Total classes:                                         %d\n"
583            @"Classes which conform to the WOTest protocol:          %d\n"
584            @"Classes which do not conform to the protocol:          %d\n"
585            @"Classes excluded from scanning:                        %d\n"
586            @"Classes that could not be scanned due to exceptions:   %d",
587            classCount,
588            conformingClassCount,
589            nonconformingClassCount,
590            excludedClassCount,
591            exceptionCount);
592
593     return [testableClasses sortedArrayUsingSelector:@selector(compare:)];
594 }
595
596 - (NSArray *)testableClassesFrom:(NSBundle *)aBundle
597 {
598     NSMutableArray  *classNames = [NSMutableArray array];
599
600     if (aBundle)    // only search if actually passed a non-nil bundle
601     {
602         // add only classes that match the passed bundle and conform to WOTest
603         for (NSString *className in [self testableClasses])
604         {
605             Class       aClass          = NSClassFromString(className);
606             NSBundle    *classBundle    = [NSBundle bundleForClass:aClass];
607             if ([classBundle isEqualTo:aBundle])
608                 [classNames addObject:className];
609         }
610     }
611
612     // return autoreleased, immutable NSArray
613     return [NSArray arrayWithArray:classNames];
614 }
615
616 - (NSArray *)testableMethodsFrom:(Class)aClass
617 {
618     // catch crashes caused by passing an "id" instead of a "Class"
619     NSParameterAssert([NSObject WOTest_isRegisteredClass:aClass] || [NSObject WOTest_isMetaClass:aClass]);
620
621     NSMutableArray *methodNames = [NSMutableArray array];
622     @try
623     {
624         NSString *prefix = @"-";            // default prefix (instance methods)
625         if (class_isMetaClass(aClass))
626             prefix = @"+";                  // special prefix (class methods)
627         else                                // this is not a metaclass
628         {
629             // get the metaclass; could also use object_getClass
630             Class   metaClass       = object_getClass(aClass);
631             NSArray *classMethods   = [self testableMethodsFrom:metaClass];
632             [methodNames addObjectsFromArray:classMethods];
633         }
634
635         unsigned int count;
636         Method *methods = class_copyMethodList(aClass, &count);
637         if (methods)
638         {
639             for (unsigned int i = 0, max = count; i < max; i++)
640             {
641                 SEL         aSelector   = method_getName(methods[i]);
642                 NSString    *name       = NSStringFromSelector(aSelector);
643                 if (name && [name hasPrefix:@"test"])
644                     [methodNames addObject:[NSString stringWithFormat:@"%@%@", prefix, name]];
645             }
646             free(methods);
647         }
648     }
649     @catch (id e)
650     {
651         NSString *error = [NSString stringWithFormat:@"exception caught trying to identify testable methods in class %@",
652             NSStringFromClass(aClass)];
653         [self writeError:error];
654     }
655
656     return [methodNames sortedArrayUsingSelector:@selector(compare:)];
657 }
658
659 - (void)printTestResultsSummary;
660 {
661     [self checkStartDate];  // just in case no tests were run, make sure that startDate is non-nil
662     double      successRate = 0.0;
663     double      failureRate = 0.0;
664     if (testsRun > 0)   // watch out for divide-by-zero if no tests run
665     {
666         successRate = ((double)(testsPassed + testsFailedExpected)      / (double)testsRun) * 100.0;
667         failureRate = ((double)(testsFailed + testsPassedUnexpected)    / (double)testsRun) * 100.0;
668     }
669     _WOLog(@"Run summary:\n"
670            @"Tests run:                         %d\n"
671            @"Tests passed:                      %d + %d expected failures (%.2f%% success rate)\n"
672            @"Tests failed:                      %d + %d unexpected passes (%.2f%% failure rate)\n"
673            @"Uncaught exceptions:               %d\n"
674            @"Low-level exceptions (crashers):   %d + %d expected\n"
675            @"Total run time:                    %.2f seconds\n",
676            testsRun,
677            testsPassed,    testsFailedExpected,    successRate,
678            testsFailed,    testsPassedUnexpected,  failureRate,
679            uncaughtExceptions,
680            lowLevelExceptionsUnexpected, lowLevelExceptionsExpected,
681            -[[self startDate] timeIntervalSinceNow]);
682
683     if (testsRun == 0)
684         _WOLog(@"warning: no tests were run\n");
685
686     // TODO: make Growl notifications optional
687     // TODO: include information about project being tested in Growl notification title
688     // TODO: add options for showing coalesced growl notifications showing individual test failures (with path and line info)
689     // TODO: make clicking on notification bring Xcode to the front, or open the file with the last failure in it etc
690     NSString *status = [NSString stringWithFormat:@"%d tests passed, %d tests failed",
691         testsPassed + testsFailedExpected, testsFailed + testsPassedUnexpected];
692
693     if ([self testsWereSuccessful])
694         [self growlNotifyTitle:@"WOTest run successful" message:status isWarning:NO sticky:NO];
695     else
696     {
697         _WOLog(@"error: testing did not complete without errors\n");
698         [self growlNotifyTitle:@"WOTest run failed" message:status isWarning:YES sticky:YES];
699
700     }
701
702     // reset start date
703     [self setStartDate:nil];
704 }
705
706 - (BOOL)testsWereSuccessful
707 {
708     return ((testsFailed + testsPassedUnexpected + uncaughtExceptions + lowLevelExceptionsUnexpected) == 0);
709 }
710
711 #pragma mark -
712 #pragma mark Low-level exception handling
713
714 - (void)installLowLevelExceptionHandler
715 {
716     if (!lowLevelExceptionHandlerInstalled)
717     {
718         WOOldLowLevelExceptionHandler = InstallExceptionHandler(NewExceptionHandlerUPP(WOLowLevelExceptionHandler));
719         lowLevelExceptionHandlerInstalled = YES;
720     }
721 }
722
723 - (void)removeLowLevelExceptionHandler
724 {
725     if (lowLevelExceptionHandlerInstalled)
726     {
727         DisposeExceptionHandlerUPP(InstallExceptionHandler(WOOldLowLevelExceptionHandler));
728         lowLevelExceptionHandlerInstalled = NO;
729     }
730 }
731
732 #pragma mark -
733 #pragma mark Growl support
734
735 - (void)growlNotifyTitle:(NSString *)title message:(NSString *)message isWarning:(BOOL)isWarning sticky:(BOOL)sticky
736 {
737     NSParameterAssert(title != nil);
738     NSParameterAssert(message != nil);
739
740     // clean up enviroment a bit (hides possible warnings caused if these set for WOTestRunner)
741     NSMutableDictionary *environment = [NSMutableDictionary dictionaryWithDictionary:[[NSProcessInfo processInfo] environment]];
742     [environment removeObjectForKey:@"DYLD_INSERT_LIBRARIES"];
743     [environment removeObjectForKey:@"WOTestBundleInjector"];
744
745     NSTask *task = [[[NSTask alloc] init] autorelease];
746     [task setLaunchPath:@"/usr/bin/env"];   // use env so as to pick up PATH, if set
747     [task setEnvironment:environment];
748     NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"growlnotify",
749         @"--name",      @"com.wincent.WOTest",
750         @"--appIcon",   @"Xcode",
751         @"--priority",  (isWarning ? @"2" : @"0"),
752         @"--message",   message,
753         title,          nil];
754     if (sticky) [arguments insertObject:@"--sticky" atIndex:0];
755     [task setArguments:arguments];
756
757     // if env can't find growl it will write a message like "env: growlnotify: No such file or directory" to the standard error
758     // suppress it by redirecting the standard error to /dev/null
759     [task setStandardError:[NSFileHandle fileHandleForWritingAtPath:@"/dev/null"]];
760
761     @try
762     {
763         [task launch];
764         [task waitUntilExit];
765
766         // handle error conditions
767         if (![task isRunning])
768         {
769             int status = [task terminationStatus];
770             if (status == 127)  // env returns this when "[t]he utility specified by utility could not be found"
771                 _WOLog(@"note: growlnotify not launched (not found in the current PATH)");
772             else if (status != EXIT_SUCCESS)
773                 // a failure to run growlnotify is relatively harmless, so use warning rather than error
774                 _WOLog(@"warning: env terminated with exit status %d while trying to run growlnotify", status);
775         }
776     }
777     @catch (NSException *e)
778     {
779         // highly unlikely that we'd ever get here, but report it anyway
780         _WOLog(@"warning: exception caught while trying to execute growlnotify using env (%@: %@)", [e name], [e reason]);
781     }
782 }
783
784 #pragma mark -
785 #pragma mark Logging methods
786
787 - (NSString *)trimmedPath:(char *)path
788 {
789     NSParameterAssert(path != NULL);
790     NSString *pathString = [NSString stringWithUTF8String:path];
791
792     unsigned trim = [self trimInitialPathComponents];
793     if (trim == 0) return pathString;
794     if (![pathString isAbsolutePath]) return pathString;    // only trim absolute paths
795     NSArray *components = [pathString pathComponents];      // note: Cocoa returns "/" here as an additional first component
796     NSAssert(components != nil, @"components != nil");
797     unsigned count = [components count];
798     if (count < trim + 2) return pathString;                // only trim if there will be at least one component left over
799     return [NSString pathWithComponents:[components subarrayWithRange:NSMakeRange(trim + 1, count - trim - 1)]];
800 }
801
802 - (void)writePassed:(BOOL)passed
803              inFile:(char *)path
804              atLine:(int)line
805             message:(NSString *)message, ...
806 {
807     testsRun++;
808     va_list args;
809     va_start(args, message);
810     NSString *string = [NSString WOTest_stringWithFormat:message arguments:args];
811     va_end(args);
812     if ([self expectFailures])  // invert sense of tests (ie. failure is good)
813     {
814         if (passed) // passed: bad
815         {
816             [self writeErrorInFile:path atLine:line message:[NSString stringWithFormat:@"Passed (unexpected pass): %@", string]];
817             testsPassedUnexpected++;
818         }
819         else        // failed: good
820         {
821             [self writeStatusInFile:path atLine:line message:[NSString stringWithFormat:@"Failed (expected failure): %@", string]];
822             testsFailedExpected++;
823         }
824     }
825     else    // normal handling (ie. passing is good, failing is bad)
826     {
827         if (passed) // passed: good
828         {
829             [self writeStatusInFile:path atLine:line message:[NSString stringWithFormat:@"Passed: %@", string]];
830             testsPassed++;
831         }
832         else        // failed: bad
833         {
834             [self writeErrorInFile:path atLine:line message:[NSString stringWithFormat:@"Failed: %@", string]];
835             testsFailed++;
836         }
837     }
838 }
839
840 - (void)cacheFile:(char *)path line:(int)line
841 {
842     [self setLastReportedFile:[self trimmedPath:path]];
843     lastReportedLine = line;
844 }
845
846 - (void)writeLastKnownLocation
847 {
848     NSString *path = [self lastReportedFile];
849     if (path)
850         _WOLog(@"%@:%d: last known location was %@:%d", path, lastReportedLine, path, lastReportedLine);
851 }
852
853 - (void)writeErrorInFile:(char *)path atLine:(int)line message:(NSString *)message, ...
854 {
855     va_list args;
856     va_start(args, message);
857     NSString *error = [NSString WOTest_stringWithFormat:message arguments:args];
858     _WOLog(@"%@:%d: error: %@", [self trimmedPath:path], line, error);
859     [self cacheFile:path line:line];
860     va_end(args);
861 }
862
863 - (void)writeWarningInFile:(char *)path atLine:(int)line message:(NSString *)message, ...
864 {
865     va_list args;
866     va_start(args, message);
867     NSString *warning = [NSString WOTest_stringWithFormat:message arguments:args];
868     _WOLog(@"%@:%d: warning: %@", [self trimmedPath:path], line, warning);
869     [self cacheFile:path line:line];
870     va_end(args);
871 }
872
873 - (void)writeUncaughtException:(NSString *)info inFile:(char *)path atLine:(int)line
874 {
875     _WOLog(@"%@:%d: error: uncaught exception during test execution: %@", [self trimmedPath:path], line, info);
876     uncaughtExceptions++;
877 }
878
879 - (void)writeStatusInFile:(char *)path atLine:(int)line message:(NSString *)message, ...
880 {
881     va_list args;
882     va_start(args, message);
883     NSString *status = [NSString WOTest_stringWithFormat:message arguments:args];
884     _WOLog(@"%@:%d %@", [self trimmedPath:path], line, status); // omit colin after line number or Xcode will show this as an error
885     [self cacheFile:path line:line];
886     va_end(args);
887 }
888
889 - (void)writeStatus:(NSString *)message, ...
890 {
891     va_list args;
892     va_start(args, message);
893     NSString *status = [NSString WOTest_stringWithFormat:message arguments:args];
894     _WOLog(@"%@", status);
895     va_end(args);
896 }
897
898 - (void)writeWarning:(NSString *)message, ...
899 {
900     va_list args;
901     va_start(args, message);
902     NSString *warning = [NSString WOTest_stringWithFormat:message arguments:args];
903     _WOLog(@"warning: %@", warning); // older versions of Xcode required initial colons "::" to show this as a warning
904     va_end(args);
905 }
906
907 - (void)writeError:(NSString *)message, ...
908 {
909     va_list args;
910     va_start(args, message);
911     NSString *error = [NSString WOTest_stringWithFormat:message arguments:args];
912     _WOLog(@"error: %@", error); // older versions of Xcode required initial colons "::" to show this as an error
913     va_end(args);
914 }
915
916 #pragma mark -
917 #pragma mark Empty (do-nothing) test methods
918
919 - (void)passTestInFile:(char *)path atLine:(int)line
920 {
921     [self writePassed:YES inFile:path atLine:line message:@"(always passes)"];
922 }
923
924 - (void)failTestInFile:(char *)path atLine:(int)line
925 {
926     [self writePassed:NO inFile:path atLine:line message:@"(always fails)"];
927 }
928
929 #pragma mark -
930 #pragma mark Boolean test methods
931
932 - (void)testTrue:(BOOL)expr inFile:(char *)path atLine:(int)line
933 {
934     [self writePassed:expr inFile:path atLine:line message:@"expected YES, got %@", (expr ? @"YES" : @"NO")];
935 }
936
937 - (void)testFalse:(BOOL)expr inFile:(char *)path atLine:(int)line
938 {
939     [self writePassed:!expr inFile:path atLine:line message:@"expected NO, got %@", (expr ? @"YES" : @"NO")];
940 }
941
942 #pragma mark -
943 #pragma mark NSValue-based tests
944
945 - (void)testValue:(NSValue *)actual isEqualTo:(NSValue *)expected inFile:(char *)path atLine:(int)line
946 {
947     NSParameterAssert(actual);
948     NSParameterAssert(expected);
949     BOOL equal = NO;
950
951     // NSValue category will throw an exception for invalid input(s)
952     @try {
953         equal = [actual WOTest_testIsEqualToValue:expected];
954     }
955     @catch (id e) {
956         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
957         uncaughtExceptions++;
958     }
959     BOOL expectedTruncated, actualTruncated;
960     [self writePassed:equal inFile:path atLine:line message:@"expected %@, got %@", WO_DESC(expected), WO_DESC(actual)];
961     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
962     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
963 }
964
965 - (void)testValue:(NSValue *)actual isNotEqualTo:(NSValue *)expected inFile:(char *)path atLine:(int)line
966 {
967     NSParameterAssert(actual);
968     NSParameterAssert(expected);
969     BOOL equal = NO;
970
971     // NSValue category will throw an exception for invalid input(s)
972     @try {
973         equal = [actual WOTest_testIsEqualToValue:expected];
974     }
975     @catch (id e) {
976         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
977         uncaughtExceptions++;
978     }
979     BOOL expectedTruncated, actualTruncated;
980     [self writePassed:(!equal) inFile:path atLine:line message:@"expected (not) %@, got %@", WO_DESC(expected), WO_DESC(actual)];
981     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
982     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
983 }
984
985 - (void)testValue:(NSValue *)actual greaterThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
986 {
987     NSParameterAssert(actual);
988     NSParameterAssert(expected);
989     BOOL greaterThan = NO;
990
991     // NSValue category will throw an exception for invalid input(s)
992     @try {
993         greaterThan = [actual WOTest_testIsGreaterThanValue:expected];
994     }
995     @catch (id e) {
996         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
997         uncaughtExceptions++;
998     }
999     BOOL expectedTruncated, actualTruncated;
1000     [self writePassed:greaterThan inFile:path atLine:line message:@"expected > %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1001     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1002     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1003 }
1004
1005 - (void)testValue:(NSValue *)actual notGreaterThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
1006 {
1007     NSParameterAssert(actual);
1008     NSParameterAssert(expected);
1009     BOOL notGreaterThan = NO;
1010
1011     // NSValue category will throw an exception for invalid input(s)
1012     @try {
1013         notGreaterThan = [actual WOTest_testIsNotGreaterThanValue:expected];
1014     }
1015     @catch (id e) {
1016         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
1017         uncaughtExceptions++;
1018     }
1019     BOOL expectedTruncated, actualTruncated;
1020     [self writePassed:notGreaterThan inFile:path atLine:line message:@"expected <= %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1021     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1022     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1023 }
1024
1025 - (void)testValue:(NSValue *)actual lessThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
1026 {
1027     NSParameterAssert(actual);
1028     NSParameterAssert(expected);
1029     BOOL lessThan = NO;
1030
1031     // NSValue category will throw an exception for invalid input(s)
1032     @try {
1033         lessThan = [actual WOTest_testIsLessThanValue:expected];
1034     }
1035     @catch (id e) {
1036         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
1037         uncaughtExceptions++;
1038     }
1039     BOOL expectedTruncated, actualTruncated;
1040     [self writePassed:lessThan inFile:path atLine:line message:@"expected < %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1041     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1042     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1043 }
1044
1045 - (void)testValue:(NSValue *)actual notLessThan:(NSValue *)expected inFile:(char *)path atLine:(int)line
1046 {
1047     NSParameterAssert(actual);
1048     NSParameterAssert(expected);
1049     BOOL notLessThan = NO;
1050
1051     // NSValue category will throw an exception for invalid input(s)
1052     @try {
1053         notLessThan = [actual WOTest_testIsNotLessThanValue:expected];
1054     }
1055     @catch (id e) {
1056         [self writeErrorInFile:path atLine:line message:@"uncaught exception (%@)", [NSException WOTest_descriptionForException:e]];
1057         uncaughtExceptions++;
1058     }
1059     BOOL expectedTruncated, actualTruncated;
1060     [self writePassed:notLessThan inFile:path atLine:line message:@"expected >= %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1061     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1062     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1063 }
1064
1065 #pragma mark -
1066 #pragma mark Pointer to void test methods
1067
1068 - (void)testNil:(void *)pointer inFile:(char *)path atLine:(int)line
1069 {
1070     BOOL result = (pointer ? NO : YES);
1071     [self writePassed:result inFile:path atLine:line message:@"expected nil, got %@",
1072         (result ? @"nil" : [NSString stringWithFormat:@"%x", pointer])];
1073 }
1074
1075 - (void)testNotNil:(void *)pointer inFile:(char *)path atLine:(int)line
1076 {
1077     BOOL result = (pointer ? YES : NO);
1078     [self writePassed:result inFile:path atLine:line message:@"expected (not) nil, got %@",
1079         (result ? [NSString stringWithFormat:@"%x", pointer] : @"nil")];
1080 }
1081
1082 - (void)testPointer:(void *)actual isEqualTo:(void *)expected inFile:(char *)path atLine:(int)line
1083 {
1084     BOOL result = (actual == expected);
1085     [self writePassed:result inFile:path atLine:line message:@"expected %x, got %x", expected, actual];
1086 }
1087
1088 - (void)testPointer:(void *)actual isNotEqualTo:(void *)expected inFile:(char *)path atLine:(int)line
1089 {
1090     BOOL result = (actual != expected);
1091     [self writePassed:result inFile:path atLine:line message:@"expected (not) %x, got %x", expected, actual];
1092 }
1093
1094 #pragma mark -
1095 #pragma mark int test methods
1096
1097 - (void)testIsInt:(char *)type inFile:(char *)path atLine:(int)line
1098 {
1099     BOOL result = (strcmp(type, "i") == 0);
1100     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"i\", got \"%s\"", type]];
1101 }
1102
1103 - (void)testIsNotInt:(char *)type inFile:(char *)path atLine:(int)line
1104 {
1105     BOOL result = (strcmp(type, "i") != 0);
1106     [self writePassed:result
1107                inFile:path
1108                atLine:line
1109               message:[NSString stringWithFormat:@"expected type (not) \"i\", got \"%s\"", type]];
1110 }
1111
1112 - (void)testIntPositive:(int)aInt inFile:(char *)path atLine:(int)line
1113 {
1114     [self testInt:aInt greaterThan:(int)0 inFile:path atLine:line];
1115 }
1116
1117 - (void)testIntNegative:(int)aInt inFile:(char *)path atLine:(int)line
1118 {
1119     [self testInt:aInt lessThan:(int)0 inFile:path atLine:line];
1120 }
1121
1122 - (void)testIntZero:(int)aInt inFile:(char *)path atLine:(int)line
1123 {
1124     [self testInt:aInt isEqualTo:(int)0 inFile:path atLine:line];
1125 }
1126
1127 - (void)testIntNotZero:(int)aInt inFile:(char *)path atLine:(int)line
1128 {
1129     [self testInt:aInt isNotEqualTo:(int)0 inFile:path atLine:line];
1130 }
1131
1132 - (void)testInt:(int)actual isEqualTo:(int)expected inFile:(char *)path atLine:(int)line
1133 {
1134     BOOL result = (actual == expected);
1135     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat:@"expected %d, got %d", expected, actual]];
1136 }
1137
1138 - (void)testInt:(int)actual isNotEqualTo:(int)expected inFile:(char *)path atLine:(int)line
1139 {
1140     BOOL result = (actual != expected);
1141     [self writePassed:result
1142                inFile:path
1143                atLine:line
1144               message:[NSString stringWithFormat:@"expected (not) %d, got %d", expected, actual]];
1145 }
1146
1147 - (void)testInt:(int)actual greaterThan:(int)expected inFile:(char *)path atLine:(int)line
1148 {
1149     BOOL result = (actual > expected);
1150     [self writePassed:result
1151                inFile:path
1152                atLine:line
1153               message:[NSString stringWithFormat:@"expected > %d, got %d", expected, actual]];
1154 }
1155
1156 - (void)testInt:(int)actual notGreaterThan:(int)expected inFile:(char *)path atLine:(int)line
1157 {
1158     BOOL result = (actual <= expected);
1159     [self writePassed:result
1160                inFile:path
1161                atLine:line
1162               message:[NSString stringWithFormat:@"expected <= %d, got %d", expected, actual]];
1163 }
1164
1165 - (void)testInt:(int)actual lessThan:(int)expected inFile:(char *)path atLine:(int)line
1166 {
1167     BOOL result = (actual < expected);
1168     [self writePassed:result
1169                inFile:path
1170                atLine:line
1171               message:[NSString stringWithFormat:@"expected < %d, got %d", expected, actual]];
1172 }
1173
1174 - (void)testInt:(int)actual notLessThan:(int)expected inFile:(char *)path atLine:(int)line
1175 {
1176     BOOL result = (actual >= expected);
1177     [self writePassed:result
1178                inFile:path
1179                atLine:line
1180               message:[NSString stringWithFormat:@"expected >= %d, got %d", expected, actual]];
1181 }
1182
1183 #pragma mark -
1184 #pragma mark unsigned test methods
1185
1186 - (void)testIsUnsigned:(char *)type inFile:(char *)path atLine:(int)line
1187 {
1188     BOOL result = (strcmp(type, "I") == 0);
1189     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"I\", got \"%s\"", type]];
1190 }
1191
1192 - (void)testIsNotUnsigned:(char *)type inFile:(char *)path atLine:(int)line
1193 {
1194     BOOL result = (strcmp(type, "I") != 0);
1195     [self writePassed:result
1196                inFile:path
1197                atLine:line
1198               message:[NSString stringWithFormat:@"expected type (not) \"I\", got \"%s\"", type]];
1199 }
1200
1201 - (void)testUnsignedZero:(unsigned)aUnsigned inFile:(char *)path atLine:(int)line
1202 {
1203     return [self testUnsigned:aUnsigned isEqualTo:(unsigned)0 inFile:path atLine:line];
1204 }
1205
1206 - (void)testUnsignedNotZero:(unsigned)aUnsigned inFile:(char *)path atLine:(int)line
1207 {
1208     return [self testUnsigned:aUnsigned isNotEqualTo:(unsigned)0 inFile:path atLine:line];
1209 }
1210
1211 - (void)testUnsigned:(unsigned)actual isEqualTo:(unsigned)expected inFile:(char *)path atLine:(int)line
1212 {
1213     BOOL result = (actual == expected);
1214     [self writePassed:result
1215                inFile:path
1216                atLine:line
1217               message:[NSString stringWithFormat:@"expected %u, got %u", expected, actual]];
1218 }
1219
1220 - (void)testUnsigned:(unsigned)actual isNotEqualTo:(unsigned)expected inFile:(char *)path atLine:(int)line
1221 {
1222     BOOL result = (actual != expected);
1223     [self writePassed:result
1224                inFile:path
1225                atLine:line
1226               message:[NSString stringWithFormat:@"expected (not) %u, got %u", expected, actual]];
1227 }
1228
1229 - (void)testUnsigned:(unsigned)actual greaterThan:(unsigned)expected inFile:(char *)path atLine:(int)line
1230 {
1231     BOOL result = (actual > expected);
1232     [self writePassed:result
1233                inFile:path
1234                atLine:line
1235               message:[NSString stringWithFormat:@"expected > %u, got %u", expected, actual]];
1236 }
1237
1238 - (void)testUnsigned:(unsigned)actual notGreaterThan:(unsigned)expected inFile:(char *)path atLine:(int)line
1239 {
1240     BOOL result = (actual <= expected);
1241     [self writePassed:result
1242                inFile:path
1243                atLine:line
1244               message:[NSString stringWithFormat:@"expected <= %u, got %u", expected, actual]];
1245 }
1246
1247 - (void)testUnsigned:(unsigned)actual lessThan:(unsigned)expected inFile:(char *)path atLine:(int)line
1248 {
1249     BOOL result = (actual < expected);
1250     [self writePassed:result
1251                inFile:path
1252                atLine:line
1253               message:[NSString stringWithFormat:@"expected < %u, got %u", expected, actual]];
1254 }
1255
1256 - (void)testUnsigned:(unsigned)actual notLessThan:(unsigned)expected inFile:(char *)path atLine:(int)line
1257 {
1258     BOOL result = (actual >= expected);
1259     [self writePassed:result
1260                inFile:path
1261                atLine:line
1262               message:[NSString stringWithFormat:@"expected >= %u, got %u", expected, actual]];
1263 }
1264
1265 #pragma mark -
1266 #pragma mark float test methods without error margins
1267
1268 - (void)testIsFloat:(char *)type inFile:(char *)path atLine:(int)line
1269 {
1270     BOOL result = (strcmp(type, "f") == 0);
1271     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"f\", got \"%s\"", type]];
1272 }
1273
1274 - (void)testIsNotFloat:(char *)type inFile:(char *)path atLine:(int)line
1275 {
1276     BOOL result = (strcmp(type, "f") != 0);
1277     [self writePassed:result
1278                inFile:path
1279                atLine:line
1280               message:[NSString stringWithFormat:@"expected type (not) \"f\", got \"%s\"", type]];
1281 }
1282
1283 - (void)testFloatPositive:(float)aFloat inFile:(char *)path atLine:(int)line
1284 {
1285     return [self testFloat:aFloat greaterThan:(float)0.0 withinError:(float)0.0 inFile:path atLine:line];
1286 }
1287
1288 - (void)testFloatNegative:(float)aFloat inFile:(char *)path atLine:(int)line
1289 {
1290     return [self testFloat:aFloat lessThan:(float)0.0 withinError:(float)0.0 inFile:path atLine:line];
1291 }
1292
1293 - (void)testFloatZero:(float)aFloat inFile:(char *)path atLine:(int)line
1294 {
1295     return [self testFloat:aFloat isEqualTo:(float)0.0 withinError:(float)0.0 inFile:path atLine:line];
1296 }
1297
1298 - (void)testFloatNotZero:(float)aFloat inFile:(char *)path atLine:(int)line
1299 {
1300     return [self testFloat:aFloat isNotEqualTo:(float)0.0 withinError:(float)0.0 inFile:path atLine:line];
1301 }
1302
1303 - (void)testFloat:(float)actual isEqualTo:(float)expected inFile:(char *)path atLine:(int)line
1304 {
1305     return [self testFloat:actual isEqualTo:expected withinError:(float)0.0 inFile:path atLine:line];
1306 }
1307
1308 - (void)testFloat:(float)actual isNotEqualTo:(float)expected inFile:(char *)path atLine:(int)line
1309 {
1310     return [self testFloat:actual isNotEqualTo:expected withinError:(float)0.0 inFile:path atLine:line];
1311 }
1312
1313 - (void)testFloat:(float)actual greaterThan:(float)expected inFile:(char *)path atLine:(int)line
1314 {
1315     return [self testFloat:actual greaterThan:expected withinError:(float)0.0 inFile:path atLine:line];
1316 }
1317
1318 - (void)testFloat:(float)actual notGreaterThan:(float)expected inFile:(char *)path atLine:(int)line
1319 {
1320     return [self testFloat:actual notGreaterThan:expected withinError:(float)0.0 inFile:path atLine:line];
1321 }
1322
1323 - (void)testFloat:(float)actual lessThan:(float)expected inFile:(char *)path atLine:(int)line
1324 {
1325     return [self testFloat:actual lessThan:expected withinError:(float)0.0 inFile:path atLine:line];
1326 }
1327
1328 - (void)testFloat:(float)actual notLessThan:(float)expected inFile:(char *)path atLine:(int)line
1329 {
1330     return [self testFloat:actual notLessThan:expected withinError:(float)0.0 inFile:path atLine:line];
1331 }
1332
1333 #pragma mark -
1334 #pragma mark float test methods with error margins
1335
1336 - (void)testFloatPositive:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line
1337 {
1338     return [self testFloat:aFloat greaterThan:(float)0.0 withinError:error inFile:path atLine:line];
1339 }
1340
1341 - (void)testFloatNegative:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line
1342 {
1343     return [self testFloat:aFloat lessThan:(float)0.0 withinError:error inFile:path atLine:line];
1344 }
1345
1346 - (void)testFloatZero:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line
1347 {
1348     return [self testFloat:aFloat isEqualTo:(float)0.0 withinError:error inFile:path atLine:line];
1349 }
1350
1351 - (void)testFloatNotZero:(float)aFloat withinError:(float)error inFile:(char *)path atLine:(int)line
1352 {
1353     return [self testFloat:aFloat isNotEqualTo:(float)0.0 withinError:error inFile:path atLine:line];
1354 }
1355
1356 - (void)testFloat:(float)actual isEqualTo:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1357 {
1358     BOOL result = (fabsf(actual - expected) <= error);
1359     [self writePassed:result
1360                inFile:path
1361                atLine:line
1362               message:
1363         [NSString stringWithFormat:@"expected %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1364 }
1365
1366 - (void)testFloat:(float)actual isNotEqualTo:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1367 {
1368     BOOL result = (fabsf(actual - expected) > -error);
1369     [self writePassed:result
1370                inFile:path
1371                atLine:line
1372               message:
1373         [NSString stringWithFormat:@"expected (not) %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1374 }
1375
1376 - (void)testFloat:(float)actual greaterThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1377 {
1378     BOOL result = ((actual - expected) > -error);
1379     [self writePassed:result
1380                inFile:path
1381                atLine:line
1382               message:
1383         [NSString stringWithFormat:@"expected > %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1384 }
1385
1386 - (void)testFloat:(float)actual notGreaterThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1387 {
1388     BOOL result = ((actual - expected) <= error);
1389     [self writePassed:result
1390                inFile:path
1391                atLine:line
1392               message:
1393         [NSString stringWithFormat:@"expected <= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1394 }
1395
1396 - (void)testFloat:(float)actual lessThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1397 {
1398     BOOL result = ((actual - expected) < -error);
1399     [self writePassed:result
1400                inFile:path
1401                atLine:line
1402               message:
1403         [NSString stringWithFormat:@"expected < %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1404 }
1405
1406 - (void)testFloat:(float)actual notLessThan:(float)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1407 {
1408     BOOL result = ((actual - expected) >= error);
1409     [self writePassed:result
1410                inFile:path
1411                atLine:line
1412               message:
1413         [NSString stringWithFormat:@"expected >= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1414 }
1415
1416 #pragma mark -
1417 #pragma mark double test methods without error margins
1418
1419 - (void)testIsDouble:(char *)type inFile:(char *)path atLine:(int)line
1420 {
1421     BOOL result = (strcmp(type, "d") == 0);
1422     [self writePassed:result inFile:path atLine:line message:[NSString stringWithFormat: @"expected type \"d\", got \"%s\"", type]];
1423 }
1424
1425 - (void)testIsNotDouble:(char *)type inFile:(char *)path atLine:(int)line
1426 {
1427     BOOL result = (strcmp(type, "d") != 0);
1428     [self writePassed:result
1429                inFile:path
1430                atLine:line
1431               message:[NSString stringWithFormat:@"expected type (not) \"d\", got \"%s\"", type]];
1432 }
1433
1434 - (void)testDoublePositive:(double)aDouble inFile:(char *)path atLine:(int)line
1435 {
1436     return [self testDouble:aDouble greaterThan:(double)0.0 withinError:(double)0.0 inFile:path atLine:line];
1437 }
1438
1439 - (void)testDoubleNegative:(double)aDouble inFile:(char *)path atLine:(int)line
1440 {
1441     return [self testDouble:aDouble lessThan:(double)0.0 withinError:(double)0.0 inFile:path atLine:line];
1442 }
1443
1444 - (void)testDoubleZero:(double)aDouble inFile:(char *)path atLine:(int)line
1445 {
1446     return [self testDouble:aDouble isEqualTo:(double)0.0 withinError:(double)0.0 inFile:path atLine:line];
1447 }
1448
1449 - (void)testDoubleNotZero:(double)aDouble inFile:(char *)path atLine:(int)line
1450 {
1451     return [self testDouble:aDouble isNotEqualTo:(double)0.0 withinError:(double)0.0 inFile:path atLine:line];
1452 }
1453
1454 - (void)testDouble:(double)actual isEqualTo:(double)expected inFile:(char *)path atLine:(int)line
1455 {
1456     return [self testDouble:actual isEqualTo:expected withinError:(double)0.0 inFile:path atLine:line];
1457 }
1458
1459 - (void)testDouble:(double)actual isNotEqualTo:(double)expected inFile:(char *)path atLine:(int)line
1460 {
1461     return [self testDouble:actual isNotEqualTo:expected withinError:(double)0.0 inFile:path atLine:line];
1462 }
1463
1464 - (void)testDouble:(double)actual greaterThan:(double)expected inFile:(char *)path atLine:(int)line
1465 {
1466     return [self testDouble:actual greaterThan:expected withinError:(double)0.0 inFile:path atLine:line];
1467 }
1468
1469 - (void)testDouble:(double)actual notGreaterThan:(double)expected inFile:(char *)path atLine:(int)line
1470 {
1471     return [self testDouble:actual notGreaterThan:expected withinError:(double)0.0 inFile:path atLine:line];
1472 }
1473
1474 - (void)testDouble:(double)actual lessThan:(double)expected inFile:(char *)path atLine:(int)line
1475 {
1476     return [self testDouble:actual lessThan:expected withinError:(double)0.0 inFile:path atLine:line];
1477 }
1478
1479 - (void)testDouble:(double)actual notLessThan:(double)expected inFile:(char *)path atLine:(int)line
1480 {
1481     return [self testDouble:actual notLessThan:expected withinError:(double)0.0 inFile:path atLine:line];
1482 }
1483
1484 #pragma mark -
1485 #pragma mark double test methods with error margins
1486
1487 - (void)testDoublePositive:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line
1488 {
1489     return [self testDouble:aDouble greaterThan:(double)0.0 withinError:error inFile:path atLine:line];
1490 }
1491
1492 - (void)testDoubleNegative:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line
1493 {
1494     return [self testDouble:aDouble lessThan:(double)0.0 withinError:error inFile:path atLine:line];
1495 }
1496
1497 - (void)testDoubleZero:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line
1498 {
1499     return [self testDouble:aDouble isEqualTo:(double)0.0 withinError:error inFile:path atLine:line];
1500 }
1501
1502 - (void)testDoubleNotZero:(double)aDouble withinError:(double)error inFile:(char *)path atLine:(int)line
1503 {
1504     return [self testDouble:aDouble isNotEqualTo:(double)0.0 withinError:error inFile:path atLine:line];
1505 }
1506
1507 - (void)testDouble:(double)actual isEqualTo:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line
1508 {
1509     BOOL result = (fabs(actual - expected) <= error);
1510     [self writePassed:result
1511                inFile:path
1512                atLine:line
1513               message:
1514         [NSString stringWithFormat:@"expected %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1515 }
1516
1517 - (void)testDouble:(double)actual isNotEqualTo:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line
1518 {
1519     BOOL result = (fabs(actual - expected) > -error);
1520     [self writePassed:result
1521                inFile:path
1522                atLine:line
1523               message:
1524         [NSString stringWithFormat:@"expected (not) %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1525 }
1526
1527 - (void)testDouble:(double)actual greaterThan:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line
1528 {
1529     BOOL result = ((actual - expected) > -error);
1530     [self writePassed:result
1531                inFile:path
1532                atLine:line
1533               message:
1534         [NSString stringWithFormat:@"expected > %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1535 }
1536
1537 - (void)testDouble:(double)actual notGreaterThan:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line
1538 {
1539     BOOL result = ((actual - expected) <= error);
1540     [self writePassed:result
1541                inFile:path
1542                atLine:line
1543               message:
1544         [NSString stringWithFormat:@"expected <= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1545 }
1546
1547 - (void)testDouble:(double)actual lessThan:(double)expected withinError:(double)error inFile:(char *)path atLine:(int)line
1548 {
1549     BOOL result = ((actual - expected) < -error);
1550     [self writePassed:result
1551                inFile:path
1552                atLine:line
1553               message:
1554         [NSString stringWithFormat:@"expected < %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1555 }
1556
1557 - (void)testDouble:(double)actual notLessThan:(double)expected withinError:(float)error inFile:(char *)path atLine:(int)line
1558 {
1559     BOOL result = ((actual - expected) >= error);
1560     [self writePassed:result
1561                inFile:path
1562                atLine:line
1563               message:
1564         [NSString stringWithFormat:@"expected >= %f (%C%f), got %f", expected, WO_UNICODE_PLUS_MINUS_SIGN, error, actual]];
1565 }
1566
1567 #pragma mark -
1568 #pragma mark object test methods
1569
1570 - (void)testObject:(id)actual isEqualTo:(id)expected inFile:(char *)path atLine:(int)line
1571 {
1572     BOOL equal = NO;
1573     if (!actual && !expected) equal = YES; // equal (both nil)
1574     else if (actual) equal = [actual isEqual:expected];
1575     BOOL expectedTruncated, actualTruncated;
1576     [self writePassed:equal inFile:path atLine:line message:@"expected \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1577     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1578     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1579 }
1580
1581 - (void)testObject:(id)actual isNotEqualTo:(id)expected inFile:(char *)path atLine:(int)line
1582 {
1583     BOOL equal = NO;
1584     if (!actual && !expected) equal = YES; // equal (both nil)
1585     else if (actual) equal = [actual isEqual:expected];
1586     BOOL expectedTruncated, actualTruncated;
1587     [self writePassed:(!equal)
1588                inFile:path
1589                atLine:line
1590               message:@"expected (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1591     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1592     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1593 }
1594
1595 #pragma mark -
1596 #pragma mark NSString test methods
1597
1598 - (void)testString:(NSString *)actual isEqualTo:(NSString *)expected inFile:(char *)path atLine:(int)line
1599 {
1600     if (actual && ![actual isKindOfClass:[NSString class]])
1601         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1602                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1603                            inFile:path
1604                            atLine:line];
1605     if (expected && ![expected isKindOfClass:[NSString class]])
1606         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1607                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1608                            inFile:path
1609                            atLine:line];
1610     BOOL equal = NO;
1611     if (!actual && !expected) equal = YES; // equal (both nil)
1612     else if (actual) equal = [actual isEqualToString:expected];
1613     BOOL expectedTruncated, actualTruncated;
1614     [self writePassed:equal inFile:path atLine:line message:@"expected \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1615     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1616     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1617 }
1618
1619 - (void)testString:(NSString *)actual isNotEqualTo:(NSString *)expected inFile:(char *)path atLine:(int)line
1620 {
1621     if (actual && ![actual isKindOfClass:[NSString class]])
1622         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1623                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1624                            inFile:path
1625                            atLine:line];
1626     if (expected && ![expected isKindOfClass:[NSString class]])
1627         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1628                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1629                            inFile:path
1630                            atLine:line];
1631     BOOL equal = NO;
1632     if (!actual && !expected) equal = YES; // equal (both nil)
1633     else if (actual) equal = [actual isEqualToString:expected];
1634     BOOL expectedTruncated, actualTruncated;
1635     [self writePassed:equal
1636                inFile:path
1637                atLine:line
1638               message:@"expected (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1639     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1640     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1641 }
1642
1643 - (void)testString:(NSString *)actual hasPrefix:(NSString *)expected inFile:(char *)path atLine:(int)line
1644 {
1645     if (!expected)
1646         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1647                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1648                            inFile:path
1649                            atLine:line];
1650     if (actual && ![actual isKindOfClass:[NSString class]])
1651         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1652                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1653                            inFile:path
1654                            atLine:line];
1655     if (![expected isKindOfClass:[NSString class]])
1656         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1657                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1658                            inFile:path
1659                            atLine:line];
1660     BOOL result = actual ? NO : [actual hasPrefix:expected];
1661     BOOL expectedTruncated, actualTruncated;
1662     [self writePassed:result
1663                inFile:path
1664                atLine:line
1665               message:@"expected prefix \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1666     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1667     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1668 }
1669
1670 - (void)testString:(NSString *)actual doesNotHavePrefix:(NSString *)expected inFile:(char *)path atLine:(int)line
1671 {
1672     if (!expected)
1673         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1674                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1675                            inFile:path
1676                            atLine:line];
1677     if (actual && ![actual isKindOfClass:[NSString class]])
1678         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1679                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1680                            inFile:path
1681                            atLine:line];
1682     if (![expected isKindOfClass:[NSString class]])
1683         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1684                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1685                            inFile:path
1686                            atLine:line];
1687     BOOL result = actual ? (![actual hasPrefix:expected]) : NO;
1688     BOOL expectedTruncated, actualTruncated;
1689     [self writePassed:result
1690                inFile:path
1691                atLine:line
1692               message:@"expected prefix (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1693     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1694     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1695 }
1696
1697 - (void)testString:(NSString *)actual hasSuffix:(NSString *)expected inFile:(char *)path atLine:(int)line
1698 {
1699     if (!expected)
1700         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1701                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1702                            inFile:path
1703                            atLine:line];
1704     if (actual && ![actual isKindOfClass:[NSString class]])
1705         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1706                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1707                            inFile:path
1708                            atLine:line];
1709     if (![expected isKindOfClass:[NSString class]])
1710         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1711                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1712                            inFile:path
1713                            atLine:line];
1714     BOOL result = actual ? NO : [actual hasSuffix:expected];
1715     BOOL expectedTruncated, actualTruncated;
1716     [self writePassed:result
1717                inFile:path
1718                atLine:line
1719               message:@"expected suffix \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1720     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1721     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1722 }
1723
1724 - (void)testString:(NSString *)actual doesNotHaveSuffix:(NSString *)expected inFile:(char *)path atLine:(int)line
1725 {
1726     if (!expected)
1727         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1728                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1729                            inFile:path
1730                            atLine:line];
1731     if (actual && ![actual isKindOfClass:[NSString class]])
1732         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1733                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1734                            inFile:path
1735                            atLine:line];
1736     if (![expected isKindOfClass:[NSString class]])
1737         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1738                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1739                            inFile:path
1740                            atLine:line];
1741     BOOL result = actual ? (![actual hasSuffix:expected]) : NO;
1742     BOOL expectedTruncated, actualTruncated;
1743     [self writePassed:result
1744                inFile:path
1745                atLine:line
1746               message:@"expected suffix (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1747     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1748     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1749 }
1750
1751 - (void)testString:(NSString *)actual contains:(NSString *)expected inFile:(char *)path atLine:(int)line
1752 {
1753     if (!expected)
1754         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1755                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1756                            inFile:path
1757                            atLine:line];
1758     if (actual && ![actual isKindOfClass:[NSString class]])
1759         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1760                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1761                            inFile:path
1762                            atLine:line];
1763     if (![expected isKindOfClass:[NSString class]])
1764         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1765                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1766                            inFile:path
1767                            atLine:line];
1768     BOOL result = actual ? NO : (!NSEqualRanges([actual rangeOfString:expected], NSMakeRange(NSNotFound, 0)));
1769     BOOL expectedTruncated, actualTruncated;
1770     [self writePassed:result
1771                inFile:path
1772                atLine:line
1773               message:@"expected contains \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1774     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1775     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1776 }
1777
1778 - (void)testString:(NSString *)actual doesNotContain:(NSString *)expected inFile:(char *)path atLine:(int)line
1779 {
1780     if (!expected)
1781         [NSException WOTest_raise:WO_TEST_NIL_PARAMETER_EXCEPTION
1782                            reason:WO_NIL_PARAMETER_EXCEPTION_REASON
1783                            inFile:path
1784                            atLine:line];
1785     if (actual && ![actual isKindOfClass:[NSString class]])
1786         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1787                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(actual)
1788                            inFile:path
1789                            atLine:line];
1790     if (![expected isKindOfClass:[NSString class]])
1791         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1792                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(expected)
1793                            inFile:path
1794                            atLine:line];
1795     BOOL result = actual ? YES : (NSEqualRanges([actual rangeOfString:expected], NSMakeRange(NSNotFound, 0)));
1796     BOOL expectedTruncated, actualTruncated;
1797     [self writePassed:result
1798                inFile:path
1799                atLine:line
1800               message:@"expected contains (not) \"%@\", got \"%@\"", WO_DESC(expected), WO_DESC(actual)];
1801     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1802     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1803 }
1804
1805 #pragma mark -
1806 #pragma mark NSArray test methods
1807
1808 - (void)testArray:(NSArray *)actual isEqualTo:(NSArray *)expected inFile:(char *)path atLine:(int)line
1809 {
1810     if (actual && ![actual isKindOfClass:[NSArray class]])
1811         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1812                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(actual)
1813                             inFile:path
1814                             atLine:line];
1815     if (expected && ![expected isKindOfClass:[NSArray class]])
1816         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1817                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(expected)
1818                             inFile:path
1819                             atLine:line];
1820     BOOL equal = NO;
1821     if (!actual && !expected) equal = YES; // equal (both nil)
1822     else if (actual) equal = [actual isEqualToArray:expected];
1823     BOOL expectedTruncated, actualTruncated;
1824     [self writePassed:equal inFile:path atLine:line message:@"expected %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1825     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1826     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1827 }
1828
1829 - (void)testArray:(NSArray *)actual isNotEqualTo:(NSArray *)expected inFile:(char *)path atLine:(int)line
1830 {
1831     if (actual && ![actual isKindOfClass:[NSArray class]])
1832         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1833                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(actual)
1834                             inFile:path
1835                             atLine:line];
1836     if (expected && ![expected isKindOfClass:[NSArray class]])
1837         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1838                             reason:WO_EXPECTED_ARRAY_EXCEPTION_REASON(expected)
1839                             inFile:path
1840                             atLine:line];
1841     BOOL equal = NO;
1842     if (!actual && !expected) equal = YES; // equal (both nil)
1843     else if (actual) equal = [actual isEqualToArray:expected];
1844     BOOL expectedTruncated, actualTruncated;
1845     [self writePassed:(!equal) inFile:path atLine:line message:@"expected (not) %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1846     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1847     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1848 }
1849
1850 #pragma mark -
1851 #pragma mark NSDictionary test methods
1852
1853 - (void)testDictionary:(NSDictionary *)actual isEqualTo:(NSDictionary *)expected inFile:(char *)path atLine:(int)line
1854 {
1855     if (actual && ![actual isKindOfClass:[NSArray class]])
1856         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1857                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(actual)
1858                             inFile:path
1859                             atLine:line];
1860     if (expected && ![expected isKindOfClass:[NSArray class]])
1861         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1862                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(expected)
1863                             inFile:path
1864                             atLine:line];
1865     BOOL equal = NO;
1866     if (!actual && !expected) equal = YES; // equal (both nil)
1867     else if (actual) equal = [actual isEqualToDictionary:expected];
1868     BOOL expectedTruncated, actualTruncated;
1869     [self writePassed:equal inFile:path atLine:line message:@"expected %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1870     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1871     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1872 }
1873
1874 - (void)testDictionary:(NSDictionary *)actual isNotEqualTo:(NSDictionary *)expected inFile:(char *)path atLine:(int)line
1875 {
1876     if (actual && ![actual isKindOfClass:[NSDictionary class]])
1877         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1878                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(actual)
1879                             inFile:path
1880                             atLine:line];
1881     if (expected && ![expected isKindOfClass:[NSDictionary class]])
1882         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1883                             reason:WO_EXPECTED_DICTIONARY_EXCEPTION_REASON(expected)
1884                             inFile:path
1885                             atLine:line];
1886     BOOL equal = NO;
1887     if (!actual && !expected) equal = YES; // equal (both nil)
1888     else if (actual) equal = [actual isEqualToDictionary:expected];
1889     BOOL expectedTruncated, actualTruncated;
1890     [self writePassed:(!equal) inFile:path atLine:line message:@"expected (not) %@, got %@", WO_DESC(expected), WO_DESC(actual)];
1891     if (expectedTruncated)  _WOLog(@"expected result (not truncated): %@", WO_LONG_DESC(expected));
1892     if (actualTruncated)    _WOLog(@"actual result (not truncated): %@", WO_LONG_DESC(actual));
1893 }
1894
1895 #pragma mark -
1896 #pragma mark Exception test methods
1897
1898 - (void)testThrowsException:(id)exception inFile:(char *)path atLine:(int)line
1899 {
1900     BOOL result = (exception ? YES : NO);
1901     [self writePassed:result
1902                inFile:path
1903                atLine:line
1904               message:[NSString stringWithFormat:@"expected exception, got %@", [NSException WOTest_nameForException:exception]]];
1905 }
1906
1907 - (void)testDoesNotThrowException:(id)exception inFile:(char *)path atLine:(int)line
1908 {
1909     BOOL result = (exception ? NO : YES);
1910     [self writePassed:result
1911                inFile:path
1912                atLine:line
1913               message:
1914         [NSString stringWithFormat:@"expected no exception, got %@", [NSException WOTest_nameForException:exception]]];
1915 }
1916
1917 - (void)testThrowsException:(id)exception named:(NSString *)name inFile:(char *)path atLine:(int)line
1918 {
1919     if (name && ![name isKindOfClass:[NSString class]])
1920     {
1921         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1922                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(name)
1923                            inFile:path
1924                            atLine:line];
1925     }
1926
1927     BOOL        result      = NO;
1928     NSString    *actualName = [NSException WOTest_nameForException:exception];
1929
1930     if (exception && [actualName isEqualToString:name]) result = YES;
1931
1932     [self writePassed:result
1933                inFile:path
1934                atLine:line
1935               message:[NSString stringWithFormat:@"expected %@, got %@", name, actualName]];
1936 }
1937
1938 - (void)testDoesNotThrowException:(id)exception named:(NSString *)name inFile:(char *)path atLine:(int)line
1939 {
1940     if (name && ![name isKindOfClass:[NSString class]])
1941     {
1942         [NSException WOTest_raise:WO_TEST_CLASS_MISMATCH_EXCEPTION
1943                            reason:WO_EXPECTED_STRING_EXCEPTION_REASON(name)
1944                            inFile:path
1945                            atLine:line];
1946     }
1947
1948     BOOL        result      = YES;
1949     NSString    *actualName = [NSException WOTest_nameForException:exception];
1950
1951     if (exception && [actualName isEqualToString:name]) result = NO;
1952
1953     [self writePassed:result inFile:path atLine:line message:@"expected (not) %@, got %@", name, actualName];
1954 }
1955
1956 #pragma mark -
1957 #pragma mark Random value generator methods
1958
1959 // TODO: move these into a category, these are more "test helpers" rather than actual "testing methods"
1960
1961 - (int)anInt
1962 {
1963     return (int)(WO_RANDOM_SIGN * (WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
1964 }
1965
1966 - (int)aPositiveInt
1967 {
1968     return (int)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1969 }
1970
1971 - (int)aNegativeInt
1972 {
1973     return (int)-(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1974 }
1975
1976 - (int)aZeroInt
1977 {
1978     return 0;
1979 }
1980
1981 - (int)aBigInt
1982 {
1983     return (int)(WO_RANDOM_SIGN * (WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
1984 }
1985
1986 - (int)aBigPositiveInt
1987 {
1988     return (int)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1989 }
1990
1991 - (int)aBigNegativeInt
1992 {
1993     return (int)-(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
1994 }
1995
1996 - (int)aSmallInt
1997 {
1998     return (int)(WO_RANDOM_SIGN * (WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
1999 }
2000
2001 - (int)aSmallPositiveInt
2002 {
2003     return (int)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2004 }
2005
2006 - (int)aSmallNegativeInt
2007 {
2008     return (int)-(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2009 }
2010
2011 - (unsigned)anUnsigned
2012 {
2013     return (unsigned)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2014 }
2015
2016 - (unsigned)aZeroUnsigned
2017 {
2018     return 0;
2019 }
2020
2021 - (unsigned)aBigUnsigned
2022 {
2023     return (unsigned)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2024 }
2025
2026 - (unsigned)aSmallUnsigned
2027 {
2028     return (unsigned)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2029 }
2030
2031 - (float)aFloat
2032 {
2033     return (float)(WO_RANDOM_SIGN * (WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2034 }
2035
2036 - (float)aPositiveFloat
2037 {
2038     return (float)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2039 }
2040
2041 - (float)aNegativeFloat
2042 {
2043     return (float)-(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2044 }
2045
2046 - (float)aZeroFloat
2047 {
2048     return 0.0;
2049 }
2050
2051 - (float)aBigFloat
2052 {
2053     return (float)(WO_RANDOM_SIGN * (WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2054 }
2055
2056 - (float)aBigPositiveFloat
2057 {
2058     return (float)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2059 }
2060
2061 - (float)aBigNegativeFloat
2062 {
2063     return (float)-(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2064 }
2065
2066 - (float)aSmallFloat
2067 {
2068     return (float)(WO_RANDOM_SIGN * (WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2069 }
2070
2071 - (float)aSmallPositiveFloat
2072 {
2073     return (float)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2074 }
2075
2076 - (float)aSmallNegativeFloat
2077 {
2078     return (float)-(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2079 }
2080
2081 - (double)aDouble
2082 {
2083     return (double)(WO_RANDOM_SIGN * (WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2084 }
2085
2086 - (double)aPositiveDouble
2087 {
2088     return (double)(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2089 }
2090
2091 - (double)aNegativeDouble
2092 {
2093     return (double)-(WO_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2094 }
2095
2096 - (double)aZeroDouble
2097 {
2098     return 0.0;
2099 }
2100
2101 - (double)aBigDouble
2102 {
2103     return (double)(WO_RANDOM_SIGN * (WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2104 }
2105
2106 - (double)aBigPositiveDouble
2107 {
2108     return (double)(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2109 }
2110
2111 - (double)aBigNegativeDouble
2112 {
2113     return (double)-(WO_BIG_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2114 }
2115
2116 - (double)aSmallDouble
2117 {
2118     return (double)(WO_RANDOM_SIGN * (WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN)));
2119 }
2120
2121 - (double)aSmallPositiveDouble
2122 {
2123     return (double)(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2124 }
2125
2126 - (double)aSmallNegativeDouble
2127 {
2128     return (double)-(WO_SMALL_TEST_VALUE + (WO_RANDOM_OFFSET * WO_RANDOM_SIGN));
2129 }
2130
2131 #pragma mark -
2132 #pragma mark Accessors
2133
2134 - (NSDate *)startDate
2135 {
2136     return [[startDate retain] autorelease];
2137 }
2138
2139 - (void)setStartDate:(NSDate *)aStartDate
2140 {
2141     [aStartDate retain];
2142     [startDate release];
2143     startDate = aStartDate;
2144 }
2145
2146 - (unsigned)classesWithTests
2147 {
2148     return classesWithTests;
2149 }
2150
2151 - (unsigned)classesWithoutTests
2152 {
2153     return classesWithoutTests;
2154 }
2155
2156 - (unsigned)methodsWithTests
2157 {
2158     return methodsWithTests;
2159 }
2160
2161 - (unsigned)testsRun
2162 {
2163     return testsRun;
2164 }
2165
2166 - (unsigned)testsPassed
2167 {
2168     return testsPassed;
2169 }
2170
2171 - (unsigned)testsFailed
2172 {
2173     return testsFailed;
2174 }
2175
2176 - (unsigned)uncaughtExceptions
2177 {
2178     return uncaughtExceptions;
2179 }
2180
2181 - (unsigned)testsFailedExpected
2182 {
2183     return testsFailedExpected;
2184 }
2185
2186 - (unsigned)testsPassedUnexpected
2187 {
2188     return testsPassedUnexpected;
2189 }
2190
2191 - (BOOL)expectFailures
2192 {
2193     return expectFailures;
2194 }
2195
2196 - (void)setExpectFailures:(BOOL)aValue
2197 {
2198     expectFailures = aValue;
2199 }
2200
2201 - (unsigned)lowLevelExceptionsExpected
2202 {
2203     return lowLevelExceptionsExpected;
2204 }
2205
2206 - (unsigned)lowLevelExceptionsUnexpected
2207 {
2208     return lowLevelExceptionsUnexpected;
2209 }
2210
2211 - (BOOL)expectLowLevelExceptions
2212 {
2213     return expectLowLevelExceptions;
2214 }
2215
2216 - (void)setExpectLowLevelExceptions:(BOOL)aValue
2217 {
2218     expectLowLevelExceptions = aValue;
2219 }
2220
2221 - (BOOL)handlesLowLevelExceptions
2222 {
2223     return handlesLowLevelExceptions;
2224 }
2225
2226 - (void)setHandlesLowLevelExceptions:(BOOL)aValue
2227 {
2228     handlesLowLevelExceptions = aValue;
2229 }
2230
2231 - (unsigned)verbosity
2232 {
2233     return verbosity;
2234 }
2235
2236 - (void)setVerbosity:(unsigned)aVerbosity
2237 {
2238     verbosity = aVerbosity;
2239 }
2240
2241 - (unsigned)trimInitialPathComponents
2242 {
2243     return trimInitialPathComponents;
2244 }
2245
2246 - (void)setTrimInitialPathComponents:(unsigned)aTrimInitialPathComponents
2247 {
2248     trimInitialPathComponents = aTrimInitialPathComponents;
2249 }
2250
2251 - (NSString *)lastReportedFile
2252 {
2253     return [[lastReportedFile retain] autorelease];
2254 }
2255
2256 - (void)setLastReportedFile:(NSString *)aLastReportedFile
2257 {
2258     if (lastReportedFile != aLastReportedFile)
2259     {
2260         [aLastReportedFile retain];
2261         [lastReportedFile release];
2262         lastReportedFile = aLastReportedFile;
2263     }
2264 }
2265
2266 - (BOOL)warnsAboutSignComparisons
2267 {
2268     return warnsAboutSignComparisons;
2269 }
2270
2271 - (void)setWarnsAboutSignComparisons:(BOOL)aWarnsAboutSignComparisons
2272 {
2273     warnsAboutSignComparisons = aWarnsAboutSignComparisons;
2274 }
2275
2276 @end