/***********************************************************************
**
**	Filename	: test.c
**	Project		: Unit Test
**	Subsystem	: 
**	Module		: Basic routines for performing a Unit Test
**	Document	: 
**
*************************************************************************
**
**	Brief Description
**	=================
**
**	Functions to provide a nice, consistent Unit Test infrastructure.
**	
**
*************************************************************************
**
**  Change History
**  --------------
**
** 20-Jun-2005    Graeme McKerrell     Populated lub_test_stop_here() t ensure it is reached when a
**                                                                optimising compiler is used.
** 7-Dec-2004    Graeme McKerrell     Updated to use the "lub_test_" prefix 
**                                    rather than "unittest_"
**  4  Mar 2004  Graeme McKerrell     Ported for use in Garibaldi
**  4  Oct 2002  Graeme McKerrell     updated to use central interface
**                                    definition
**  18-Mar-2002	 Graeme McKerrell     Added unitest_stop_here() support
**  16-Mar-2002	 Graeme McKerrell     LINTed...
**  14 Nov 2000  Brett B. Bonner      created
**
*************************************************************************
**
**  Copyright (C) 3Com Corporation. All Rights Reserved.
**
\************************************************************************/
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
/*lint -esym(534,vsprintf) */
/*lint -esym(632,va_list,__va_list) */
/*lint -esym(633,va_list,__gnuc_va_list) */
#include "lub/test.h"



/* Where to direct output (bitmasks) */
#define LUB_TEST_LOGTOFILE   0x1
#define LUB_TEST_LOGTOSTDOUT 0x2

/* Termination Mode */
typedef enum {
  ContinueOnFail,
  StopOnFail
} TerminationMode;

/* local variables */
static char unitTestName[80];
static char seqDescr[80];
static FILE *logp=NULL;
static lub_test_verbosity_t verbosity = LUB_TEST_NORMAL;
static int outputTo;
static lub_test_status_t unitTestStatus=LUB_TEST_PASS;
static int seqNum=0;
static int testNum=0;
static int failureCount=0;
static int testCount=0;
static TerminationMode termMode=ContinueOnFail;

/* local functions */
static void testLogNoIndent(lub_test_verbosity_t level, const char *format, ...);
static void printUsage(void);

/* definitions */
#define LOGGING_TO_FILE ((outputTo & LUB_TEST_LOGTOFILE) != 0)
#define LOGGING_TO_STDOUT ((outputTo & LUB_TEST_LOGTOSTDOUT) != 0)


/*
 * This is provided as a debug aid.
 */
void 
lub_test_stop_here(void)
{
  /* If any test fails, the unit test fails */
  unitTestStatus = LUB_TEST_FAIL;
  failureCount++;
  if (termMode == StopOnFail) {
    lub_test_end();
    exit(1);
  }
}

static lub_test_status_t
checkStatus(lub_test_status_t value)
{
    /* Test number gets incremented automatically... */
    testNum++;
    testCount++; /* update total number of tests performed */

    if(LUB_TEST_FAIL == value)
    {
        lub_test_stop_here();
    }
    return value;
}

/*******************************************************
* unit-test-level functions
********************************************************/
/* NAME:    unitTestLog
   PURPOSE: Log output to file and/or stdout, verbosity-filtered
   ARGS:    level - priority of this message.
                    Will be logged only if priority equals or exceeds
                    the current lub_test_verbosity_t level.
            format, args - printf-style format and parameters
   RETURN:  none
*/
static void 
unitTestLog(lub_test_verbosity_t level, const char *format, ...)
{
    va_list args;
    char string[320]; /* ought to be big enough; that's 4 lines */

    /* Turn format,args into a string */
    va_start(args, format);
    vsprintf(string, format, args);
    va_end(args);

    /* output to selected destination if lub_test_verbosity_t equals or exceeds
     current setting. */ 
    if (level <= verbosity) 
    {
        if (LOGGING_TO_FILE) 
        {
            if (NULL != logp)
            {
                fprintf(logp, "%s\n", string);
            }
            else
            {
                fprintf(stderr, "ERROR: Trying to log to file, but no logfile is open!\n");
            }
        }
        if (LOGGING_TO_STDOUT) 
        {
            fprintf(stdout, "%s\n", string);
        }
    }
}

