Runner.java

/* *******************************************************************
 * Copyright (c) 1999-2001 Xerox Corporation,
 *               2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Xerox/PARC     initial implementation
 * ******************************************************************/


package org.aspectj.testing.run;

import java.util.Enumeration;
import java.util.Hashtable;

import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.Message;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.util.LangUtil;

/**
 * Run IRun, setting status and invoking listeners
 * for simple and nested runs.
 * <p>
 * This manages baseline IRun status reporting:
 * any throwables are caught and reported, and
 * the status is started before running and
 * (if not already completed) completed after.
 * <p>
 * This runs any IRunListeners specified directly in the
 * run*(..., IRunListener) methods
 * as well as any specified indirectly by registering listeners per-type
 * in {@link registerListener(Class, IRunListener)}
 * <p>
 * For correct handling of nested runs, this sets up
 * status parent/child relationships.
 * It uses the child result object supplied directly in the
 * runChild(..., IRunStatus childStatus,..) methods,
 * or (if that is null) one obtained from the child IRun itself,
 * or (if that is null) a generic IRunStatus.
 * <p>
 * For IRunIterator, this uses IteratorWrapper to wrap the
 * iterator as an IRun.  Runner and IteratorWrapper coordinate
 * to handle fast-fail (aborting further iteration when an IRun fails).
 * The IRunIterator itself may specify fast-fail by returning true
 * from {@link IRunIterator#abortOnFailure()}, or clients can
 * register IRunIterator by Object or type for fast-failure using
 * {@link registerFastFailIterator(IRunIterator)} or
 * {@link registerFastFailIterator(Class)}.
 * This also ensures that
 * {@link IRunIterator#iterationCompleted()} is
 * called after the iteration process has completed.
 */
public class Runner {
    // XXX need to consider wiring in a logger - sigh

    private static final IMessage FAIL_NORUN
        = MessageUtil.fail("Null IRun parameter to Runner.run(IRun..)");
//    private static final IMessage FAIL_NORUN_ITERATOR
//        = MessageUtil.fail("Null IRunterator parameter to Runner.run(IRunterator...)");

    public Runner() {
    }

    /**
     * Do the run, setting run status, invoking
     * listener, and aborting as necessary.
     * If the run is null, the status is
     * updated, but the listener is never run.
     * If the listener is null, then the runner does a lookup
     * for the listeners of this run type.
     * Any exceptions thrown by the listener(s) are added
     * to the status messages and processing continues.
     * unless the status is aborted.
     * The status will be completed when this method completes.
     * @param run the IRun to run - if null, issue a FAIL message
     * to that effect in the result.
     * @throws IllegalArgumentException if status is null
     * @return boolean result returned from IRun
     * or false if IRun did not complete normally
     * or status.runResult() if aborted.
     */
    /* XXX later permit null status
     * If the status is null, this tries to complete
     * the run without a status.  It ignores exceptions
     * from the listeners, but does not catch any from the run.
     */
    public boolean run(IRun run, IRunStatus status,
                             IRunListener listener) {
        return run(run, status, listener, (Class) null);
    }

    public boolean run(IRun run, IRunStatus status,
                             IRunListener listener, Class exceptionClass) {
        if (!precheck(run, status)) {
            return false;
        }
        RunListeners listeners = getListeners(run, listener);
        return runPrivate(run, status, listeners, exceptionClass);

    }

    /**
     * Run child of parent, handling interceptor registration, etc.
     * @throws IllegalArgumentException if parent or child status is null
     */
    public boolean runChild(IRun child,
                                 IRunStatus parentStatus,
                                 IRunStatus childStatus,
                                 IRunListener listener) {
        return runChild(child, parentStatus, childStatus, listener, null);
    }

