/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code 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.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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 in the LICENSE file that
 * accompanied this code).
 *
 * 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 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;

import com.sun.javatest.httpd.HttpdServer;
import com.sun.javatest.httpd.RootRegistry;

import com.sun.javatest.util.BackupPolicy;
import com.sun.javatest.util.DynamicArray;
import com.sun.javatest.util.I18NResourceBundle;
import com.sun.javatest.util.ReadAheadIterator;

/**
 * The object responsible for coordinating the execution of a test run.
 */
public class Harness
{
    /**
     * This exception is used to report problems while executing a test run.
     */
    public static class Fault extends Exception
    {
	Fault(I18NResourceBundle i18n, String s) {
	    super(i18n.getString(s));
	}

	Fault(I18NResourceBundle i18n, String s, Object o) {
	    super(i18n.getString(s, o));
	}

	Fault(I18NResourceBundle i18n, String s, Object[] o) {
	    super(i18n.getString(s, o));
	}
    }

    /**
     * This interface provides a means for Harness to report
     * on events that might be of interest as it executes.
     */
    public interface Observer
    {
	/**
	 * The harness is beginning to execute tests.
	 * @param params the parameters for the test run
	 */
	void startingTestRun(Parameters params);

	/**
	 * The harness is about to run the given test.
	 *
	 * @param tr The test result which is going to receive the data
	 *        from the current execution of that test.
	 */
	void startingTest(TestResult tr);

	/**
	 * The harness has finished running the given test.
	 * This message is sent without repect to the resulting test's
	 * completion status (pass, fail, etc...).
	 *
	 * @param tr The result object containing the results from the
	 *        execution which was just completed.
	 */
	void finishedTest(TestResult tr);

	/**
	 * The harness is about to stop a test run, before it has finished 
	 * executing all the specified tests. The method is not notified if
	 * the test run completes normally, after executing all the specified 
	 * tests.
	 */
	void stoppingTestRun();

	/**
	 * The harness has finished running tests and is doing other activities
	 * (writing the report, updating caches, etc...).  This message will
	 * be broadcast both when error conditions terminate the run or when
	 * a test completes normally. It may provide a reasonable opportunity
	 * for a client to clean up any resources that were used during the test
	 * run, before a new run is started.
	 */
	void finishedTesting();

	/**
	 * The test run has been completed, either because the user requested
	 * that the harness stop, the harness decided to terminate the test run,
	 * or all requested tests have been run.  The harness is now ready to
	 * perform another test run. Note that since the actions of other observers
	 * are undefined, a new test run may have already been started by the time
	 * this method is called for any specific observer.
	 *
	 * @param allOK True if all tests passed, false otherwise.
	 */
	void finishedTestRun(boolean allOK);

	/**
	 * The given error occured.
	 *
	 * @param msg A description of the error event.
	 */
	void error(String msg);
    }


    /**
     * Instantiate a harness.
     * @param classDir	The class dir to put in the environment for otherJVM tests
     * @deprecated	Use Harness() instead
     * @see #Harness()
     * @see #setClassDir
     */
    public Harness(File classDir) {
	this();
	setClassDir(classDir);
    }
    
    /**
     * Instantiate a harness.
     */
    public Harness() {
	backupPolicy = BackupPolicy.noBackups();

	params = null;

	if (!Boolean.getBoolean("javatest.noTraceRequired")) {
	    trace = new Trace(backupPolicy);
	    addObserver(trace);
	}

	// web server
	if (HttpdServer.isActive()) {
	    httpHandler = new HarnessHttpHandler(this);
	    RootRegistry.getInstance().addHandler("/harness", "JT Harness",
						  httpHandler);
	}
    }

    //--------------------------------------------------------------------------

    /**
     * Get the backup policy object used by this harness, used to determine
     * the policy for backing up files before overwriting them.
     * @return the backup policy object used by this harness
     * @see #setBackupPolicy
     */
    public BackupPolicy getBackupPolicy() {
	return backupPolicy;
    }

    /**
     * Set the backup policy object to be used by this harness, 
     * used to determine the policy for backing up files before 
     * overwriting them.
     * @param bp the backup policy object used by this harness
     * @see #getBackupPolicy
     */
    public void setBackupPolicy(BackupPolicy bp) {
	backupPolicy = bp;
    }

    /**
     * Check if a trace file should be generated while performing a test run.
     * @return true if and only if a trace file should be generated
     * @see #setTracingRequired
     */
    public boolean isTracingRequired() {
	return (trace != null);
    }

