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