/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the  "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id$
 */

/*
 *
 * Reporter.java
 *
 */
package org.apache.qetest;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * Class defining how a test can report results including convenience methods.
 * <p>Tests generally interact with a Reporter, which turns around to call
 * a Logger to actually store the results.  The Reporter serves as a
 * single funnel for all results, hiding both the details and number of
 * actual loggers that might currently be turned on (file, screen, network,
 * etc.) from the test that created us.</p>
 * <p>Note that Reporter adds numerous convenience methods that, while they
 * are not strictly necessary to express a test's results, make coding
 * tests much easier.  Reporter is designed to be subclassed for your
 * particular application; in general you only need to provide setup mechanisims
 * specific to your testing/product environment.</p>
 * @todo all methods should check that available loggers are OK
 * @todo explain better how results are rolled up and calculated
 * @author Shane_Curcuru@lotus.com
 * @author Jo_Grant@lotus.com
 * @version $Id$
 */
public class Reporter implements Logger
{

    /**
     * Parameter: (optional) Name of results summary file.
     * <p>This is a custom parameter optionally used in writeResultsStatus.</p>
     */
    public static final String OPT_SUMMARYFILE = "summaryFile";


    /**
     * Constructor calls initialize(p).
     * @param p Properties block to initialize us with.
     */
    public Reporter(Properties p)
    {
        ready = initialize(p);
    }

    /** If we're ready to start outputting yet. */
    protected boolean ready = false;

    //-----------------------------------------------------
    //-------- Implement Logger Control and utility routines --------
    //-----------------------------------------------------

    /**
     * Return a description of what this Logger/Reporter does.
     * @author Shane_Curcuru@lotus.com
     * @return description of how this Logger outputs results, OR
     * how this Reporter uses Loggers, etc..
     */
    public String getDescription()
    {
        return "Reporter: default reporter implementation";
    }

    /**
     * Returns information about the Property name=value pairs that
     * are understood by this Logger/Reporter.
     * @author Shane_Curcuru@lotus.com
     * @return same as {@link java.applet.Applet.getParameterInfo}.
     */
    public String[][] getParameterInfo()
    {

        String pinfo[][] =
        {
            { OPT_LOGGERS, "String", "FQCN of Loggers to add" },
            { OPT_LOGFILE, "String",
              "Name of file to use for file-based Logger output" },
            { OPT_LOGGINGLEVEL, "int",
              "to setLoggingLevel() to control amount of output" },
            { OPT_PERFLOGGING, "boolean",
              "if we should log performance data as well" },
            { OPT_INDENT, "int",
              "number of spaces to indent for supporting Loggers" },
            { OPT_DEBUG, "boolean", "generic debugging flag" }
        };

        return pinfo;
    }

    /**
     * Accessor methods for a properties block.
     * @return our Properties block.
     * @todo should this clone first?
     */
    public Properties getProperties()
    {
        return reporterProps;
    }

    /**
     * Accessor methods for a properties block.
     * Always having a Properties block allows users to pass common
     * options to a Logger/Reporter without having to know the specific
     * 'properties' on the object.
     * <p>Much like in Applets, users can call getParameterInfo() to
     * find out what kind of properties are available.  Callers more
     * commonly simply call initialize(p) instead of setProperties(p)</p>
     * @author Shane_Curcuru@lotus.com
     * @param p Properties to set (should be cloned).
     */
    public void setProperties(Properties p)
    {
        if (p != null)
            reporterProps = (Properties) p.clone();
    }

    /**
     * Call once to initialize this Logger/Reporter from Properties.
     * <p>Simple hook to allow Logger/Reporters with special output
     * items to initialize themselves.</p>
     *
     * @author Shane_Curcuru@lotus.com
     * @param p Properties block to initialize from.
     * @param status, true if OK, false if an error occoured.
     */
    public boolean initialize(Properties p)
    {

        setProperties(p);

        String dbg = reporterProps.getProperty(OPT_DEBUG);

        if ((dbg != null) && dbg.equalsIgnoreCase("true"))
        {
            setDebug(true);
        }

        String perf = reporterProps.getProperty(OPT_PERFLOGGING);

        if ((perf != null) && perf.equalsIgnoreCase("true"))
        {
            setPerfLogging(true);
        }

        // int values need to be parsed
        String logLvl = reporterProps.getProperty(OPT_LOGGINGLEVEL);

        if (logLvl != null)
        {
            try
            {
                setLoggingLevel(Integer.parseInt(logLvl));
            }
            catch (NumberFormatException numEx)
            { /* no-op */
            }
        }

        // Add however many loggers are askedfor
        boolean b = true;
        StringTokenizer st =
            new StringTokenizer(reporterProps.getProperty(OPT_LOGGERS),
                                LOGGER_SEPARATOR);
        int i;

        for (i = 0; st.hasMoreTokens(); i++)
        {
            String temp = st.nextToken();

            if ((temp != null) && (temp.length() > 1))
            {
                b &= addLogger(temp, reporterProps);
            }
        }

        return true;
    }

    /**
     * Is this Logger/Reporter ready to log results?
     * @author Shane_Curcuru@lotus.com
     * @return status - true if it's ready to report, false otherwise
     * @todo should we check our contained Loggers for their status?
     */
    public boolean isReady()
    {
        return ready;
    }