    /**
     * Set whether a trace file should be generated while performing a test run.
     * @param b whether or not a trace file should be generated
     * @see #isTracingRequired
     */
    public void setTracingRequired(boolean b) {
	if (b && trace == null) {
	    trace = new Trace(backupPolicy);
	    addObserver(trace);
	    //currentResults.addObserver(trace);
	}
	else if (!b && trace != null) {
	    removeObserver(trace);
	    //currentResults.removeObserver(trace);
	    trace = null;
	}
    }

    /**
     * Get the class directory or jar file containing JT Harness.
     * @return the class directory or jar file containing JT Harness
     * @see #setClassDir
     */
    public static File getClassDir() {
	return classDir;
    }

    /**
     * Specify the class directory or jar file containing JT Harness.
     * @param classDir the class directory or jar file containing JT Harness
     * @see #getClassDir
     */	
    public static void setClassDir(File classDir) {
	if (Harness.classDir != null && Harness.classDir != classDir)
	    throw new IllegalStateException(i18n.getString("harness.classDirAlreadySet"));
	Harness.classDir = classDir;
    }
	

    //--------------------------------------------------------------------------

    /**
     * Get the current parameters of the harness.
     *
     * @return null if the parameters have not been set.
     */
    public Parameters getParameters() {
	return params;
    }	

    //--------------------------------------------------------------------------

    /**
     * Get the current test environment being used by the harness.
     * This is similar to getParameters().getEnv(), except that the environment
     * returned here has some standard additional fields set by the harness
     * itself.
     *
     * @return null if the environment has not been set.
     */
    public TestEnvironment getEnv() {
	return env;
    }

    //--------------------------------------------------------------------------

    /**
     * Get the current set of results.  This will either be the set of results
     * from which are currently running, or the results from the last run.
     *
     * @return null if no results are currently available.  This will be the case
     *         if the Harness has not been run, or the parameters have been changed
     *         without doing a new run.
     */
    public TestResultTable getResultTable() {
	WorkDirectory wd = (params == null ? null : params.getWorkDirectory());
	return (wd == null ? null : wd.getTestResultTable());
    }

    //--------------------------------------------------------------------------

    /**
     * Add an observer to be notified during the execution of a test run.
     * Observers are notified of events in the reverse order they were added --
     * the most recently added observer gets notified first.
     * @param o the observer to be added
     * @see #removeObserver
     */
    public void addObserver(Observer o) {
	notifier.addObserver(o);
    }

    /**
     * Remove a previously registered observer so that it will no longer
     *  be notified during the execution of a test run.
     * It is safe for observers to remove themselves during a notification;
     * most obviously, an observer may remove itself during finishedTesting()
     * or finishedTestRun().
     * @param o the observer to be removed
     * @see #addObserver
     */
    public void removeObserver(Observer o) {
	notifier.removeObserver(o);
    }

    //--------------------------------------------------------------------------

    /**
     * Start running all the tests defined by a new set of parameters.
     * The tests are run asynchronously, in a separate worker thread.
     * @param p		The parameters to be set when the tests are run.
     *			Any errors in the parameters are reported to
     *			any registered observers.
     * @throws Harness.Fault if the harness is currently running tests
     *			and so cannot start running any more tests right now.
     * @see #isRunning
     * @see #stop
     * @see #waitUntilDone
     */
    public void start(Parameters p) throws Fault {
	startWorker(p);
    }

    /**
     * Wait until the harness completes the current task.
     * @exception InterruptedException if the thread making the call is
     * interrupted.
     */
    public synchronized void waitUntilDone() throws InterruptedException {
	while (worker != null) {
	    wait();
	}
    }


    /**
     * Stop the harness executing any tests. If no tests are running,
     * the method does nothing; otherwise it notifies any observers,
     * and interrupts the thread doing the work. The worker may carry
     * on for a short time after this method is called, while it waits
     * for all the related tasks to complete.
     * @see #waitUntilDone
     */
    public synchronized void stop() {
	if (worker != null) {
	    if (!stopping) {
		notifier.stoppingTestRun();
		stopping = true;
	    }
	    worker.interrupt();
	}
    }

