// $Id: UnitTest.m,v 1.12 2005/05/23 05:34:45 will_mason Exp $
// vi: set ft=objc:

/*
 * ObjectiveLib - a library of containers and algorithms for Objective-C
 *
 * Copyright (c) 2004
 * Will Mason
 *
 * Portions:
 *
 * Copyright (c) 1994
 * Hewlett-Packard Company
 *
 * Copyright (c) 1996,1997
 * Silicon Graphics Computer Systems, Inc.
 *
 * Copyright (c) 1997
 * Moscow Center for SPARC Technology
 *
 * Copyright (c) 1999 
 * Boris Fomitchev
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * You may contact the author at will_mason@users.sourceforge.net.
 */

// vi: set ft=objc:

#import "UnitTest.h"
#import "UnitTestRunner.h"
#import <Foundation/NSLock.h>
#import <Foundation/NSException.h>
#if defined(__NEXT_RUNTIME__)
#import <objc/objc-runtime.h>
#else
#import <objc/objc-api.h>
#endif

static NSLock* messageLock = nil;

@interface TestRecord : NSObject
{
@private
    NSString*   name;
    NSString*   qualifiedName;
    id          object;
    SEL         sel;
}

- (id) initWithName: (NSString*)n object: (id)obj selector: (SEL)s;
- (void) dealloc;
- (BOOL) isEqual: (id)object;
- (NSString*) recName;
- (id) object;
- (NSString*) qualifiedName;
- (SEL) selector;

@end

@implementation TestRecord

- (id) initWithName: (NSString*)n object: (id)obj selector: (SEL)s
{
    [super init];
    name = n;
    [name retain];
    object = obj;
    sel = s;
    return self;
}

- (void) dealloc
{
    [name release];
    if (qualifiedName != nil)
        [qualifiedName release];
    [super dealloc];
}

- (BOOL) isEqual: (id)obj
{
    return [obj isMemberOfClass: [TestRecord class]] &&
           [((TestRecord*)obj)->name isEqual: name] &&
           ((TestRecord*)obj)->object == object &&
           ((TestRecord*)obj)->sel == sel;
}

- (NSString*) recName
{
    return name;
}

- (id) object
{
    return object;
}

- (NSString*) qualifiedName
{
    if (qualifiedName == nil)
    {
        qualifiedName = [[NSString alloc]
            initWithFormat: @"[%s %@]", [object class]->name, name];
    }
    return qualifiedName;
}

- (SEL) selector
{
    return sel;
}

@end

@implementation UnitTest

- (id) init
{
    return [self initWithOut: stdout err: stderr];
}

- (id) initWithOut: (FILE*)o err: (FILE*)e
{
    Class myClass;
    void* itor = 0;
    struct objc_method_list* methods;
    int i;
    SEL theSEL;
    const char* theName;
    TestRecord* rec;

    NSAssert(sizeof(SEL) == sizeof(int), @"The type sizes are incorrect");

    [super init];

    failedTests = [[NSMutableArray alloc] initWithCapacity: 10];
    tests = [[NSMutableArray alloc] initWithCapacity: 10];
    outStream = o;
    errStream = e;

    myClass = [self class];
#if defined(__NEXT_RUNTIME__)
    methods = class_nextMethodList(myClass, &itor);
#else
    methods = myClass->methods;
#endif
    while (methods != NULL)
    {
        for (i = 0; i < methods->method_count; i++)
        {
            theSEL = methods->method_list[i].method_name;
#if defined(__NEXT_RUNTIME__)
            theName = sel_getName(theSEL);
#else
            theName = sel_get_name(theSEL);
#endif
            if (strncmp(theName, "test", 4) == 0 &&
                strlen(theName) > 4
#if defined(__NEXT_RUNTIME__)
               && method_getNumberOfArguments(&methods->method_list[i]) == 2)
#else
               )
#endif
            {
                rec = [[TestRecord alloc] initWithName: [NSString stringWithCString: theName]
                                          object: self
                                          selector: theSEL];
                [tests addObject: rec];
                [rec release];
            }
        }
#if defined(__NEXT_RUNTIME__)
        methods = class_nextMethodList(myClass, &itor);
#else
        methods = NULL;
#endif
    }

    if (messageLock == nil)
        messageLock = [[NSLock alloc] init];
    
    return self;
}

- (void) dealloc
{
    [failedTests release];
    [tests release];
    [super dealloc];
}