    /**
     * Run child of parent, handling interceptor registration, etc.
     * If the child run is supposed to throw an exception, then pass
     * the exception class.
     * After this returns, the childStatus is guaranteed to be completed.
     * If an unexpected exception is thrown, an ABORT message
     * is passed to childStatus.
     * @param parentStatus the IRunStatus for the parent - must not be null
     * @param childStatus the IRunStatus for the child - default will be created if null
     * @param exceptionClass the Class of any expected exception
     * @throws IllegalArgumentException if parent status is null
     */
    public boolean runChild(IRun child,
                                 IRunStatus parentStatus,
                                 IRunStatus childStatus,
                                 IRunListener listener,
                                 Class exceptionClass) {
        if (!precheck(child, parentStatus)) {
            return false;
        }
        if (null == childStatus) {
            childStatus = new RunStatus(new MessageHandler(), this);
        }
        installChildStatus(child, parentStatus, childStatus);
        if (!precheck(child, childStatus)) {
            return false;
        }
        RunListeners listeners = getListeners(child, listener);
        if (null != listeners) {
            try {
                listeners.addingChild(parentStatus, childStatus);
            } catch (Throwable t) {
                String m = "RunListenerI.addingChild(..) exception " + listeners;
                parentStatus.handleMessage(MessageUtil.abort(m, t)); // XXX
            }
        }
        boolean result = false;
        try {
            result = runPrivate(child, childStatus, listeners, exceptionClass);
        } finally {
            if (!childStatus.isCompleted()) {
                childStatus.finish(result ? IRunStatus.PASS : IRunStatus.FAIL);
                childStatus.handleMessage(MessageUtil.debug("XXX parent runner set completion"));
            }
        }
        boolean childResult = childStatus.runResult();
        if (childResult != result) {
            childStatus.handleMessage(MessageUtil.info("childResult != result=" + result));
        }
        return childResult;
    }

    public IRunStatus makeChildStatus(IRun run, IRunStatus parent, IMessageHolder handler) {
        return installChildStatus(run, parent, new RunStatus(handler, this));
    }

    /**
     * Setup the child status before running
     * @param run the IRun of the child process (not null)
     * @param parent the IRunStatus parent of the child status (not null)
     * @param child the IRunStatus to install - if null, a generic one is created
     * @return the IRunStatus child status (as passed in or created if null)
     */
    public IRunStatus installChildStatus(IRun run, IRunStatus parent, IRunStatus child) {
        if (null == parent) {
            throw new IllegalArgumentException("null parent");
        }
        if (null == child) {
            child = new RunStatus(new MessageHandler(), this);
        }
        child.setIdentifier(run); // XXX leak if ditching run...
        parent.addChild(child);
        return child;
    }

    /**
     * Do a run by running all the subrunners provided by the iterator,
     * creating a new child status of status for each.
     * If the iterator is null, the result is
     * updated, but the interceptor is never run.
     * @param iterator the IRunteratorI for all the IRun to run
     * - if null, abort (if not completed) or message to status.
     * @throws IllegalArgumentException if status is null
     */
    public boolean runIterator(IRunIterator iterator, IRunStatus status,
                             IRunListener listener) {
        LangUtil.throwIaxIfNull(status, "status");
        if (status.aborted()) {
            return status.runResult();
        }
        if (null == iterator) {
            if (!status.isCompleted()) {
                status.abort(IRunStatus.ABORT_NORUN);
            } else {
                status.handleMessage(FAIL_NORUN);
            }
            return false;
        }
        IRun wrapped = wrap(iterator, listener);
        return run(wrapped, status, listener);
    }

    /**
     * Signal whether to abort on failure for this run and iterator,
     * based on the iterator and any fast-fail registrations.
     * @param iterator the IRunIterator to stop running if this returns true
     * @param run the IRun that failed
     * @return true to halt further iterations
     */
    private boolean abortOnFailure(IRunIterator iterator, IRun run) {
        return ((null == iterator) || iterator.abortOnFailure()); // XxX not complete
    }


    /**
     * Tell Runner to stop iterating over IRun for an IRunIterator
     * if any IRun.run(IRunStatus) fails.
     * This overrides a false result from IRunIterator.abortOnFailure().
     * @param iterator the IRunIterator to fast-fail - ignored if null.
     * @see IRunIterator#abortOnFailure()
     */
    public void registerFastFailIterator(IRunIterator iterator) { // XXX unimplemented
        throw new UnsupportedOperationException("ignoring " + iterator);
    }