    /**
     * Flush this Logger/Reporter - should ensure all output is flushed.
     * Note that the flush operation is not necessarily pertinent to
     * all types of Logger/Reporter - console-type Loggers no-op this.
     * @author Shane_Curcuru@lotus.com
     */
    public void flush()
    {

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].flush();
        }
    }

    /**
     * Close this Logger/Reporter - should include closing any OutputStreams, etc.
     * Logger/Reporters should return isReady() = false after closing.
     * @author Shane_Curcuru@lotus.com
     */
    public void close()
    {

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].close();
        }
    }

    /**
     * Generic properties for this Reporter.
     * <p>Use a Properties block to make it easier to add new features
     * and to be able to pass data to our loggers.  Any properties that
     * we recognize will be set here, and the entire block will be passed
     * to any loggers that we control.</p>
     */
    protected Properties reporterProps = new Properties();

    /**
     * This determines the amount of data actually logged out to results.
     * <p>Setting this higher will result in more data being logged out.
     * Values range from Reporter.CRITICALMSG (0) to TRACEMSG (60).
     * For non-performance-critical testing, you may wish to set this high,
     * so all data gets logged, and then use reporting tools on the test output
     * to filter for human use (since the appropriate level is stored with
     * every logMsg() call)</p>
     * @see #logMsg(int, java.lang.String)
     */
    protected int loggingLevel = DEFAULT_LOGGINGLEVEL;

    /**
     * Marker that a testcase is currently running.
     * <p>NEEDSWORK: should do a better job of reporting results in cases
     * where users might not call testCaseInit/testCaseClose in non-nested pairs.</p>
     */
    protected boolean duringTestCase = false;

    /**
     * Flag if we should force loggers closed upon testFileClose.
     * <p>Default: true.  Standalone tests can leave this alone.
     * Test Harnesses may want to reset this so they can have multiple
     * file results in one actual output 'file' for file-based loggers.</p>
     */
    protected boolean closeOnFileClose = true;

    /**
     * Accessor method for closeOnFileClose.  
     *
     * @return our value for closeOnFileClose
     */
    public boolean getCloseOnFileClose()
    {
        return closeOnFileClose;
    }

    /**
     * Accessor method for closeOnFileClose.  
     *
     * @param b value to set for closeOnFileClose
     */
    public void setCloseOnFileClose(boolean b)
    {
        closeOnFileClose = b;
    }

    //-----------------------------------------------------
    //-------- Test results computation members and methods --------
    //-----------------------------------------------------

    /** Name of the current test. */
    protected String testName;

    /** Description of the current test. */
    protected String testComment;

    /** Number of current case within a test, usually automatically calculated. */
    protected int caseNum;

    /** Description of current case within a test. */
    protected String caseComment;

    /** Overall test result of current test, automatically calculated. */
    protected int testResult;

    /** Overall test result of current testcase, automatically calculated. */
    protected int caseResult;

    /**
     * Counters for overall number of results - passes, fails, etc.
     * @todo update this if we use TestResult objects
     */
    protected static final int FILES = 0;

    /** NEEDSDOC Field CASES          */
    protected static final int CASES = 1;

    /** NEEDSDOC Field CHECKS          */
    protected static final int CHECKS = 2;

    /** NEEDSDOC Field MAX_COUNTERS          */
    protected static final int MAX_COUNTERS = CHECKS + 1;

    /**
     * Counters for overall number of results - passes, fails, etc.
     * @todo update this if we use TestResult objects
     */
    protected int[] incpCount = new int[MAX_COUNTERS];

    /** NEEDSDOC Field passCount          */
    protected int[] passCount = new int[MAX_COUNTERS];

    /** NEEDSDOC Field ambgCount          */
    protected int[] ambgCount = new int[MAX_COUNTERS];

    /** NEEDSDOC Field failCount          */
    protected int[] failCount = new int[MAX_COUNTERS];

    /** NEEDSDOC Field errrCount          */
    protected int[] errrCount = new int[MAX_COUNTERS];
    

    //-----------------------------------------------------
    //-------- Composite Pattern Variables And Methods --------
    //-----------------------------------------------------

    /**
     * Optimization: max number of loggers, stored in an array.
     * <p>This is a design decision: normally, you might use a ConsoleReporter,
     * some sort of file-based one, and maybe a network-based one.</p>
     */
    protected int MAX_LOGGERS = 3;

    /**
     * Array of loggers to whom we pass results.
     * <p>Store our loggers in an array for optimization, since we want
     * logging calls to take as little time as possible.</p>
     */
    protected Logger[] loggers = new Logger[MAX_LOGGERS];

    /** NEEDSDOC Field numLoggers          */
    protected int numLoggers = 0;

    /**
     * Add a new Logger to our array, optionally initializing it with Properties.
     * <p>Store our Loggers in an array for optimization, since we want
     * logging calls to take as little time as possible.</p>
     * @todo enable users to add more than MAX_LOGGERS
     * @author Gang Of Four
     * @param rName fully qualified class name of Logger to add.
     * @param p (optional) Properties block to initialize the Logger with.
     * @return status - true if successful, false otherwise.
     */
    public boolean addLogger(String rName, Properties p)
    {

        if ((rName == null) || (rName.length() < 1))
            return false;

        debugPrintln("addLogger(" + numLoggers + ", " + rName + " ...)");

        if ((numLoggers + 1) > loggers.length)
        {

            // @todo enable users to add more than MAX_LOGGERS
            return false;
        }

        // Attempt to add Logger to our list
        Class rClass;
        Constructor rCtor;

        try
        {
            rClass = Class.forName(rName);

            debugPrintln("rClass is " + rClass.toString());

            if (p == null)

            // @todo should somehow pass along our own props as well
            // Need to ensure Reporter and callers of this method always 
            //  coordinate the initialization of the Loggers we hold
            {
                loggers[numLoggers] = (Logger) rClass.newInstance();
            }
            else
            {
                Class[] parameterTypes = new Class[1];

                parameterTypes[0] = java.util.Properties.class;
                rCtor = rClass.getConstructor(parameterTypes);

                Object[] initArgs = new Object[1];

                initArgs[0] = (Object) p;
                loggers[numLoggers] = (Logger) rCtor.newInstance(initArgs);
            }
        }
        catch (Exception e)
        {

            // @todo should we inform user why it failed?
            // Note: the logMsg may fail since we might not have any reporters at this point!
            debugPrintln("addLogger exception: " + e.toString());
            logCriticalMsg("addLogger exception: " + e.toString());
            logThrowable(CRITICALMSG, e, "addLogger exception:");

            return false;
        }

        // Increment counter for later use
        numLoggers++;

        return true;
    }

    /**
     * Return an Hashtable of all active Loggers.
     * @todo revisit; perhaps use a Vector
     * @reurns Hash of all active Loggers; null if none
     *
     * NEEDSDOC ($objectName$) @return
     */
    public Hashtable getLoggers()
    {

        // Optimization
        if (numLoggers == 0)
            return (null);

        Hashtable temp = new Hashtable();

        for (int i = 0; i < numLoggers; i++)
        {
            temp.put(loggers[i].getClass().getName(), loggers[i]);
        }

        return temp;
    }

    /**
     * Add the default Logger to this Reporter, whatever it is.
     * <p>Only adds the Logger if numLoggers <= 0; if the user has already
     * setup another Logger, this is a no-op (for the testwriter who doesn't
     * want the performance hit or annoyance of having Console output)</p>
     * @author Gang Of Four
     * @return status - true if successful, false otherwise.
     */
    public boolean addDefaultLogger()
    {

        // Optimization - return true, since they already have a logger
        if (numLoggers > 0)
            return true;

        return addLogger(DEFAULT_LOGGER, reporterProps);
    }

    //-----------------------------------------------------
    //-------- Testfile / Testcase start and stop routines --------
    //-----------------------------------------------------

    /**
     * Call once to initialize your Loggers for your test file.
     * Also resets test name, result, case results, etc.
     * <p>Currently, you must init/close your test file before init/closing
     * any test cases.  No checking is currently done to ensure that
     * mismatched test files are not nested.  This is an area that needs
     * design decisions and some work eventually to be a really clean design.</p>
     * <p>Not only do nested testfiles/testcases have implications for good
     * testing practices, they may also have implications for various Loggers,
     * especially XML or other ones with an implicit hierarcy in the reports.</p>
     * @author Shane_Curcuru@lotus.com
     * @param name file name or tag specifying the test.
     * @param comment comment about the test.
     */
    public void testFileInit(String name, String comment)
    {

        testName = name;
        testComment = comment;
        testResult = DEFAULT_RESULT;
        caseNum = 0;
        caseComment = null;
        caseResult = DEFAULT_RESULT;
        duringTestCase = false;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].testFileInit(testName, testComment);
        }

        // Log out time whole test script starts
        // Note there is a slight delay while logPerfMsg calls all reporters
        long t = System.currentTimeMillis();

        logPerfMsg(TEST_START, t, testName);
    }

    /**
     * Call once to close out your test and summate results.
     * <p>will close an open testCase before closing the file.  May also
     * force all Loggers closed if getCloseOnFileClose() (which may imply
     * that no more output will be logged to file-based reporters)</p>
     * @author Shane_Curcuru@lotus.com
     * @todo make this settable as to how/where the resultsCounters get output
     */
    public void testFileClose()
    {

        // Cache the time whole test script ends
        long t = System.currentTimeMillis();

        if (duringTestCase)
        {

            // Either user messed up (forgot to call testCaseClose) or something went wrong
            logErrorMsg("WARNING! testFileClose when duringTestCase=true!");

            // Force call to testCaseClose()
            testCaseClose();
        }

        // Actually log the time the test script ends after closing any potentially open testcases
        logPerfMsg(TEST_STOP, t, testName);

        // Increment our results counters 
        incrementResultCounter(FILES, testResult);

        // Print out an overall count of results by type
        // @todo make this settable as to how/where the resultsCounters get output
        logResultsCounters();

        // end this testfile - finish up any reporting we need to
        for (int i = 0; i < numLoggers; i++)
        {

            // Log we're done and then flush
            loggers[i].testFileClose(testComment, resultToString(testResult));
            loggers[i].flush();

            // Only close each reporter if asked to; this implies we're done
            //  and can't perform any more logging ourselves (or our reporters)
            if (getCloseOnFileClose())
            {
                loggers[i].close();
            }
        }

        // Note: explicitly leave testResult, caseResult, etc. set for debugging
        //       purposes or for use by external test harnesses
    }

    /**
     * Implement Logger-only method.
     * <p>Here, a Reporter is simply acting as a logger: so don't
     * summate any results, do performance measuring, or anything
     * else, just pass the call through to our Loggers.
     * @param msg message to log out
     * @param result result of testfile
     */
    public void testFileClose(String msg, String result)
    {

        if (duringTestCase)
        {

            // Either user messed up (forgot to call testCaseClose) or something went wrong
            logErrorMsg("WARNING! testFileClose when duringTestCase=true!");

            // Force call to testCaseClose()
            testCaseClose();
        }

        // end this testfile - finish up any reporting we need to
        for (int i = 0; i < numLoggers; i++)
        {

            // Log we're done and then flush
            loggers[i].testFileClose(testComment, resultToString(testResult));
            loggers[i].flush();

            // Only close each reporter if asked to; this implies we're done
            //  and can't perform any more logging ourselves (or our reporters)
            if (getCloseOnFileClose())
            {
                loggers[i].close();
            }
        }
    }

    /**
     * Call once to start each test case; logs out testcase number and your comment.
     * <p>Testcase numbers are calculated as integers incrementing from 1.  Will
     * also close any previously init'd but not closed testcase.</p>
     * @author Shane_Curcuru@lotus.com
     * @todo investigate tieing this to the actual testCase methodnames,
     * instead of blindly incrementing the counter
     * @param comment short description of this test case's objective.
     */
    public void testCaseInit(String comment)
    {

        if (duringTestCase)
        {

            // Either user messed up (forgot to call testCaseClose) or something went wrong
            logErrorMsg("WARNING! testCaseInit when duringTestCase=true!");

            // Force call to testCaseClose()
            testCaseClose();
        }

        caseNum++;

        caseComment = comment;
        caseResult = DEFAULT_RESULT;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].testCaseInit(String.valueOf(caseNum) + " "
                                    + caseComment);
        }

        duringTestCase = true;

        // Note there is a slight delay while logPerfMsg calls all reporters
        long t = System.currentTimeMillis();

        logPerfMsg(CASE_START, t, caseComment);
    }

    /**
     * Call once to end each test case and sub-summate results.
     * @author Shane_Curcuru@lotus.com
     */
    public void testCaseClose()
    {

        long t = System.currentTimeMillis();

        logPerfMsg(CASE_STOP, t, caseComment);

        if (!duringTestCase)
        {
            logErrorMsg("WARNING! testCaseClose when duringTestCase=false!");

            // Force call to testCaseInit()
            // NEEDSWORK: should we really do this?  This ensures any results
            //            are well-formed, however a user might not expect this.
            testCaseInit("WARNING! testCaseClose when duringTestCase=false!");
        }

        duringTestCase = false;
        testResult = java.lang.Math.max(testResult, caseResult);

        // Increment our results counters 
        incrementResultCounter(CASES, caseResult);

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].testCaseClose(
                String.valueOf(caseNum) + " " + caseComment,
                resultToString(caseResult));
        }
    }

    /**
     * Implement Logger-only method.
     * <p>Here, a Reporter is simply acting as a logger: so don't
     * summate any results, do performance measuring, or anything
     * else, just pass the call through to our Loggers.
     * @param msg message of name of test case to log out
     * @param result result of testfile
     */
    public void testCaseClose(String msg, String result)
    {

        if (!duringTestCase)
        {
            logErrorMsg("WARNING! testCaseClose when duringTestCase=false!");

            // Force call to testCaseInit()
            // NEEDSWORK: should we really do this?  This ensures any results
            //            are well-formed, however a user might not expect this.
            testCaseInit("WARNING! testCaseClose when duringTestCase=false!");
        }

        duringTestCase = false;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].testCaseClose(
                String.valueOf(caseNum) + " " + caseComment,
                resultToString(caseResult));
        }
    }

    /**
     * Calls back into a Test to run test cases in order.
     * <p>Use reflection to call back and execute each testCaseXX method
     * in the calling test in order, catching exceptions along the way.</p>
     * //@todo rename to 'executeTestCases' or something
     * //@todo implement options: either an inclusion or exclusion list
     * @author Shane Curcuru
     * @param testObject the test object itself.
     * @param numTestCases number of consecutively numbered test cases to execute.
     * @param options (future use: options to pass to testcases)
     * @return status, true if OK, false if big bad error occoured
     */
    public boolean executeTests(Test testObject, int numTestCases,
                                Object options)
    {

        // Flag denoting if we've had any errors
        boolean gotException = false;

        // Declare all needed java variables
        String tmpErrString = "executeTests: no errors yet";
        Object noArgs[] = new Object[0];  // use options instead
        Class noParams[] = new Class[0];
        Method currTestCase;
        Class testClass;

        // Get class reference for the test applet itself
        testClass = testObject.getClass();

        logTraceMsg("executeTests: running " + numTestCases + " tests now.");

        for (int tcNum = 1; tcNum <= numTestCases; tcNum++)
        {
            try
            {  // get a reference to the next test case that we'll be calling
                tmpErrString = "executeTests: No such method: testCase"
                               + tcNum + "()";
                currTestCase = testClass.getMethod("testCase" + tcNum,
                                                   noParams);

                // Now directly invoke that test case
                tmpErrString =
                    "executeTests: Method threw an exception: testCase"
                    + tcNum + "(): ";

                logTraceMsg("executeTests: invoking testCase" + tcNum
                            + " now.");
                currTestCase.invoke(testObject, noArgs);
            }
            catch (InvocationTargetException ite)
            {
                // Catch any error, log it as an error, and allow next test case to run
                gotException = true;
                testResult = java.lang.Math.max(ERRR_RESULT, testResult);
                tmpErrString += ite.toString();
                logErrorMsg(tmpErrString);

                // Grab the contained error, log it if available 
                java.lang.Throwable containedThrowable =
                    ite.getTargetException();
                if (containedThrowable != null)
                {
                    logThrowable(ERRORMSG, containedThrowable, tmpErrString + "(1)");
                }
                logThrowable(ERRORMSG, ite, tmpErrString + "(2)");
            }  // end of catch
            catch (Throwable t)
            {
                // Catch any error, log it as an error, and allow next test case to run
                gotException = true;
                testResult = java.lang.Math.max(ERRR_RESULT, testResult);
                tmpErrString += t.toString();
                logErrorMsg(tmpErrString);
                logThrowable(ERRORMSG, t, tmpErrString);
            }  // end of catch
        }  // end of for

        // Convenience functionality: remind user if they appear to 
        //  have set numTestCases too low 
        try
        {  
            // Get a reference to the *next* test case after numTestCases
            int moreTestCase = numTestCases + 1;
            currTestCase = testClass.getMethod("testCase" + moreTestCase, noParams);

            // If we get here, we found another testCase - warn the user
            logWarningMsg("executeTests: extra testCase"+ moreTestCase
                          + " found, perhaps numTestCases is too low?");
        }
        catch (Throwable t)
        {
            // Ignore errors: we don't care, since they didn't 
            //  ask us to look for this method anyway
        }

        // Return true only if everything passed
        if (testResult == PASS_RESULT)
            return true;
        else
            return false;
    }  // end of executeTests

    //-----------------------------------------------------
    //-------- Test results logging routines --------
    //-----------------------------------------------------

    /**
     * Accessor for loggingLevel, determines what level of log*() calls get output.
     * @return loggingLevel, as an int.
     */
    public int getLoggingLevel()
    {
        return loggingLevel;
    }

    /**
     * Accessor for loggingLevel, determines what level of log*() calls get output.
     * @param setLL loggingLevel; normalized to be between CRITICALMSG and TRACEMSG.
     */
    public void setLoggingLevel(int setLL)
    {

        if (setLL < CRITICALMSG)
        {
            loggingLevel = CRITICALMSG;
        }
        else if (setLL > TRACEMSG)
        {
            loggingLevel = TRACEMSG;
        }
        else
        {
            loggingLevel = setLL;
        }
    }

    /**
     * Report a comment to result file with specified severity.
     * <p>Works in conjunction with {@link #loggingLevel };
     * only outputs messages that are more severe (i.e. lower)
     * than the current logging level.</p>
     * <p>Note that some Loggers may limit the comment string,
     * either in overall length or by stripping any linefeeds, etc.
     * This is to allow for optimization of file or database-type
     * reporters with fixed fields.  Users who need to log out
     * special string data should use logArbitrary() instead.</p>
     * <p>Remember, use {@link #check(String, String, String)
     * various check*() methods} to report the actual results
     * of your tests.</p>
     * @author Shane_Curcuru@lotus.com
     * @param level severity of message.
     * @param msg comment to log out.
     * @see #loggingLevel
     */
    public void logMsg(int level, String msg)
    {

        if (level > loggingLevel)
            return;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logMsg(level, msg);
        }
    }

    /**
     * Report an arbitrary String to result file with specified severity.
     * Log out the String provided exactly as-is.
     * @author Shane_Curcuru@lotus.com
     * @param level severity or class of message.
     * @param msg arbitrary String to log out.
     */
    public void logArbitrary(int level, String msg)
    {

        if (level > loggingLevel)
            return;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logArbitrary(level, msg);
        }
    }

    /**
     * Logs out statistics to result file with specified severity.
     * <p>This is a general-purpose way to log out numeric statistics.  We accept
     * both a long and a double to allow users to save whatever kind of numbers
     * they need to, with the simplest API.  The actual meanings of the numbers
     * are dependent on the implementer.</p>
     * @author Shane_Curcuru@lotus.com
     * @param level severity of message.
     * @param lVal statistic in long format.
     * @param dVal statistic in doubleformat.
     * @param msg comment to log out.
     */
    public void logStatistic(int level, long lVal, double dVal, String msg)
    {

        if (level > loggingLevel)
            return;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logStatistic(level, lVal, dVal, msg);
        }
    }

    /**
     * Logs out a element to results with specified severity.
     * This method is primarily for reporters that output to fixed
     * structures, like files, XML data, or databases.
     * @author Shane_Curcuru@lotus.com
     * @param level severity of message.
     * @param element name of enclosing element
     * @param attrs hash of name=value attributes
     * @param msg Object to log out; up to reporters to handle
     * processing of this; usually logs just .toString().
     */
    public void logElement(int level, String element, Hashtable attrs,
                           Object msg)
    {

        if (level > loggingLevel)
            return;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logElement(level, element, attrs, msg);
        }
    }

    /**
     * Logs out Throwable.toString() and a stack trace of the 
     * Throwable with the specified severity.
     * <p>Works in conjuntion with {@link #setLoggingLevel(int)}; 
     * only outputs messages that are more severe than the current 
     * logging level.</p>
     * <p>This uses logArbitrary to log out your msg - message, 
     * a newline, throwable.toString(), a newline,
     * and then throwable.printStackTrace().</p>
     * <p>Note that this does not imply a failure or problem in 
     * a test in any way: many tests may want to verify that 
     * certain exceptions are thrown, etc.</p>
     * @author Shane_Curcuru@lotus.com
     * @param level severity of message.
     * @param throwable throwable/exception to log out.
     * @param msg description of the throwable.
     */
    public void logThrowable(int level, Throwable throwable, String msg)
    {

        if (level > loggingLevel)
            return;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logThrowable(level, throwable, msg);
        }
    }

    /**
     * Logs out contents of a Hashtable with specified severity.
     * <p>Works in conjuntion with setLoggingLevel(int); only outputs messages that
     * are more severe than the current logging level.</p>
     * <p>Loggers should store or log the full contents of the hashtable.</p>
     * @author Shane_Curcuru@lotus.com
     * @param level severity of message.
     * @param hash Hashtable to log the contents of.
     * @param msg description of the Hashtable.
     */
    public void logHashtable(int level, Hashtable hash, String msg)
    {
        if (level > loggingLevel)
          return;
        
        // Don't log anyway if level is 10 or less.
        //@todo revisit this decision: I don't like having special
        //  rules like this to exclude output.  On the other hand, 
        //  if the user set loggingLevel this low, they really don't 
        //  want much output coming out, and hashtables are big
        if (loggingLevel <= 10)
            return;

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logHashtable(level, hash, msg);
        }
    }

    /**
     * Logs out an critical a comment to results; always printed out.
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void logCriticalMsg(String msg)
    {
        logMsg(CRITICALMSG, msg);
    }

    // There is no logFailsOnlyMsg(String msg) method

    /**
     * Logs out an error a comment to results.
     * <p>Note that subclassed libraries may choose to override to
     * cause a fail to happen along with printing out the message.</p>
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void logErrorMsg(String msg)
    {
        logMsg(ERRORMSG, msg);
    }

    /**
     * Logs out a warning a comment to results.
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void logWarningMsg(String msg)
    {
        logMsg(WARNINGMSG, msg);
    }

    /**
     * Logs out an status a comment to results.
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void logStatusMsg(String msg)
    {
        logMsg(STATUSMSG, msg);
    }

    /**
     * Logs out an informational a comment to results.
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void logInfoMsg(String msg)
    {
        logMsg(INFOMSG, msg);
    }

    /**
     * Logs out an trace a comment to results.
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void logTraceMsg(String msg)
    {
        logMsg(TRACEMSG, msg);
    }

    //-----------------------------------------------------
    //-------- Test results reporting check* routines --------
    //-----------------------------------------------------
    // There is no public void checkIncp(String comment) method

    /* EXPERIMENTAL: have duplicate set of check*() methods 
       that all output some form of ID as well as comment. 
       Leave the non-ID taking forms for both simplicity to the 
       end user who doesn't care about IDs as well as for 
       backwards compatibility.
    */

    /**
     * Writes out a Pass record with comment.
     * @author Shane_Curcuru@lotus.com
     * @param comment comment to log with the pass record.
     */
    public void checkPass(String comment)
    {
        checkPass(comment, null);
    }

    /**
     * Writes out an ambiguous record with comment.
     * @author Shane_Curcuru@lotus.com
     * @param comment to log with the ambg record.
     */
    public void checkAmbiguous(String comment)
    {
        checkAmbiguous(comment, null);
    }

    /**
     * Writes out a Fail record with comment.
     * @author Shane_Curcuru@lotus.com
     * @param comment comment to log with the fail record.
     */
    public void checkFail(String comment)
    {
        checkFail(comment, null);
    }
    

    /**
     * Writes out an Error record with comment.
     * @author Shane_Curcuru@lotus.com
     * @param comment comment to log with the error record.
     */
    public void checkErr(String comment)
    {
        checkErr(comment, null);
    }

    /**
     * Writes out a Pass record with comment.
     * A Pass signifies that an individual test point has completed and has
     * been verified to have behaved correctly.
     * <p>If you need to do your own specific comparisons, you can
     * do them in your code and then just call checkPass or checkFail.</p>
     * <p>Derived classes must implement this to <B>both</B> report the
     * results out appropriately <B>and</B> to summate the results, if needed.</p>
     * <p>Pass results are a low priority, except for INCP (incomplete).  Note
     * that if a test never calls check*(), it will have an incomplete result.</p>
     * @author Shane_Curcuru@lotus.com
     * @param comment to log with the pass record.
     * @param ID token to log with the pass record.
     */
    public void checkPass(String comment, String id)
    {

        // Increment our results counters 
        incrementResultCounter(CHECKS, PASS_RESULT);

        // Special: only report it actually if needed
        if (getLoggingLevel() > FAILSONLY)
        {
            for (int i = 0; i < numLoggers; i++)
            {
                loggers[i].checkPass(comment, id);
            }
        }

        caseResult = java.lang.Math.max(PASS_RESULT, caseResult);
    }

    /**
     * Writes out an ambiguous record with comment.
     * <p>Ambiguous results are neither pass nor fail. Different test
     * libraries may have slightly different reasons for using ambg.</p>
     * <p>Derived classes must implement this to <B>both</B> report the
     * results out appropriately <B>and</B> to summate the results, if needed.</p>
     * <p>Ambg results have a middling priority, and take precedence over incomplete and pass.</p>
     * <p>An Ambiguous result may signify that the test point has completed and either
     * appears to have succeded, or that it has produced a result but there is no known
     * 'gold' result to compare it to.</p>
     * @author Shane_Curcuru@lotus.com
     * @param comment to log with the ambg record.
     * @param ID token to log with the pass record.
     */
    public void checkAmbiguous(String comment, String id)
    {

        // Increment our results counters 
        incrementResultCounter(CHECKS, AMBG_RESULT);

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].checkAmbiguous(comment, id);
        }

        caseResult = java.lang.Math.max(AMBG_RESULT, caseResult);
    }

    /**
     * Writes out a Fail record with comment.
     * <p>If you need to do your own specific comparisons, you can
     * do them in your code and then just call checkPass or checkFail.</p>
     * <p>Derived classes must implement this to <B>both</B> report the
     * results out appropriately <B>and</B> to summate the results, if needed.</p>
     * <p>Fail results have a high priority, and take precedence over incomplete, pass, and ambiguous.</p>
     * <p>A Fail signifies that an individual test point has completed and has
     * been verified to have behaved <B>in</B>correctly.</p>
     * @author Shane_Curcuru@lotus.com
     * @param comment to log with the fail record.
     * @param ID token to log with the pass record.
     */
    public void checkFail(String comment, String id)
    {

        // Increment our results counters 
        incrementResultCounter(CHECKS, FAIL_RESULT);

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].checkFail(comment, id);
        }

        caseResult = java.lang.Math.max(FAIL_RESULT, caseResult);
    }

    /**
     * Writes out an Error record with comment.
     * <p>Derived classes must implement this to <B>both</B> report the
     * results out appropriately <B>and</B> to summate the results, if needed.</p>
     * <p>Error results have the highest priority, and take precedence over
     * all other results.</p>
     * <p>An Error signifies that something unusual has gone wrong with the execution
     * of the test at this point - likely something that will require a human to
     * debug to see what really happened.</p>
     * @author Shane_Curcuru@lotus.com
     * @param comment to log with the error record.
     * @param ID token to log with the pass record.
     */
    public void checkErr(String comment, String id)
    {

        // Increment our results counters 
        incrementResultCounter(CHECKS, ERRR_RESULT);

        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].checkErr(comment, id);
        }

        caseResult = java.lang.Math.max(ERRR_RESULT, caseResult);
    }

    //-----------------------------------------------------
    //-------- Simplified Performance Logging - beyond interface Reporter --------
    //-----------------------------------------------------

    /** NEEDSDOC Field DEFAULT_PERFLOGGING_LEVEL          */
    protected final boolean DEFAULT_PERFLOGGING_LEVEL = false;

    /**
     * This determines if performance information is logged out to results.
     * <p>When true, extra performance records are written out to result files.</p>
     * @see #logPerfMsg(java.lang.String, long, java.lang.String)
     */
    protected boolean perfLogging = DEFAULT_PERFLOGGING_LEVEL;

    /**
     * Accessor for perfLogging, determines if we log performance info.
     * @todo add PerfLogging to Reporter interface
     * @return Whether or not we log performance info.
     */
    public boolean getPerfLogging()
    {
        return (perfLogging);
    }

    /**
     * Accessor for perfLogging, determines if we log performance info.
     * @param Whether or not we log performance info.
     *
     * NEEDSDOC @param setPL
     */
    public void setPerfLogging(boolean setPL)
    {
        perfLogging = setPL;
    }

    /**
     * Constants used to mark performance records in output.
     */

    // Note: string representations are explicitly set to all be 
    //       4 characters long to make it simpler to parse results
    public static final String TEST_START = "TSrt";

    /** NEEDSDOC Field TEST_STOP          */
    public static final String TEST_STOP = "TStp";

    /** NEEDSDOC Field CASE_START          */
    public static final String CASE_START = "CSrt";

    /** NEEDSDOC Field CASE_STOP          */
    public static final String CASE_STOP = "CStp";

    /** NEEDSDOC Field USER_TIMER          */
    public static final String USER_TIMER = "UTmr";

    /** NEEDSDOC Field USER_TIMESTAMP          */
    public static final String USER_TIMESTAMP = "UTim";

    /** NEEDSDOC Field USER_MEMORY          */
    public static final String USER_MEMORY = "UMem";

    /** NEEDSDOC Field PERF_SEPARATOR          */
    public static final String PERF_SEPARATOR = ";";

    /**
     * Logs out a performance statistic.
     * <p>Only logs times if perfLogging set to true.</p>
     * <p>As an optimization for record-based Loggers, this is a rather simplistic
     * way to log performance info - however it's sufficient for most purposes.</p>
     * @author Frank Bell
     * @param type type of performance statistic.
     * @param data long value of performance statistic.
     * @param msg comment to log out.
     */
    public void logPerfMsg(String type, long data, String msg)
    {

        if (getPerfLogging())
        {
            double dummy = 0;

            for (int i = 0; i < numLoggers; i++)
            {

                // NEEDSWORK: simply put it at the current loggingLevel we have set
                //            Is there a better way to mesh performance output with the rest?
                loggers[i].logStatistic(loggingLevel, data, dummy,
                                        type + PERF_SEPARATOR + msg);
            }
        }
    }

    /**
     * Captures current time in milliseconds, only if perfLogging.
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    protected Hashtable perfTimers = new Hashtable();

    /**
     * NEEDSDOC Method startTimer 
     *
     *
     * NEEDSDOC @param msg
     */
    public void startTimer(String msg)
    {

        // Note optimization: only capture times if perfLogging
        if ((perfLogging) && (msg != null))
        {
            perfTimers.put(msg, new Long(System.currentTimeMillis()));
        }
    }

    /**
     * Captures current time in milliseconds and logs out difference.
     * Will only log times if perfLogging set to true.
     * <p>Only logs time if it finds a corresponding msg entry that was startTimer'd.</p>
     * @author Shane_Curcuru@lotus.com
     * @param msg comment to log out.
     */
    public void stopTimer(String msg)
    {

        // Capture time immediately to reduce latency
        long stopTime = System.currentTimeMillis();

        // Note optimization: only use times if perfLogging
        if ((perfLogging) && (msg != null))
        {
            Long startTime = (Long) perfTimers.get(msg);

            logPerfMsg(USER_TIMER, (stopTime - startTime.longValue()), msg);
            perfTimers.remove(msg);
        }
    }

    /**
     * Accessor for currently running test case number, read-only.
     * @return current test case number.
     */
    public int getCurrentCaseNum()
    {
        return caseNum;
    }

    /**
     * Accessor for current test case's result, read-only.
     * @return current test case result.
     */
    public int getCurrentCaseResult()
    {
        return caseResult;
    }

    /**
     * Accessor for current test case's description, read-only.
     * @return current test case result.
     */
    public String getCurrentCaseComment()
    {
        return caseComment;
    }

    /**
     * Accessor for overall test file result, read-only.
     * @return test file's overall result.
     */
    public int getCurrentFileResult()
    {
        return testResult;
    }

    /**
     * Utility method to log out overall result counters.  
     *
     * @param count number of this kind of result
     * @param desc description of this kind of result
     */
    protected void logResultsCounter(int count, String desc)
    {

        // Optimization: Only log the kinds of results we have
        if (count > 0)
            logStatistic(loggingLevel, count, 0, desc);
    }

    /** Utility method to log out overall result counters. */
    public void logResultsCounters()
    {

        // NEEDSWORK: what's the best format to display this stuff in?
        // NEEDSWORK: what loggingLevel should we use?
        // NEEDSWORK: temporarily skipping the 'files' since 
        //            we only have tests with one file being run
        // logResultsCounter(incpCount[FILES], "incpCount[FILES]");
        logResultsCounter(incpCount[CASES], "incpCount[CASES]");
        logResultsCounter(incpCount[CHECKS], "incpCount[CHECKS]");

        // logResultsCounter(passCount[FILES], "passCount[FILES]");
        logResultsCounter(passCount[CASES], "passCount[CASES]");
        logResultsCounter(passCount[CHECKS], "passCount[CHECKS]");

        // logResultsCounter(ambgCount[FILES], "ambgCount[FILES]");
        logResultsCounter(ambgCount[CASES], "ambgCount[CASES]");
        logResultsCounter(ambgCount[CHECKS], "ambgCount[CHECKS]");

        // logResultsCounter(failCount[FILES], "failCount[FILES]");
        logResultsCounter(failCount[CASES], "failCount[CASES]");
        logResultsCounter(failCount[CHECKS], "failCount[CHECKS]");

        // logResultsCounter(errrCount[FILES], "errrCount[FILES]");
        logResultsCounter(errrCount[CASES], "errrCount[CASES]");
        logResultsCounter(errrCount[CHECKS], "errrCount[CHECKS]");
    }

    /** 
     * Utility method to store overall result counters. 
     *
     * @return a Hashtable of various results items suitable for
     * passing to logElement as attrs
     */
    protected Hashtable createResultsStatusHash()
    {
        Hashtable resHash = new Hashtable();
        if (incpCount[CASES] > 0)
            resHash.put(INCP + "-cases", new Integer(incpCount[CASES]));
        if (incpCount[CHECKS] > 0)
            resHash.put(INCP + "-checks", new Integer(incpCount[CHECKS]));

        if (passCount[CASES] > 0)
            resHash.put(PASS + "-cases", new Integer(passCount[CASES]));
        if (passCount[CHECKS] > 0)
            resHash.put(PASS + "-checks", new Integer(passCount[CHECKS]));

        if (ambgCount[CASES] > 0)
            resHash.put(AMBG + "-cases", new Integer(ambgCount[CASES]));
        if (ambgCount[CHECKS] > 0)
            resHash.put(AMBG + "-checks", new Integer(ambgCount[CHECKS]));

        if (failCount[CASES] > 0)
            resHash.put(FAIL + "-cases", new Integer(failCount[CASES]));
        if (failCount[CHECKS] > 0)
            resHash.put(FAIL + "-checks", new Integer(failCount[CHECKS]));

        if (errrCount[CASES] > 0)
            resHash.put(ERRR + "-cases", new Integer(errrCount[CASES]));
        if (errrCount[CHECKS] > 0)
            resHash.put(ERRR + "-checks", new Integer(errrCount[CHECKS]));
        return resHash;
    }

    /** 
     * Utility method to write out overall result counters. 
     * 
     * <p>This writes out both a testsummary element as well as 
     * writing a separate marker file for the test's currently 
     * rolled-up test results.</p>
     *
     * <p>Note if writeFile is true, we do a bunch of additional 
     * processing, including deleting any potential marker 
     * files, along with creating a new marker file.  This section 
     * of code explicitly does file creation and also includes 
     * some basic XML-isms in it.</p>
     * 
     * <p>Marker files look like: [testStat][testName].xml, where 
     * testStat is the actual current status, like 
     * Pass/Fail/Ambg/Errr/Incp, and testName comes from the 
     * currently executing test; this may be overridden by 
		  * setting OPT_SUMMARYFILE.</p>
     *
     * @param writeFile if we should also write out a separate 
     * Passname/Failname marker file as well
     */
    public void writeResultsStatus(boolean writeFile)
    {
        final String DEFAULT_SUMMARY_NAME = "ResultsSummary.xml";
        Hashtable resultsHash = createResultsStatusHash();
        resultsHash.put("desc", testComment);
        resultsHash.put("testName", testName);
        //@todo the actual path in the property below may not necessarily 
        //  either exist or be the correct location vis-a-vis the file
        //  that we're writing out - but it should be close
        resultsHash.put(OPT_LOGFILE, reporterProps.getProperty(OPT_LOGFILE, DEFAULT_SUMMARY_NAME));
        try
        {
            resultsHash.put("baseref", System.getProperty("user.dir"));
        } 
        catch (Exception e) { /* no-op, ignore */ }

        String elementName = "teststatus";
        String overallResult = resultToString(getCurrentFileResult());
        // Ask each of our loggers to report this
        for (int i = 0; i < numLoggers; i++)
        {
            loggers[i].logElement(CRITICALMSG, elementName, resultsHash, overallResult);
        }

        // Only continue if user asked us to
        if (!writeFile)
            return;

        // Now write an actual file out as a marker for enclosing 
        //  harnesses and build environments

        // Calculate the name relative to any logfile we have
        String logFileBase = null;
        try
        {
            // CanonicalPath gives a better path, especially if 
            //  you mix your path separators up
            logFileBase = (new File(reporterProps.getProperty(OPT_LOGFILE, DEFAULT_SUMMARY_NAME))).getCanonicalPath();
        } 
        catch (IOException ioe)
        {
            logFileBase = (new File(reporterProps.getProperty(OPT_LOGFILE, DEFAULT_SUMMARY_NAME))).getAbsolutePath();
        }
        logFileBase = (new File(logFileBase)).getParent();
		 		 // Either use the testName or an optionally set summary name
		 		 String summaryFileBase = reporterProps.getProperty(OPT_SUMMARYFILE, testName + ".xml");
        final File[] summaryFiles = 
        {
            // Note array is ordered; should be re-designed so this doesn't matter
            // Coordinate PASS name with results.marker in build.xml
            // File name rationale: put Pass/Fail/etc first, so they 
            //  all show up together in dir listing; include 
            //  testName so you know where it came from; make it 
            //  .xml since it is an XML file
            new File(logFileBase, INCP + "-" + summaryFileBase),
            new File(logFileBase, PASS + "-" + summaryFileBase),
            new File(logFileBase, AMBG + "-" + summaryFileBase),
            new File(logFileBase, FAIL + "-" + summaryFileBase),
            new File(logFileBase, ERRR + "-" + summaryFileBase)
        };
        // Clean up any pre-existing files that might be confused 
        //  as markers from this testrun
        for (int i = 0; i < summaryFiles.length; i++)
        {
            if (summaryFiles[i].exists())
                summaryFiles[i].delete();
        }

        File summaryFile = null;
        switch (getCurrentFileResult())
        {
            case INCP_RESULT:
                summaryFile = summaryFiles[0];
                break;
            case PASS_RESULT:
                summaryFile = summaryFiles[1];
                break;
            case AMBG_RESULT:
                summaryFile = summaryFiles[2];
                break;
            case FAIL_RESULT:
                summaryFile = summaryFiles[3];
                break;
            case ERRR_RESULT:
                summaryFile = summaryFiles[4];
                break;
            default:
                // Use error case, this should never happen
                summaryFile = summaryFiles[4];
                break;
        }
		 		 resultsHash.put(OPT_SUMMARYFILE, summaryFile.getPath());
        // Now actually write out the summary file
        try
        {
            PrintWriter printWriter = new PrintWriter(new FileWriter(summaryFile));
            // Fake the output of Logger.logElement mostly; except 
            //  we add an XML header so this is a legal XML doc
            printWriter.println("<?xml version=\"1.0\"?>"); 
            printWriter.println("<" + elementName); 
            for (Enumeration keys = resultsHash.keys();
                    keys.hasMoreElements(); /* no increment portion */ )
            {
                Object key = keys.nextElement();
                printWriter.println(key + "=\"" + resultsHash.get(key) + "\"");
            }
            printWriter.println(">"); 
            printWriter.println(overallResult); 
            printWriter.println("</" + elementName + ">"); 
            printWriter.close();
        }
        catch(Exception e)
        {
            logErrorMsg("writeResultsStatus: Can't write: " + summaryFile);
        }
    }

    //-----------------------------------------------------
    //-------- Test results reporting check* routines --------
    //-----------------------------------------------------

    /**
     * Compares actual and expected, and logs the result, pass/fail.
     * The comment you pass is added along with the pass/fail, of course.
     * Currenly, you may pass a pair of any of these simple {type}:
     * <ui>
     * <li>boolean</li>
     * <li>byte</li>
     * <li>short</li>
     * <li>int</li>
     * <li>long</li>
     * <li>float</li>
     * <li>double</li>
     * <li>String</li>
     * </ui>
     * <p>While tests could simply call checkPass(comment), providing these convenience
     * method can save lines of code, since you can replace:</p>
     * <code>if (foo = bar) <BR>
     *           checkPass(comment); <BR>
     *       else <BR>
     *           checkFail(comment);</code>
     * <p>With the much simpler:</p>
     * <code>check(foo, bar, comment);</code>
     * <p>Plus, you can either use or ignore the boolean return value.</p>
     * <p>Note that individual methods checkInt(...), checkLong(...), etc. also exist.
     * These type-independent overriden methods are provided as a convenience to
     * Java-only testwriters.  JavaScript scripts must call the
     * type-specific checkInt(...), checkString(...), etc. methods directly.</p>
     * <p>Note that testwriters are free to ignore the boolean return value.</p>
     * @author Shane_Curcuru@lotus.com
     * @param actual value returned from your test code.
     * @param expected value that test should return to pass.
     * @param comment to log out with result.
     * @return status, true=pass, false otherwise
     * @see #checkPass
     * @see #checkFail
     * @see #checkObject
     */
    public boolean check(boolean actual, boolean expected, String comment)
    {
        return (checkBool(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(byte actual, byte expected, String comment)
    {
        return (checkByte(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(short actual, short expected, String comment)
    {
        return (checkShort(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(int actual, int expected, String comment)
    {
        return (checkInt(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(long actual, long expected, String comment)
    {
        return (checkLong(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(float actual, float expected, String comment)
    {
        return (checkFloat(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(double actual, double expected, String comment)
    {
        return (checkDouble(actual, expected, comment));
    }

    /**
     * NEEDSDOC Method check 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (check) @return
     */
    public boolean check(String actual, String expected, String comment)
    {
        return (checkString(actual, expected, comment));
    }

    // No check(Object, Object, String) currently provided, please call checkObject(...) directly

    /**
     * Compares actual and expected (Object), and logs the result, pass/fail.
     * <p><b>Special note for checkObject:</b></p>
     * <p>Since this takes an object reference and not a primitive type,
     * it works slightly differently than other check{Type} methods.</p>
     * <ui>
     * <li>If both are null, then Pass</li>
     * <li>Else If actual.equals(expected) than Pass</li>
     * <li>Else Fail</li>
     * </ui>
     * @author Shane_Curcuru@lotus.com
     * @param actual Object returned from your test code.
     * @param expected Object that test should return to pass.
     * @param comment to log out with result.
     * @see #checkPass
     * @see #checkFail
     * @see #check
     *
     * NEEDSDOC ($objectName$) @return
     */
    public boolean checkObject(Object actual, Object expected, String comment)
    {

        // Pass if both null, or both valid & equals
        if (actual != null)
        {
            if (actual.equals(expected))
            {
                checkPass(comment);

                return true;
            }
            else
            {
                checkFail(comment);

                return false;
            }
        }
        else
        {  // actual is null, so can't use .equals
            if (expected == null)
            {
                checkPass(comment);

                return true;
            }
            else
            {
                checkFail(comment);

                return false;
            }
        }
    }

    /**
     * NEEDSDOC Method checkBool 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkBool) @return
     */
    public boolean checkBool(boolean actual, boolean expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * NEEDSDOC Method checkByte 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkByte) @return
     */
    public boolean checkByte(byte actual, byte expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * NEEDSDOC Method checkShort 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkShort) @return
     */
    public boolean checkShort(short actual, short expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * NEEDSDOC Method checkInt 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkInt) @return
     */
    public boolean checkInt(int actual, int expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * NEEDSDOC Method checkLong 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkLong) @return
     */
    public boolean checkLong(long actual, long expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * NEEDSDOC Method checkFloat 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkFloat) @return
     */
    public boolean checkFloat(float actual, float expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * NEEDSDOC Method checkDouble 
     *
     *
     * NEEDSDOC @param actual
     * NEEDSDOC @param expected
     * NEEDSDOC @param comment
     *
     * NEEDSDOC (checkDouble) @return
     */
    public boolean checkDouble(double actual, double expected, String comment)
    {

        if (actual == expected)
        {
            checkPass(comment);

            return true;
        }
        else
        {
            checkFail(comment);

            return false;
        }
    }

    /**
     * Compares actual and expected (String), and logs the result, pass/fail.
     * <p><b>Special note for checkString:</b></p>
     * <p>Since this takes a String object and not a primitive type,
     * it works slightly differently than other check{Type} methods.</p>
     * <ui>
     * <li>If both are null, then Pass</li>
     * <li>Else If actual.compareTo(expected) == 0 than Pass</li>
     * <li>Else Fail</li>
     * </ui>
     * @author Shane_Curcuru@lotus.com
     * @param actual String returned from your test code.
     * @param expected String that test should return to pass.
     * @param comment to log out with result.
     * @see #checkPass
     * @see #checkFail
     * @see #checkObject
     *
     * NEEDSDOC ($objectName$) @return
     */
    public boolean checkString(String actual, String expected, String comment)
    {

        // Pass if both null, or both valid & equals
        if (actual != null)
        {

            // .compareTo returns 0 if the strings match lexicographically
            if ((expected != null) && (actual.compareTo(expected) == 0))
            {
                checkPass(comment);

                return true;
            }
            else
            {
                checkFail(comment);

                return false;
            }
        }
        else
        {  // actual is null, so can't use .equals
            if (expected == null)
            {
                checkPass(comment);

                return true;
            }
            else
            {
                checkFail(comment);

                return false;
            }
        }
    }

    /**
     * Uses an external CheckService to Compares actual and expected,
     * and logs the result, pass/fail.
     * <p>CheckServices may be implemented to do custom equivalency
     * checking between complex object types. It is the responsibility
     * of the CheckService to call back into us to report results.</p>
     * @author Shane_Curcuru@lotus.com
     * @param CheckService implementation to use
     *
     * @param service a non-null CheckService implementation for 
     * this type of actual and expected object
     * @param actual Object returned from your test code.
     * @param expected Object that test should return to pass.
     * @param comment to log out with result.
     * @return status true if PASS_RESULT, false otherwise
     * @see #checkPass
     * @see #checkFail
     * @see #check
     */
    public boolean check(CheckService service, Object actual,
                         Object expected, String comment)
    {

        if (service == null)
        {
            checkErr("CheckService null for: " + comment);

            return false;
        }

        if (service.check(this, actual, expected, comment) == PASS_RESULT)
            return true;
        else
            return false;
    }

    /**
     * Uses an external CheckService to Compares actual and expected,
     * and logs the result, pass/fail.
     */
    public boolean check(CheckService service, Object actual,
                         Object expected, String comment, String id)
    {

        if (service == null)
        {
            checkErr("CheckService null for: " + comment);

            return false;
        }

        if (service.check(this, actual, expected, comment, id) == PASS_RESULT)
            return true;
        else
            return false;
    }

    /** Flag to control internal debugging of Reporter; sends extra info to System.out. */
    protected boolean debug = false;

    /**
     * Accessor for internal debugging flag.  
     *
     * NEEDSDOC ($objectName$) @return
     */
    public boolean getDebug()
    {
        return (debug);
    }

    /**
     * Accessor for internal debugging flag.  
     *
     * NEEDSDOC @param setDbg
     */
    public void setDebug(boolean setDbg)
    {

        debug = setDbg;

        debugPrintln("setDebug enabled");  // will only print if setDbg was true
    }

    /**
     * Basic debugging output wrapper for Reporter.  
     *
     * NEEDSDOC @param msg
     */
    public void debugPrintln(String msg)
    {

        if (!debug)
            return;

        // If we have reporters, use them
        if (numLoggers > 0)
            logCriticalMsg("RI.dP: " + msg);

            // Otherwise, just dump to the console
        else
            System.out.println("RI.dP: " + msg);
    }

    /**
     * Utility method to increment result counters.  
     *
     * NEEDSDOC @param ctrOffset
     * NEEDSDOC @param r
     */
    public void incrementResultCounter(int ctrOffset, int r)
    {

        switch (r)
        {
        case INCP_RESULT :
            incpCount[ctrOffset]++;
            break;
        case PASS_RESULT :
            passCount[ctrOffset]++;
            break;
        case AMBG_RESULT :
            ambgCount[ctrOffset]++;
            break;
        case FAIL_RESULT :
            failCount[ctrOffset]++;
            break;
        case ERRR_RESULT :
            errrCount[ctrOffset]++;
            break;
        default :
            ;  // NEEDSWORK: should we report this, or allow users to add their own counters?
        }
    }

    /**
     * Utility method to translate an int result to a string.  
     *
     * NEEDSDOC @param r
     *
     * NEEDSDOC ($objectName$) @return
     */
    public static String resultToString(int r)
    {

        switch (r)
        {
        case INCP_RESULT :
            return (INCP);
        case PASS_RESULT :
            return (PASS);
        case AMBG_RESULT :
            return (AMBG);
        case FAIL_RESULT :
            return (FAIL);
        case ERRR_RESULT :
            return (ERRR);
        default :
            return ("Unkn");  // NEEDSWORK: should have better constant for this
        }
    }
}  // end of class Reporter