/* NAME:    TestStartLog
   PURPOSE: Sets up logging destination(s) and opens logfile.
   ARGS:    whereToLog   - where output gets directed to
                           Bitmask of lub_test_LOGTOFILE, lub_test_LOGTOSTDOUT
            logfile      - file name to open.
                           Use NULL if not logging to file.
   RETURN:  BOOL_FALSE == failure
            BOOL_TRUE  == success
*/
static int TestStartLog(int whereToLog, const char *logFile) 
{
    bool_t status = BOOL_TRUE;
    /* Where are we logging? */
    outputTo = whereToLog;
  
    /* open log file */
    if (LOGGING_TO_FILE && (strlen(logFile) < 1)) 
    {
        status = BOOL_FALSE;
        fprintf(stderr, "ERROR: No logfile name specified.\n");  
    }
    if (LOGGING_TO_FILE && status) 
    {
        if ( (logp = fopen(logFile, "w")) == NULL ) 
        {
            status = BOOL_FALSE;
            fprintf(stderr, 
                    "ERROR: could not open log file '%s'.\n",
                    logFile);
        }
    }
    return status;
}

/* NAME:    lub_test_begin
   PURPOSE: Starts unit test.  
   ARGS:    format,args  - Specification of unit test name
   RETURN:  none
*/
void 
lub_test_begin(const char *name, ...) 
{
    va_list args;

    /* Process varargs list into unit test name string */
    va_start(args, name);
    vsprintf(unitTestName, name, args);
    va_end(args);
    unitTestLog(LUB_TEST_NORMAL, "BEGIN: Testing '%s'.", unitTestName);

    /* reset counters */
    seqNum=testNum=testCount=failureCount=0;
} 

/* NAME:    lub_test_get_status
   PURPOSE: Reports current unit test status
   ARGS:    none
   RETURN:  Status code - TESTPASS/TESTFAIL
*/
lub_test_status_t
lub_test_get_status(void)
{
    return unitTestStatus;
}

/* NAME:    lub_test_failure_count
   PURPOSE: Reports current number of test failures
   ARGS:    none
   RETURN:  Count of failures
*/
int 
lub_test_failure_count(void)
{
    return failureCount;
}

/* NAME:    lub_test_end
   PURPOSE: Ends test.  Closes log file.
   ARGS:    none
   RETURN:  none
*/
void 
lub_test_end(void)
{
    char result[40];

    if (unitTestStatus == LUB_TEST_PASS) 
    {
        sprintf(result, "PASSED (%d tests)",testCount);
    } 
    else 
    {
        if (failureCount == 1)
        {
            sprintf(result, "FAILED (%d failure, %d tests)", 
                    failureCount, testCount);
        }
        else 
        {
              sprintf(result, "FAILED (%d failures, %d tests)", 
                      failureCount, testCount);
        }
    }
    if ((termMode == ContinueOnFail) ||
        (unitTestStatus == LUB_TEST_PASS)) 
    {
        /* ran to end - either due to continue-on-fail, or because
           everything passed */ 
        unitTestLog(LUB_TEST_TERSE, "END: Test '%s' %s.\n",unitTestName, result);
    } 
    else 
    { 
        /* stopped on first failure */
        unitTestLog(LUB_TEST_TERSE, "END: Test '%s': STOPPED AT FIRST FAILURE.\n",unitTestName);    
    }
    if (LOGGING_TO_FILE) 
    {
        fclose(logp);
    }
}

/* NAME:    printUsage
   PURPOSE: Prints usage message
   ARGS:    none
   RETURN:  none
*/
static void 
printUsage(void) 
{
    printf("Usage:\n");
    printf("-terse, -normal, -verbose    : set lub_test_verbosity_t level (default: normal)\n");
    printf("-stoponfail, -continueonfail : set behavior upon failure (default: continue)\n");
    printf("-logfile [FILENAME]          : log to FILENAME (default: 'test.log')\n"); 
    printf("-nologfile                   : disable logging to file \n");
    printf("-stdout, -nostdout           : enable/disable logging to STDOUT\n");
    printf("-usage, -help                : print these options and exit\n");
    printf("\nAll arguments are optional.  Defaults are equivalent to: \n");
    printf("  -normal -continueonfail -logfile test.log -stdout\n");
}