    /**
     * Run the tests defined by a new set of parameters.
     * @param params	The parameters to be used; they will be validated first.
     * @return true if and only if all the selected tests were executed successfully, and all passed
     * @throws Harness.Fault if the harness is currently running tests
     *			and cannot start running any more tests right now.
     * @throws InterruptedException if the thread making the call is
     * interrupted, perhaps because of an asynchronoous call of stop().
     * @see #isRunning
     * @see #stop
     * @see #waitUntilDone
     */
    public boolean batch(Parameters params)
	throws Fault, InterruptedException 
    {
        isBatchRun = true;
	// allow full read-ahead by default now - as of 3.2.1
	// this allows the not run field of verbose mode to work
	if (Boolean.getBoolean("javatest.noReadAhead"))
	    readAheadMode = ReadAheadIterator.NONE;

	synchronized (this) {
	    if (worker != null)
		throw new Fault(i18n, "harness.alreadyRunning");
	    worker = Thread.currentThread();
	} 

	// parameters will be checked later, in runTests, but check here
	// too to specifically verify that workDir is set
	if (!params.isValid())
	    throw new Harness.Fault(i18n, "harness.incompleteParameters",
				    params.getErrorMessage());

	boolean ok = false;
	try {
	    workDir = params.getWorkDirectory();
	    resultTable = workDir.getTestResultTable();
            // XXX this was a performance enhancer for 3.x
            //   it will not work if the user expects previous results
            //   to be erased/hidden if they no longer exist in the test
            //   suite.  that goal and this one are fundamentally opposed
	    //resultTable.suppressFinderScan(true);
	    ok = runTests(params, ZERO_TESTS_OK);
	}
	catch (TestSuite.Fault e) {
	    throw new Fault(i18n, "harness.testsuiteError", e.getMessage());
	}
	finally {
	    synchronized (this) {
		worker = null;
		notifyAll();
	    }

	    notifier.finishedTestRun(ok);
            isBatchRun = false;
	}

	return ok;
    }

    /**
     * Check if the harness is currently executing a test suite or not.
     * @return true if and only if the harness is currently executing a test suite.
     * @see #start
     * @see #batch
     * @see #stop
     * @see #waitUntilDone
     */
    public boolean isRunning() {
	return (worker != null);
    }

    /**
     * Was the harness invoked in batch mode?  If it is not in batch mode, this
     * typically implies that the user is using an interactive GUI interface.
     * @return True if the harness is running and was invoked in batch mode.
     * @throws IllegalStateException If the harness is not running, care should
     *         be taken to handle this in case the run terminates.
     */
    public synchronized boolean isBatchRun() throws IllegalStateException {
        if (!isRunning())
            throw new IllegalStateException();

        return isBatchRun;
    }

    /**
     * Indicates whether the harness has located all the tests it will execute.
     * If true, then <tt>getTestsFoundCount()</tt> will return the number of test
     * which will be executed during this test run; assuming the harness does not
     * halt for special cases (errors, user request, etc...).  If false,
     * <tt>getTestsFoundCount()</tt> returns the number of tests located so
     * far.
     * @return True if all tests have been located.  False if the harness is
     *         still looking for tests.  Always false if the harness is not
     *         running.
     * @see #isRunning()
     * @see #getTestsFoundCount()
     */
    public boolean isAllTestsFound() {
	if (isRunning() && raTestIter != null)
	    return raTestIter.isSourceExhausted();
	else
	    return false;
    }

    /**
     * Find time since the start of the current or last run.
     * If no run is in progress, this is the time it took to complete the
     * last run.
     *
     * @return Zero if no run has ever been started yet.  Elapsed time in
     *         milliseconds otherwise.
     */
    public long getElapsedTime() {
	long time = 0l;

	if (startTime == -1l)
	    time = 0l;			// no data avail.
	else if (finishTime == -1l) {	// isRunning() isn't good enough
	    long now = System.currentTimeMillis();
	    time = now - startTime;	// we are still running
	}
	else
	    time = finishTime - startTime;

	return time;
    }

    /**
     * Get the time at which the last run start.
     * 
     * @return Time when the last run started in milliseconds.  -1 if there is
     *         no previous run.
     * @see #getFinishTime
     */
    public long getStartTime() {
	return startTime;
    }

    /**
     * Get the time at which the last run finished.  This is the time when
     * the last test completed, and does not include post-run cleanup time.
     * 
     * @return Time when the last run finished in milliseconds.  -1 if there is
     *         no previous run or a run is in progress.
     * @see #getStartTime
     */
    public long getFinishTime() {
	return finishTime;
    }