    /**
     * Tell Runner to stop iterating over IRun for an IRunIterator
     * if any IRun.run(IRunStatus) fails,
     * if the IRunIterator is assignable to iteratorType.
     * This overrides a false result from IRunIterator.abortOnFailure().
     * @param iteratorType the IRunIterator Class to fast-fail
     *              - ignored if null, must be assignable to IRunIterator
     * @see IRunIterator#abortOnFailure()
     */
    public void registerFastFailIteratorTypes(Class iteratorType) { // XXX unimplemented
        throw new UnsupportedOperationException("ignoring " + iteratorType);
    }

    /**
     * Register a run listener for any run assignable to type.
     * @throws IllegalArgumentException if either is null
     */
    public void registerListener(Class type, IRunListener listener) { // XXX unregister
        if (null == type) {
            throw new IllegalArgumentException("null type");
        }
        if (null == listener) {
            throw new IllegalArgumentException("null listener");
        }
        ClassListeners.addListener(type, listener);
    }

    /**
     * Wrap this IRunIterator.
     * This wrapper takes care of calling
     * <code>iterator.iterationCompleted()</code> when done, so
     * after running this, clients should not invoker IRunIterator
     * methods on this iterator.
     * @return the iterator wrapped as a single IRun
     */
    public IRun wrap(IRunIterator iterator, IRunListener listener) {
        LangUtil.throwIaxIfNull(iterator, "iterator");
        return new IteratorWrapper(this, iterator, listener);
    }

    /**
     * This gets any listeners registered for the run
     * based on the class of the run (or of the wrapped iterator,
     * if the run is an IteratorWrapper).
     * @return a listener with all registered listener and parm,
     * or null if parm is null and there are no listeners
     */
    protected RunListeners getListeners(IRun run, IRunListener listener) {
        if (null == run) {
            throw new IllegalArgumentException("null run");
        }
        Class runClass = run.getClass();
        if (runClass == IteratorWrapper.class) {
            IRunIterator it = ((IteratorWrapper) run).iterator;
            if (null != it) {
                runClass = it.getClass();
            }
            // fyi expecting: listener == ((IteratorWrapper) run).listener
        }
        RunListeners listeners = ClassListeners.getListeners(runClass);
        if (null != listener) {
            listeners.addListener(listener);
        }
        return listeners; // XXX implement registration
    }

    /** check status and run before running */
    private boolean precheck(IRun run, IRunStatus status) {
        if (null == status) {
            throw new IllegalArgumentException("null status");
        }
        // check abort request coming in
        if (status.aborted()) {
            return status.runResult();
        } else if (status.isCompleted()) {
            throw new IllegalStateException("status completed before starting");
        }

        if (!status.started()) {
            status.start();
        }
        return true;
    }

    /** This assumes precheck has happened and listeners have been obtained */
    private boolean runPrivate(IRun run, IRunStatus status,
                             RunListeners listeners, Class exceptionClass) {
        IRunListener listener = listeners;
        if (null != listener) {
            try {
                listener.runStarting(status);
            } catch (Throwable t) {
                String m = listener + " RunListenerI.runStarting(..) exception";
                IMessage mssg = new Message(m, IMessage.WARNING, t, null);
                // XXX need IMessage.EXCEPTION - WARNING is ambiguous
                status.handleMessage(mssg);
            }
        }
        // listener can set abort request
        if (status.aborted()) {
            return status.runResult();
        }
        if (null == run) {
            if (!status.isCompleted()) {
                status.abort(IRunStatus.ABORT_NORUN);
            } else {
                status.handleMessage(MessageUtil.FAIL_INCOMPLETE);
            }
            return false;
        }

        boolean result = false;
        try {
            result = run.run(status);
            if (!status.isCompleted()) {
                status.finish(result?IRunStatus.PASS: IRunStatus.FAIL);
            }
        } catch (Throwable thrown) {
            if (!status.isCompleted()) {
                status.thrown(thrown);
            } else {
                String m = "run status completed but run threw exception";
                status.handleMessage(MessageUtil.abort(m, thrown));
                result = false;
            }
        } finally {
            if (!status.isCompleted()) {
                // XXX should never get here... - hides errors to set result
                status.finish(result ? IRunStatus.PASS : IRunStatus.FAIL);
                if (!status.isCompleted()) {
                    status.handleMessage(MessageUtil.debug("child set of status failed"));
                }
            }
        }


        try {
            if ((null != listener) && !status.aborted()) {
                listener.runCompleted(status);
            }
        } catch (Throwable t) {
            String m = listener + " RunListenerI.runCompleted(..) exception";
            status.handleMessage(MessageUtil.abort(m, t));
        }
        return result;
    }