/* NAME:    lub_test_parse_command_line
   PURPOSE: Parses command-line arguments & sets unit test options.
   ARGS:    argc, argv - as passed to main()
   RETURN:  none (will exit if there is an error)
*/
void 
lub_test_parse_command_line(int argc, const char * const *argv) 
{
    bool_t status=BOOL_TRUE;
    int logDest=0;
    static char defaultFile[] = "test.log"; 
    char *logFile;
    bool_t logFileAllocatedFromHeap=BOOL_FALSE;

    /* variables used to detect conflicting options */
    int verbset=0;
    int failset=0;
    int logfset=0;
    int stdset=0;

    /* Set logFile to defaultFile so there is always a valid filename.
       Also set logging destination to both STDOUT and FILE.
       These will be changed later if user specifies something else. */
    logFile = defaultFile;
    logDest = (LUB_TEST_LOGTOFILE | LUB_TEST_LOGTOSTDOUT);

    /* Loop through the command line arguments.  */
    while (--argc > 0) 
    {
        /* Usage/Help */
        if ((strstr(argv[argc], "-usage") != NULL) ||
            (strstr(argv[argc], "-help") != NULL)) 
        {
            printUsage();
            exit (0);
        }
        /* lub_test_verbosity_t Levels */
        else if (strstr(argv[argc], "-terse") != NULL) 
        { 
            verbosity = LUB_TEST_TERSE; verbset++;
        }
        else if (strstr(argv[argc], "-normal") != NULL) 
        {
            verbosity = LUB_TEST_NORMAL; verbset++;
        }
        else if (strstr(argv[argc], "-verbose") != NULL) 
        {
            verbosity = LUB_TEST_VERBOSE; verbset++;
        }
        /* Failure behavior */
        else if (strstr(argv[argc], "-stoponfail") != NULL) 
        {
            termMode = StopOnFail; failset++;
        }
        else if (strstr(argv[argc], "-continueonfail") != NULL) 
        {
            termMode = ContinueOnFail; failset++;
        }
        /* Log file options */
        else if (strstr(argv[argc], "-logfile") != NULL) 
        {
            logDest |= LUB_TEST_LOGTOFILE;
            /* Was a log file name specified?  
               We assume it's a file name if it doesn't start with '-' */
            if (strstr(argv[argc+1], "-") != argv[argc+1]) 
            {
                /* Yes, got a filename */
                logFile = (char *)malloc(strlen(argv[argc+1])+1);

                if (NULL == logFile) 
                {
                    status = BOOL_FALSE;
                    fprintf(stderr, "unitTestParseCL: ERROR: Memory allocation problem.\n");
                }
                else 
                {
                    /* all is well, go ahead and copy the string */
                    strcpy(logFile, argv[argc+1]);
                }
                logFileAllocatedFromHeap=BOOL_TRUE;
            }
            logfset++;
        }
        else if (strstr(argv[argc], "-nologfile") != NULL) 
        {
            logDest &= ~LUB_TEST_LOGTOFILE;
            logfset++;
        }
        /* Stdout options */
        else if (strstr(argv[argc], "-stdout") != NULL) 
        {
            logDest |= LUB_TEST_LOGTOSTDOUT;
            stdset++;
        }
        else if (strstr(argv[argc], "-nostdout") != NULL) 
        {
            logDest &= ~LUB_TEST_LOGTOSTDOUT;
            stdset++;
        }
        /* Unhandled arguments */
        else 
        {
            /* If the next argument down in the list is '-logfile', then
               this is the logfile name; don't complain. */
            if (strstr(argv[argc-1], "-logfile") == NULL) 
            {
                /* This is an unrecognized option. 
                   Don't bother setting status and forcing an exit; just ignore it. */
                fprintf(stderr,"Unrecognized argument: '%s', ignoring it...\n",argv[argc]);
            }
        }
    }
    /* See if there is a logging destination */
    if (logDest == 0) 
    {
        fprintf(stderr, "WARNING: No logging is enabled to either stdout or a logfile; expect no output.\n");
    }
    /* Make sure there were no conflicting options */
    if (verbset > 1) 
    {
        fprintf(stderr,"ERROR: conflicting lub_test_verbosity_t options specified.\n");
        fprintf(stderr,"       Specify only ONE of -terse, -normal, -verbose\n");
        status = BOOL_FALSE;
    }
    if (failset > 1) 
    {
        fprintf(stderr,"ERROR: conflicting Failure Mode options specified.\n");
        fprintf(stderr,"       Specify only ONE of -stoponfail, -continueonfail\n");
        status = BOOL_FALSE;
    }
    if (logfset > 1) 
    {
        fprintf(stderr,"ERROR: conflicting Logfile options specified.\n");
        fprintf(stderr,"       Specify only ONE of -logfile, -nologfile\n");
        status = BOOL_FALSE;
    }
    if (stdset > 1) 
    {
        fprintf(stderr,"ERROR: conflicting Stdout options specified.\n");
        fprintf(stderr,"       Specify only ONE of -stdout, -nostdout\n");
        status = BOOL_FALSE;
    }

    /* Set up log file, if things are OK so far */
    if (status && !TestStartLog(logDest, logFile))
    {
          status = BOOL_FALSE;
    }
    if (logFileAllocatedFromHeap) 
    {
        /*lint -e673 Possibly inappropriate deallocation (free) for 'static' data */
        free(logFile);
        /*lint +e673 */
    }
    if (BOOL_FALSE == status) 
    {
        fprintf(stderr, "Something bad has occurred.  Aborting...\n");
        exit(1);
    }

}