    /**
     * Find out the estimated time required to complete the remaining tests.
     *
     * @return A time estimate in milliseconds.  Zero if no run is in progress or
     *         no estimate is available.
     */
    public long getEstimatedTime() {
	if (isRunning() == false || numTestsDone == 0)
	    return 0l;

	long estRemain = getElapsedTime() * (getTestsFoundCount() - numTestsDone) / numTestsDone;
	return estRemain;
    }

    /**
     * Find out how many tests to run have been located so far.  Data will pertain
     * to the previous run (if any) if isRunning() is false.  The return will be
     * zero if isRunning() is false and there is no previous run for this instance
     * of the Harness.
     *
     * @return Number of tests which the harness will try to run.  Greater than or
     *         equal to zero and less than or equal to the total number of tests in
     *         the testsuite.
     * @see #isRunning()
     */
    public int getTestsFoundCount() {
	if (raTestIter == null) 
	    return 0;

	synchronized (raTestIter) {
	    return raTestIter.getUsedElementCount() + raTestIter.getOutputQueueSize();
	}
    }

    /**
     * Set the threshold for automatic haulting of a test run.
     * The current algorithm is to begin at zero, add one for every failure,
     * five for every error and subtract two for each pass.  This value must be
     * set before the run begins, do not change it during a run.
     * @see #getAutostopThreshold
     */
    public void setAutostopThreshold(int n) {
        autostopThreshold = n;
    }

    /**
     * @see #setAutoStopThreshold
     */
    public int getAutostopThreshold(int n) {
        return autostopThreshold;
    }

    /**
     * Start a worker thread going to perform run tests asynchronously.
     */
    private synchronized void startWorker(final Parameters p) throws Fault {
	if (worker != null)
	    throw new Fault(i18n, "harness.alreadyRunning");

	worker = new Thread() {
	    public void run() {	
		boolean ok = false;
		try {
		    ok = runTests(p, ZERO_TESTS_ERROR);
		}
		catch (Fault e) {
		    notifyLocalizedError(e.getMessage());
		}
		catch (TestSuite.Fault e) {
		    notifyLocalizedError(e.getMessage());
		}
		catch (InterruptedException e) {
		    notifyError(i18n, "harness.interrupted");
		}
		finally {
		    synchronized (Harness.this) {
			worker = null;
			Harness.this.notifyAll();
		    }

		    notifier.finishedTestRun(ok);
		}
	    }
	};

	worker.setName("Harness:Worker");
	worker.setPriority(Thread.NORM_PRIORITY - 2); // below AWT!
	worker.start();
    }