- (void) errInFile: (const char*)file line: (int)line format: (const char* const)format, ...
{
    char buf[8192];
    va_list args;
    id testInProcess = [UnitTestRunner testInProcess];

    [messageLock lock];
    NSAssert(errStream != NULL, @"The error stream may not be NULL");
    errorCount++;
    NSAssert(testInProcess != nil, @"There is no test in process");
    if (![failedTests containsObject: testInProcess])
        [failedTests addObject: testInProcess];
    sprintf(buf, "FAIL (%s,%i): %s", file, line, format);
    va_start(args, format);
    vfprintf(errStream, buf, args);
    va_end(args);
    fputc('\n', errStream);
    fflush(errStream);
    [messageLock unlock];
}

- (FILE*) errStream
{
    return errStream;
}

- (int) errorCount
{
    return errorCount;
}

- (BOOL) hasTestWithName: (NSString*)test
{
    id rec = [self lookUpTestWithName: test];
    return (rec != nil) ? YES : NO;
}

- (void) listFailedTestsToStream: (FILE*)stream delimiter: (char)delim
{
    int i;
    int count = [failedTests count];
    int last = count - 1;

    for (i = 0; i < count; i++)
    {
        fputs([[[failedTests objectAtIndex: i] qualifiedName] cString], stream);
        if (i != last)
            fputc(delim, stream);
    }
}

- (void) logMessage: (const char* const)format, ...
{
    va_list args;

    NSAssert(outStream != NULL, @"The output stream may not be NULL");
    va_start(args, format);
    [messageLock lock];
    vfprintf(outStream, format, args);
    va_end(args);
    fputc('\n', outStream);
    fflush(outStream);
    [messageLock unlock];
}

- (id) lookUpTestWithName: (NSString*)name
{
    NSEnumerator* itor = [tests objectEnumerator];
    id rec;

    while (rec = [itor nextObject])
    {
        if ([name isEqualToString: [rec recName]])
        {
            return rec;
        }
    }
    return nil;
}

- (FILE*) outStream
{
    return outStream;
}

- (BOOL) preventAutoLoad
{
    return NO;
}

- (void) runAllTests
{
    NSEnumerator* itor = [tests objectEnumerator];
    id test;

    while (test = [itor nextObject])
    {
        [self runTest: test];
    }
}

- (void) runTest: (id)test
{
    const char* cname;
    NSAutoreleasePool* autoPool;

    NSAssert(outStream != NULL, @"The output stream may not be NULL");
    autoPool = [[NSAutoreleasePool alloc] init];
    [UnitTestRunner pushTestInProcess: test];
    cname = [[test qualifiedName] cString];
    fprintf(outStream, "========================================================================\nBEGIN: %s\n------------------------------------------------------------------------\n", cname);


    if ([[test object] setUp])
    {
#if !defined(GNUSTEP)
        NS_DURING
#endif
            [[test object] performSelector: [test selector]];
#if !defined(GNUSTEP)
        NS_HANDLER
            fprintf(errStream, "*** Uncaught exception: %s - %s\n",
                [[localException name] cString], [[localException reason] cString]);
        NS_ENDHANDLER
#endif
        if (![[test object] tearDown])
            fprintf(errStream, "* %s tear-down failed *\n", cname);
    }
    else
    {
        fprintf(errStream, "* %s set-up failed *\n", cname);
    }
    [UnitTestRunner popTestInProcess];
    fprintf(outStream, "------------------------------------------------------------------------\n  END: %s\n========================================================================\n", cname);
    [autoPool release];
 }

- (BOOL) runTestWithName: (NSString*)name
{
    id test = [self lookUpTestWithName: name];

    if (test != nil)
    {
        [self runTest: test];
        return YES;
    }
    else
    {
        return NO;
    }
}

- (void) setStreamOut: (FILE*)o err: (FILE*)e
{
    outStream = o;
    errStream = e;
}

- (BOOL) setUp
{
    return YES;
}

- (BOOL) tearDown
{
    return YES;
}

- (int) testCount
{
    return [tests count];
}

- (NSArray*) testListQualified: (BOOL)flag
{
    NSMutableArray* names = [NSMutableArray arrayWithCapacity: [tests count]];
    NSEnumerator* itor = [tests objectEnumerator];
    id current;
    NSString* str;

    while (current = [itor nextObject])
    {
        if (flag)
            str = [current qualifiedName];
        else
            str = [current recName];
        [names addObject: str];
    }

    return names;
}

@end