/*******************************************************
* sequence level functions
********************************************************/

/* NAME:    lub_test_seq_begin
   PURPOSE: Starts a new test sequence.
   ARGS:    num - sequence number, must be different than last one
            format, args - printf-style name for the sequence
   RETURN:  none
*/
void 
lub_test_seq_begin(int num, const char *seqName, ...)
{
    va_list args;

    /* Get sequence name */
    va_start(args, seqName);
    vsprintf(seqDescr, seqName, args);
    va_end(args);

    /* Start new sequence number */
    if (num == seqNum) 
    {
        seqNum++;
        unitTestLog(LUB_TEST_TERSE, "seq_start: duplicate sequence number.  Using next available sequence number (%d).\n", seqNum);
    } 
    else 
    {
        seqNum = num;
    }

    /* Reset test number */
    testNum = 0;

    /* Log beginning of sequence */
    lub_test_seq_log(LUB_TEST_NORMAL, "*** Sequence '%s' begins ***", seqDescr);
}

/* NAME:    lub_test_seq_end
   PURPOSE: Ends current test sequence
   ARGS:    none
   RETURN:  none
*/
void 
lub_test_seq_end(void)
{
    lub_test_seq_log(LUB_TEST_NORMAL, 
                     "----------------------------------------"
                     "--------------------");
}

/* NAME:    lub_test_seq_log
   PURPOSE: Log output to file and/or stdout, verbosity-filtered and
            formatted as a "sequence-level" message
   ARGS:    level - priority of this message.
                    Will be logged only if priority equals or exceeds
                    the current lub_test_verbosity_t level.
            format, args - printf-style format and parameters
   RETURN:  none
*/
void
lub_test_seq_log(lub_test_verbosity_t level, const char *format, ...)
{
    va_list args;
    char string[160];

    /* Turn format,args into a string */
    va_start(args, format);
    vsprintf(string, format, args);
    va_end(args);

    unitTestLog(level, "%03d     :        %s", seqNum, string);
}

/*******************************************************
* test level functions
********************************************************/

/* NAME:    lub_test_check
   PURPOSE: True/False test of an expression.  If True, test passes.
   ARGS:    expr - expression to evaluate.
            format, args - Printf-style format/parameters describing test.  
   RETURN:  Status code - PASS or FAIL
*/
lub_test_status_t
lub_test_check(bool_t expr, const char *testName, ...)
{
    va_list args;
    char testDescr[80];
    lub_test_status_t testStatus;
    char result[5];
    lub_test_verbosity_t verb;

    /* evaluate expression */
    testStatus = expr ? LUB_TEST_PASS : LUB_TEST_FAIL;

    /* extract description as a string */
    va_start(args, testName);
    vsprintf(testDescr, testName, args);
    va_end(args);

    /* log results */
    if (testStatus == LUB_TEST_PASS) 
    {
        verb=LUB_TEST_NORMAL;
        sprintf(result, "pass");
    } 
    else 
    {
        verb=LUB_TEST_TERSE;
        sprintf(result, "FAIL");
    }
    testLogNoIndent(verb, "[%s] %s", result, testDescr);

    return checkStatus(testStatus);
}