    //---------------------------------- nested classes
    /**
     * Wrap an IRunIterator as a IRun, coordinating
     * fast-fail IRunIterator and Runner
     */
    public static class IteratorWrapper implements IRun {
        final Runner runner;
        public final IRunIterator iterator;
        final IRunListener listener;

        public IteratorWrapper(Runner runner, IRunIterator iterator, IRunListener listener) {
            LangUtil.throwIaxIfNull(iterator, "iterator");
            LangUtil.throwIaxIfNull(runner, "runner");
            this.runner = runner;
            this.iterator = iterator;
            this.listener = listener;
        }

        /** @return null */
        public IRunStatus makeStatus() {
            return null;
        }

        /** @return true unless one failed */
        public boolean run(IRunStatus status) {
            boolean result = true;
            try {
                int i = 0;
                int numMessages = status.numMessages(IMessage.FAIL, IMessageHolder.ORGREATER);
                while (iterator.hasNextRun()) {
                    IRun run = iterator.nextRun((IMessageHandler) status, runner);
                    if (null == run) {
                        MessageUtil.debug(status,  "null run " + i + " from " + iterator);
                        continue;
                    }

                    int newMessages = status.numMessages(IMessage.FAIL, IMessageHolder.ORGREATER);
                    if (newMessages > numMessages) {
                        numMessages = newMessages;
                        String m = "run " + i + " from " + iterator
                            + " not invoked, due to fail(+) message(s) ";
                        MessageUtil.debug(status,  m);
                        continue;
                    }
                    RunStatus childStatus = null; // let runChild create
                    if (!runner.runChild(run, status, childStatus, listener)) {
                        if (result) {
                            result = false;
                        }
                        if (iterator.abortOnFailure()
                            || runner.abortOnFailure(iterator, run)) {
                            break;
                        }
                    }
                    i++;
                }
                return result;
            } finally {
                iterator.iterationCompleted();
            }
        }

        /** @return iterator, clipped to 75 char */
        public String toString() {
            String s = "" + iterator;
            if (s.length() > 75) {
                s = s.substring(0, 72) + "...";
            }
            return s;
        }
    }

    /** per-class collection of IRun */
    static class ClassListeners extends RunListeners {
        private static final Hashtable known = new Hashtable();

        static RunListeners getListeners(Class c) { // XXX costly and stupid
            Enumeration keys = known.keys();
            RunListeners many = new RunListeners();
            while (keys.hasMoreElements()) {
                Class kc = (Class) keys.nextElement();
                if (kc.isAssignableFrom(c)) {
                    many.addListener((IRunListener) known.get(kc));
                }
            }
            return many;
        }

        private static RunListeners makeListeners(Class c) {
            RunListeners result = (ClassListeners) known.get(c);
            if (null == result) {
                result = new ClassListeners(c);
                known.put(c, result);
            }
            return result;
        }

        static void addListener(Class c, IRunListener listener) {
            if (null == listener) {
                throw new IllegalArgumentException("null listener");
            }
            if (null == c) {
                c = IRun.class;
            }
            makeListeners(c).addListener(listener);
        }

        Class clazz;

        ClassListeners(Class clazz) {
            this.clazz = clazz;
        }

        public String toString() {
            return clazz.getName() + " ClassListeners: " + super.toString();
        }
    }
}