    /**
     * This method is the one that does the work and runs the tests. Any parameters
     * should have been set up in the constructor.
     * @return The result is `true' if and only if all tests passed.
     */
    // This methods notifies observers for startingTestRun and stoppingTestRun.
    // The caller should notify finishedTestRun when it is OK to run start again
    // (i.e. when worker has been reset to null.
    private boolean runTests(Parameters p, boolean zeroTestsOK)
	throws Fault, TestSuite.Fault, InterruptedException {

	boolean ok = true; // default return/finished notification value
	stopping = false;
	startTime = System.currentTimeMillis();
	finishTime = -1l;
	numTestsDone = 0;

	if (!p.isValid())
	    throw new Harness.Fault(i18n, "harness.incompleteParameters",
				    p.getErrorMessage());
	params = p;
	
	// get lots of necessary values from parameters
	testSuite = params.getTestSuite();
	workDir = params.getWorkDirectory();
	resultTable = workDir.getTestResultTable();
	excludeList = params.getExcludeList();
	
	workDir.log(i18n, "harness.starting");
	
	// for compatibility with scripts that expect the timeout factor
	// to be an integer, we write out the timeout factor as both an
	// integer and a floating point number. The integer value is 
	// determined by rounding up the floating point number.
	float tf = params.getTimeoutFactor();
	if (tf == Float.NaN)
	    tf = 1.0f;
	
	String[] timeoutFactors = { 
	    String.valueOf((int) (Math.ceil(tf))),
	    String.valueOf(tf) 
	};
	
	env = params.getEnv();
	env.put("javatestTimeoutFactor", timeoutFactors);
	env.putUrlAndFile("javatestClassDir", classDir);
	env.putUrlAndFile("harnessClassDir", classDir);  // backwards compatability
	env.putUrlAndFile("javatestWorkDir", workDir.getRoot());

	// allow architect to reset root of TS in env. if needed
	// esp. for backwards compatibility with JT 2.x test suites
	String altTSRoot = testSuite.getTestSuiteInfo("env.tsRoot");
	// need to validate alt and change into File
	File atsr = (altTSRoot == null ? null : new File(altTSRoot));

	if (atsr != null && atsr.exists()) {
	    env.putUrlAndFile("testSuiteRoot", atsr);
	    env.putUrlAndFile("testSuiteRootDir",
		    (atsr.isDirectory() ? atsr : atsr.getParentFile()));
	}
	else {
	    // normal case
	    env.putUrlAndFile("testSuiteRoot", testSuite.getRoot());
	    env.putUrlAndFile("testSuiteRootDir", testSuite.getRootDir());
	}
	
	// get items required to select the tests to be run
	String[] tests = params.getTests();
	TestFilter[] filters = params.getFilters();
	
	resultTable.waitUntilReady();
	
	// get the appropriate iterator from TRT
	if (tests == null || tests.length == 0)
	    testIter = resultTable.getIterator(filters);
	else {
	    try {
		// CLEANUP REQUIRED: validation only occurs on Files, not Strings
		// resultTable.getIterator should validate strings too
		File[] files = new File[tests.length];
		for (int i = 0; i < tests.length; i++)
		    files[i] = new File(tests[i]);
		testIter = resultTable.getIterator(files, filters);
	    }
	    catch (TestResultTable.Fault err) {
		throw new Harness.Fault(i18n, "harness.badInitFiles",
					err.getMessage());
	    }
	}
	// attach a read ahead iterator
	// use testIter to get specialized info, use raTestIter to get tests
	raTestIter = new ReadAheadIterator(testIter, readAheadMode, DEFAULT_READ_AHEAD);
	
	// autostopThreshold is currently defined by a system property,
	// but could come from parameters
	if (autostopThreshold > 0)
	    addObserver(new Autostop(autostopThreshold));
	
	// notify the test suite we are starting
	testSuite.starting(this);
	
	notifier.startingTestRun(params);

	TestRunner r = testSuite.createTestRunner();
	
	r.setWorkDirectory(workDir);
	r.setBackupPolicy(backupPolicy);
	r.setEnvironment(env);
	r.setExcludeList(excludeList);

	int concurrency = params.getConcurrency();
	concurrency = Math.max(1, Math.min(concurrency, 
					   Parameters.ConcurrencyParameters.MAX_CONCURRENCY));
	r.setConcurrency(concurrency);

	r.setNotifier(notifier);
	
	try {
	    ok = r.runTests(new Iterator() {
		    public boolean hasNext() {
			return (stopping ? false : raTestIter.hasNext());
		    }
		    public Object next() {
			TestResult tr = (TestResult) (raTestIter.next());
			try {
			    return tr.getDescription();
			}
			catch (TestResult.Fault e) {
			    stopping = true;
			    throw new JavaTestError(i18n, "harness.trProb",  tr.getWorkRelativePath(), e);
			}
		    }
		    public void remove() {
			throw new UnsupportedOperationException();
		    }
		});
	}
	catch (InterruptedException e) {
	    // swallow interrupts, because we're just going to wind up the run
	}

	finishTime = System.currentTimeMillis();

	notifier.finishedTesting();

	// calculate number of tests executed
	// NOTE: the stats here don't indicate what the results of the test run were
	int[] stats = testIter.getResultStats();
	int iteratorCount = 0;
	for (int i = 0; i < stats.length; i++)
	    iteratorCount += stats[i];

	if (iteratorCount == 0 && !zeroTestsOK) {
	    // no tests are in the error, pass, fail categories -> none selected
	    notifyError(i18n, "harness.noTests");
	    ok = false;
	}
	else {
            /* user is notified of this in real-time
               although it may not be evident in batch mode
	    if (resultTable.getTestFinder().getErrorCount() > 0) {
		notifyError(i18n, "harness.finderError");
		ok = false;
	    }
            */
	}

	if (ok && (notifier.getErrorCount() > 0 || notifier.getFailedCount() > 0))
	    ok = false;
	
	try {
	    LastRunInfo.writeInfo(workDir, startTime, finishTime, env.getName());
	}
	catch (IOException e) {
	    // ignore
	}
	
	// TRT may need to reread the entire cache
	resultTable.waitUntilReady();
	
	workDir.log(i18n, "harness.done", new Integer(ok ? 0 : 1));

	return ok;
    }


    private void notifyError(I18NResourceBundle i18n, String key) {
	notifyLocalizedError(i18n.getString(key));
    }

    private void notifyError(I18NResourceBundle i18n, String key, Object arg) {
	notifyLocalizedError(i18n.getString(key, arg));
    }

