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