/*
 *
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.midp.i3test;

import com.sun.midp.security.SecurityInitializer;
import com.sun.midp.security.SecurityToken;
import com.sun.midp.security.ImplicitlyTrustedClass;

/**
 * The main class for writing integrated internal interface (i3) tests.  Tests
 * should create a subclass of TestCase and implement the runTests() method.
 * This method is responsible for running all of the tests.  Tests consist of
 * a test declaration followed by one or more assertions.  The declaration is
 * a call to the declare() method, which simply establishes a name for the set
 * of assertions to follow.  The assertions are made through calls to the
 * assert*() family of methods.  The framework considers it an error if any 
 * assertions are made prior to a call to declare(), or if there is a call to 
 * declare() with no subsequent calls to assert().
 *
 * <p>The TestCase class and its assert() methods are loosely based on the
 * JUnit TestCase class.  JUnit uses reflection to find test methods.  Since
 * reflection isn't present in CLDC, we need to have a runTests() method that
 * calls them explicitly.
 *
 * <p>JUnit, in contrast to other test frameworks, doesn't return a result 
 * from each test.  Instead, each test simply makes assertions, and only 
 * assertions that fail are flagged.  The i3 tests follow the JUnit approach.
 * For this reason, there is no pass() method one might expect to see 
 * corresponding to the fail() method.
 *
 * <p>While it's not a requirement, it's conventional for the TestCase 
 * subclass to reside in the same package as the code under test.  This will 
 * allow the tests access to package-private variables and methods.
 * 
 * <p>Each of the different assert() calls has an optional
 * <code>message</code> parameter.  This message is emitted only when the 
 * assertion fails and should be written with this in mind.
 *
 * <p>For "negative tests" that require an exception to be thrown, a suggested 
 * style is to record the fact that the correct exception was thrown in a 
 * boolean variable.  This variable can then be tested using one of the 
 * assert() calls.  This is probably preferable to placing calls to fail() in 
 * the midst of the test method.  See testThree() in the example below.
 *
 * <p>The framework will catch any Throwable thrown by the tests and will log
 * a message and count the occurrence as an error.  This allows tests to be
 * written more simply, because they can rely on the framework to catch
 * everything. In support of this, the runTests() method has been defined with
 * a 'throws Throwable' clause.
 *
 * <p>A general rule is that tests should run independently of each other. 
 * That is, tests should not leave any side effects that other tests rely on 
 * or that could cause them to fail.  Typically, tests should create fresh 
 * objects for testing instead of reusing objects from other tests.  If a test
 * has any external side effects, such as creating threads or opening files, 
 * the test should be coded with a 'finally' clause that attempts to clean up 
 * after itself.  See testFour() in the example below.
 *
 * <p>Example:
 * <pre><code>
 * import com.sun.midp.i3test;
 * package java.lang;
 *
 * public class SampleTest extends TestCase {
 *     void testOne() {
 *         int i = 1 + 1;
 *         assertEquals("integer addition failed", 2, i);
 *     }
 * 
 *     void testTwo() {
 *         Object x = new Object();
 *         assertNotNull("Object constructor returned null", x);
 *     }
 * 
 *     void testThree() {
 *         StringBuffer sb;
 *         boolean caught = false;
 *         try {
 *             sb = new StringBuffer(-1);
 *         } catch (NegativeArraySizeException e) {
 *             caught = true;
 *         }
 *         assertTrue("no exception thrown", caught);
 *     }
 *
 *     void testFour() {
 *         Thread thr = new Thread(...).start();
 *         try {
 *             // ...
 *         } finally {
 *             thr.interrupt();
 *         }
 *     }
 * 
 *     public void runTests() {
 *         declare("testOne");
 *         testOne();
 *         declare("testTwo");
 *         testTwo();
 *         declare("testThree");
 *         testThree();
 *         declare("testFour");
 *         testFour();
 *     }
 * }
 * </code></pre>
 */
public abstract class TestCase {

    // the lone constructor

    /**
     * Constructs a TestCase.  Since TestCase is an abstract class, this 
     * constructor is called only at the time a TestCase subclass is being 
     * constructed.
     */
    public TestCase() {
        if (verbose) {
            p("## TestCase " + this.getClass().getName());
        }
        currentTestCase = this;
        currentTest = null;
        totalCases++;
        currentNumAsserts = 0;
    }