    private void notifyError(I18NResourceBundle i18n, String key, Object[] args) {
	notifyLocalizedError(i18n.getString(key, args));
    }

    private void notifyLocalizedError(String msg) {
	notifier.error(msg);
    }

    //----------member variables-----------------------------------------------------

    private BackupPolicy backupPolicy;
    private int autostopThreshold;
    { Integer i = Integer.getInteger("javatest.autostop.threshold");
      autostopThreshold = (i == null ? 0 : i.intValue());
    }

    private HarnessHttpHandler httpHandler;
    private Trace trace;

    private Thread worker;
    private Parameters params;
    private TestSuite testSuite;
    private WorkDirectory workDir;
    private ExcludeList excludeList;
    private TestResultTable.TreeIterator testIter;
    private int readAheadMode = ReadAheadIterator.FULL;
    private ReadAheadIterator raTestIter;
    private int numTestsDone;
    private TestEnvironment env;
    private TestResultTable resultTable;
    private Notifier notifier = new Notifier();

    private long startTime = -1l;
    private long finishTime = -1l;
    private boolean isBatchRun;
    private boolean stopping;

    private static File classDir;
    private static final boolean ZERO_TESTS_OK = true;
    private static final boolean ZERO_TESTS_ERROR = false;
    private static final int DEFAULT_READ_AHEAD = 100;
    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(Harness.class);

    private class Notifier implements Harness.Observer
    {
	void addObserver(Observer o) {
	    if (o == null)
		throw new NullPointerException();
	    observers = (Observer[])DynamicArray.append(observers, o);
	}

	void removeObserver(Observer o) {
	    observers = (Observer[])DynamicArray.remove(observers, o);
	}

	public void startingTestRun(Parameters params) {
	    resultTable.starting();

	    // protect against removing observers during notification
	    Observer[] stableObservers = observers;
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].startingTestRun(params);
	}

	public void startingTest(TestResult tr) {
	    // protect against removing observers during notification
	    Observer[] stableObservers = observers;
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].startingTest(tr);
	}
	
	public void finishedTest(TestResult tr) {
	    numTestsDone++;
	    resultTable.update(tr);
	    // protect against removing observers during notification
	    Observer[] stableObservers = observers;
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].finishedTest(tr);

	    switch (tr.getStatus().getType()) {
		case Status.FAILED:
		    synchronized(this) {
			failCount++;
		    }
		    break;
		case Status.ERROR:
		    synchronized(this) {
			errCount++;
		    }
		    break;
		// XXX possibility exists for NOT_RUN, this should also
		//     be recorded as a problem, or is it a "problem"?
		default:
	    }	// switch
	}
	
	public void stoppingTestRun() {
	    // protect against removing observers during notification
	    Observer[] stableObservers = observers; 
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].stoppingTestRun();
	}

	public void finishedTesting() {
	    resultTable.finished();

	    // protect against removing observers during notification
	    Observer[] stableObservers = observers;
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].finishedTesting();
	}

	public void finishedTestRun(boolean allOK) {
	    // protect against removing observers during notification
	    Observer[] stableObservers = observers;
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].finishedTestRun(allOK);
	}

	public void error(String msg) {
	    // protect against removing observers during notification
	    Observer[] stableObservers = observers;
	    for (int i = stableObservers.length - 1; i >= 0; i--)
		stableObservers[i].error(msg);
	}

	synchronized int getErrorCount() {
	    return errCount;
	}

	synchronized int getFailedCount() {
	    return failCount;
	}

	private Observer[] observers = new Observer[0];
	private volatile int errCount, failCount;
    }


    class Autostop implements Harness.Observer {
	Autostop(int threshold) {
	    this.threshold = threshold;
	}
	
	public void startingTestRun(Parameters p) { }
	
	public void startingTest(TestResult tr) { }
	
	public void finishedTest(TestResult tr) {
	    switch (tr.getStatus().getType()) {
	    case Status.FAILED:
		level++;
		break;
	    case Status.ERROR:
		level += 5;
		break;
	    default:
		level = Math.max(level - 2, 0);
	    }
	    if (level >= threshold) {
		Harness.this.notifyError(i18n, "harness.tooManyErrors");
		stop();
	    }
	}
	public void stoppingTestRun() { }
	
	public void finishedTesting() { }
	
	public void finishedTestRun(boolean allOK) { }
	
	public void error(String msg) { }

	private int level;
	private int threshold;
    }

}