/* NAME:    lub_test_check_int
   PURPOSE: Checks whether an integer equals its expected value.
   ARGS:    expect - expected value.
            actual - value being checked.
            format, args - Printf-style format/parameters describing test.
   RETURN:  Status code - PASS or FAIL
*/
lub_test_status_t
lub_test_check_int(int expect, int actual, const char *testName, ... )
{
    va_list args;
    char testDescr[80];
    lub_test_status_t testStatus;
    char result[5];
    char eqne[3];
    lub_test_verbosity_t verb;

    /* evaluate expression */
    testStatus = (expect == actual) ? LUB_TEST_PASS : LUB_TEST_FAIL;

    /* extract description as a string */
    va_start(args, testName);
    vsprintf(testDescr, testName, args);
    va_end(args);

    /* log results */
    if (testStatus == LUB_TEST_PASS) 
    {
        sprintf(result, "pass");
        sprintf(eqne, "==");
        verb = LUB_TEST_NORMAL;
    } 
    else 
    {
        sprintf(result, "FAIL");
        sprintf(eqne, "!=");
        verb = LUB_TEST_TERSE;
    }
    testLogNoIndent(verb, "[%s] (%d%s%d) %s", 
                    result, actual, eqne, expect, testDescr);

    return checkStatus(testStatus);
}

/* NAME:    lub_test_check_float
   PURPOSE: Checks whether a float is within min/max limits.
   ARGS:    min    - minimum acceptable value.
            max    - maximum acceptable value.
            actual - value being checked.
            format, args - Printf-style format/parameters describing test.
   RETURN:  Status code - PASS or FAIL
*/
lub_test_status_t 
lub_test_check_float(double min, double max, double actual, 
                 const char *testName, ...)
{
    va_list args;
    char testDescr[80];
    lub_test_status_t testStatus;
    char result[5];
    char gteq[4], lteq[4];

    /* evaluate expression */
    testStatus = ((actual >= min) && (actual <= max)) ? LUB_TEST_PASS : LUB_TEST_FAIL;

    /* extract description as a string */
    va_start(args, testName);
    vsprintf(testDescr, testName, args);
    va_end(args);

    /* log results */
    if (testStatus == LUB_TEST_PASS) 
    {
        sprintf(result, "pass");
        sprintf(gteq, " <=");
        sprintf(lteq, " <=");
    }
    else 
    {
        sprintf(result, "FAIL");
        if (actual < min) 
        {
            sprintf(gteq, "!<=");
            sprintf(lteq, " <=");
        } 
        else 
        {
            sprintf(gteq, " <=");
            sprintf(lteq, "!<=");
        }
    }

    testLogNoIndent(LUB_TEST_NORMAL, "[%s] (%8f%s%8f%s%8f) %s", 
                    result, min, gteq, actual, lteq, max, testDescr);

    return checkStatus(testStatus);
}

/* NAME:    lub_test_log
   PURPOSE: Log output to file and/or stdout, verbosity-filtered and
            formatted as a "test-level" message
   ARGS:    level - priority of this message.
                    Will be logged only if priority equals or exceeds
                    the current lub_test_verbosity_t level.
            format, args - printf-style format and parameters
   RETURN:  none
*/
void
lub_test_log(lub_test_verbosity_t level, const char *format, ...)
{
    va_list args;
    char string[160];

    /* Turn format,args into a string */
    va_start(args, format);
    vsprintf(string, format, args);
    va_end(args);

    unitTestLog(level, "%03d-%04d:        %s", seqNum, testNum, string);
}

/* NAME:    testLogNoIndent
   PURPOSE: Log output to file and/or stdout, verbosity-filtered and
            formatted as a "test-level" message.  
            This "internal" version does not indent, so that the
            pass/fail column can be printed in the proper place.
   ARGS:    level - priority of this message.
                    Will be logged only if priority equals or exceeds
                    the current lub_test_verbosity_t level.
            format, args - printf-style format and parameters
   RETURN:  none
*/
static void testLogNoIndent(lub_test_verbosity_t level, const char *format, ...)
{
    va_list args;
    char string[160];

    /* Turn format,args into a string */
    va_start(args, format);
    vsprintf(string, format, args);
    va_end(args);

    unitTestLog(level, "%03d-%04d: %s", seqNum, testNum, string);
}