    // ==================== public methods ====================

    /**
     * Runs all the tests for this TestCase.  This is an abstract method that
     * must be implemented by the subclass.  The implementation of this method
     * should run all of the tests provided in this TestCase.  This method can
     * also be used to initialize and tear down any state that might need to
     * be shared among all the tests.
     * 
     * <p>A suggested organization is to have runTests() alternate calling
     * declare() and an internal method that implements the test.  One could
     * write the declare() method as part of the internal test method, but
     * consolidating all calls in runTests() makes it a little easier to
     * ensure that each test has its own call to declare().  See the example
     * in the class documentation.
     */
    public abstract void runTests() throws Throwable;

    /**
     * Declares that the set of assertions that follow this call belong to a
     * test named <code>testName</code>.  The framework considers it an error
     * if no calls to any of the assert() methods follow a call to declare().
     *
     * @param testName the name of the test
     */
    public void declare(String testName) {
        if (testName == null) {
            throw new NullPointerException("test name is null");
        }

        if (verbose) {
            p("## test " + testName);
        }

        if (currentNumAsserts == 0 && currentTest != null) {
            p("ERROR no asserts in test case " + currentTestCaseName() +
                " test " + currentTest);
            errorTestWithoutAssert++;
        }

        currentTest = testName;
        totalTests++;
        currentNumAsserts = 0;
    }

    /**
     * Tests the assertion that the boolean <code>condition</code> is true.
     * If the condition is true, this method simply updates some statistics
     * and returns.  If the condition is false, the failure is noted and the
     * message is emitted, along with an indication of the current TestCase
     * and the name of the test currently being run.  The
     * <code>message</code> string should be phrased in a way that makes sense 
     * when the test fails.  See the example in the class documentation.
     *
     * @param message the message to be emitted if the assertion fails
     * @param condition the condition asserted to be true
     */
    public void assertTrue(String message, boolean condition) {
        if (currentTest == null) {
            p("ERROR assert \"" + message + "\" not part of any test");
            errorAssertWithoutTest++;
            return;
        }

        currentNumAsserts++;
        totalAsserts++;

        if (verbose) {
            p("## " + totalAsserts + ": " + message);
        }

        if (!condition) {
            totalFailures++;
            String m = "FAIL " + currentTestCaseName();
            if (currentTest != null) {
                m += "#" + currentTest;
            }
            if (message != null) {
                m += ": " + message;
            }
            p(m);
        }
    }

    /**
     * Asserts that <code>condition</code> is true.
     *
     * @param condition the condition to be tested
     */
    public void assertTrue(boolean condition) {
        assertTrue(null, condition);
    }
    
    /**
     * Asserts that two objects are equal according to the equals() method.
     *
     * @param expected an object containing the expected value
     * @param actual an object containing the actual value
     */
    public void assertEquals(Object expected, Object actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two objects are equal according to the equals() method.
     * Two null references are considered to be equal.
     *
     * @param message the message to be emitted if the assertion fails
     * @param expected an object containing the expected value
     * @param actual an object containing the actual value
     */
    public void assertEquals(String message, Object expected, Object actual) {
	boolean isequal = (expected == actual);
        
        // If references aren't identical, call equals() only when
        // both references are non-null.

	if (!isequal && expected != null && actual != null) {
	    isequal = expected.equals(actual);
	}
	assertTrue(message, isequal);
	if (!isequal) {
	    p("  expected: " + expected + "; actual: " + actual);
	}
    }

    /**
     * Asserts that two integer values are equal.
     *
     * @param expected the expected value
     * @param actual the actual value
     */
    public void assertEquals(int expected, int actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two integer values are equal.
     *
     * @param message the message to be emitted if the assertion fails
     * @param expected the expected value
     * @param actual the actual value
     */
    public void assertEquals(String message, int expected, int actual) {
        assertTrue(message, expected == actual);
	if (expected != actual) {
	    p("  expected: " + expected + "; actual: " + actual);
	}
    }

    /**
     * Asserts that <code>condition</code> is false.
     * 
     * @param condition the condition asserted to be false
     */
    public void assertFalse(boolean condition) {
        assertTrue(null, !condition);
    }

    /**
     * Asserts that <code>condition</code> is false.
     * 
     * @param message the message to be emitted if the assertion fails
     * @param condition the condition asserted to be false
     */
    public void assertFalse(String message, boolean condition) {
        assertTrue(message, !condition);
    }

    /**
     * Asserts that the object reference is not null.
     *
     * @param object the reference to be tested
     */
    public void assertNotNull(Object object) {
        assertTrue(null, object != null);
    }

    /**
     * Asserts that the object reference is not null.
     *
     * @param message the message to be emitted if the assertion fails
     * @param object the reference to be tested
     */
    public void assertNotNull(String message, Object object) {
        assertTrue(message, object != null);
    }

    /**
     * Asserts that two references do not point to the same object,
     * using the == operator.
     *
     * @param expected a reference to the expected object
     * @param actual a reference to the actual object
     */
    public void assertNotSame(Object expected, Object actual) {
        assertNotSame(null, expected, actual);
    }

    /**
     * Asserts that two references do not point to the same object,
     * using the == operator.
     *
     * @param message the message to be emitted if the assertion fails
     * @param expected a reference to the expected object
     * @param actual a reference to the actual object
     */
    public void assertNotSame(String message, Object expected, Object actual) {
        assertTrue(message, expected != actual);
	if (expected == actual) {
	    p("  expected: " + expected + "; actual: " + actual);
	}
    }

    /**
     * Asserts that the object reference is null.
     *
     * @param object the reference to be tested
     */
    public void assertNull(Object object) {
        assertNull(null, object);
    }

    /**
     * Asserts that the object reference is null.
     *
     * @param message the message to be emitted if the assertion fails
     * @param object the reference to be tested
     */
    public void assertNull(String message, Object object) {
        assertTrue(message, object == null);
	if (object != null) {
	    p("  actual: " + object);
	}
    }

    /**
     * Asserts that two references point to the same object, using the
     * == operator.
     *
     * @param expected a reference to the expected object
     * @param actual a reference to the actual object
     */
    public void assertSame(Object expected, Object actual) {
        assertSame(null, expected, actual);
    }

    /**
     * Asserts that two references point to the same object, using the
     * == operator.
     *
     * @param message the message to be emitted if the assertion fails
     * @param expected a reference to the expected object
     * @param actual a reference to the actual object
     */
    public void assertSame(String message, Object expected, Object actual) {
        assertTrue(message, expected == actual);
	if (expected != actual) {
	    p("  expected: " + expected + "; actual: " + actual);
	}
    }

    /**
     * Signals an unconditional assertion failure.
     */
    public void fail() {
        assertTrue(null, false);
    }

    /**
     * Signals an unconditional assertion failure.
     *
     * @param message the message to be emitted, explaining the failure
     */
    public void fail(String message) {
        assertTrue(message, false);
    }

    /**
     * Gets the system's internal security token. This is useful for testing 
     * sensitive interfaces that require a security token parameter.  This 
     * returns a valid security token only when called within the context of a 
     * call to runTests() inside a TestCase.  At other times it will throw a 
     * SecurityException.
     *
     * @return the internal security token
     * @throws SecurityException if not called from within runTests()
     */
    protected SecurityToken getSecurityToken() {
        if (tokenEnabled) {
            return internalSecurityToken;
        } else {
            throw new SecurityException();
        }
    }

    // ==================== static variables ====================

    static boolean verbose = false;

    static private class SecurityTrusted
        implements ImplicitlyTrustedClass {};
    private static SecurityToken internalSecurityToken =
        SecurityInitializer.requestToken(new SecurityTrusted());

    static boolean tokenEnabled = false;

    // general statistics

    static int totalCases = 0;    // total number of test cases
    static int totalTests = 0;    // total number of tests declared
    static int totalAsserts = 0;  // total number of asserts called
    static int totalFailures = 0; // total number of assertion failures

    // error counts

    static int errorClassNotFound = 0;
    static int errorConstructorException = 0;
    static int errorNotTestCase = 0;
    static int errorTestRunThrows = 0;
    static int errorNoTests = 0;
    static int errorAssertWithoutTest = 0;
    static int errorTestWithoutAssert = 0;

    // state information about the currently running test

    static TestCase currentTestCase;
    static String currentTest;
    static int currentNumAsserts;

    // ==================== static implementation ====================

    /**
     * Run the named test case.
     * @param testCaseName the class name of the test case
     */
    static void runTestCase(String testCaseName) {
        Class clazz;
        Object obj;
        TestCase tc;

        if (verbose) {
            System.out.println("## running test case " + testCaseName);
        }

        try {
            clazz = Class.forName(testCaseName);
        } catch (ClassNotFoundException cnfe) {
            System.out.println(
                "ERROR test class " + testCaseName + " not found");
            errorClassNotFound++;
            return;
        }

        try {
            obj = clazz.newInstance();
        } catch (Throwable t) {
            System.out.println("ERROR test class " + testCaseName +
                " constructor threw " + t);
            errorConstructorException++;
            return;
        }

        try {
            tc = (TestCase)obj;
        } catch (ClassCastException cce) {
            System.out.println("ERROR test class " + testCaseName +
                " not a subclass of TestCase");
            errorNotTestCase++;
            return;
        }

        try {
            tokenEnabled = true;
            tc.runTests();
        } catch (Throwable t) {
            String m = "ERROR " + currentTestCaseName();
            if (currentTest != null) {
                m += "#" + currentTest;
            }
            m += " threw " + t;
            p(m);
            t.printStackTrace();
            errorTestRunThrows++;
        }

        tokenEnabled = false;
        cleanup();
    }

    /**
     * Return the name of the current test case.
     * @return the name of the class for the current test case.
     */
    static String currentTestCaseName() {
        return currentTestCase.getClass().getName();
    }

    /**
     * Cleanup after running a test.
     */
    static void cleanup() {
        if (currentTest == null) {
            p("ERROR " + currentTestCaseName() + " has no tests");
            errorNoTests++;
        } else if (currentNumAsserts == 0) {
            p("ERROR no asserts in test case " + currentTestCaseName() +
                    " test " + currentTest);
            errorTestWithoutAssert++;
        }

        currentTestCase = null;
        currentTest = null;
    }

    /**
     * Set the verbose output flag.
     * @param v true to enable verbose output
     */ 
    static void setVerbose(boolean v) {
        verbose = v;
    }

    /**
     * Print the string.
     * @param s the string to print on a new line.
     */
    static void p(String s) {
        System.out.println(s);
    }

    /**
     * Print the number of errors with the category.
     * @param nerr the number of errors in the category
     * @param errstr the description of the errors
     */
    static void perror(int nerr, String errstr) {
        if (nerr != 0) {
            System.out.println("  " + nerr + " " + errstr);
        }
    }

    // print out a report of all statistics

    static void report() {
        System.out.print("Test run complete: ");
        System.out.print(totalCases
            + (totalCases == 1 ? " testcase " : " testcases "));
        System.out.print(totalTests
            + (totalTests == 1 ? " test " : " tests "));
        System.out.println(totalAsserts
            + (totalAsserts == 1 ? " assertion" : " assertions"));

        if (totalFailures != 0) {
            System.out.println(totalFailures
               + (totalFailures == 1 ? " FAILURE!!!" : " FAILURES!!!"));
        }

        int totalErrors = 
            errorClassNotFound +
            errorNotTestCase +
            errorConstructorException +
            errorTestRunThrows +
            errorNoTests +
            errorAssertWithoutTest +
            errorTestWithoutAssert;

        if (totalErrors != 0) {
            System.out.println(totalErrors
                + (totalErrors == 1 ? " ERROR!!!" : " ERRORS!!!"));
            perror(errorClassNotFound, "test class not found");
            perror(errorConstructorException, "constructor threw exception");
            perror(errorNotTestCase, "class not subclass of TestCase");
            perror(errorTestRunThrows, "test run threw exception");
            perror(errorNoTests, "no tests declared");
            perror(errorAssertWithoutTest, "asserts outside of any test");
            perror(errorTestWithoutAssert, "tests with no asserts");
        }
    }

    /**
     * Reset the counters of all errors and exceptions.
     */
    static void reset() {
        totalCases = 0;
        totalTests = 0;
        totalAsserts = 0;
        totalFailures = 0;
        currentNumAsserts = 0;

        errorClassNotFound = 0;
        errorConstructorException = 0;
        errorNotTestCase = 0;
        errorTestRunThrows = 0;
        errorNoTests = 0;
        errorAssertWithoutTest = 0;
        errorTestWithoutAssert = 0;
    }
}
