package abbot.script; import java.util.*; import java.io.File; import javax.swing.SwingUtilities; import abbot.*; import abbot.finder.Hierarchy; import abbot.finder.TestHierarchy; import abbot.i18n.Strings; import abbot.util.*; /** Provides control and tracking of the execution of a step or series of steps. By default the runner stops execution on the first encountered failure/error. The running environment is preserved to the extent possible, which includes discarding any GUI components created by the code under test.<p> If you wish to preserve the application state when there is an error, you can use the method {@link #setTerminateOnError(boolean)}. */ public class StepRunner { private static UIContext currentContext = null; private boolean stopOnFailure = true; private boolean stopOnError = true; /** Whether to terminate the app after an error/failure. */ private boolean terminateOnError = true; /** Whether to terminate the app after stopping. */ private transient boolean terminateOnStop = false; private ArrayList listeners = new ArrayList(); private Map errors = new HashMap(); /** Whether to stop running. */ private transient boolean stop = false; /** Use this to catch event dispatch exceptions. */ private EDTExceptionCatcher catcher; protected AWTFixtureHelper helper; protected Hierarchy hierarchy; /** This ctor uses a new instance of TestHierarchy as the * default Hierarchy. Note that any existing GUI components at the time * of this object's creation will be ignored. */ public StepRunner() { this(new AWTFixtureHelper()); } /** Create a new runner. The given {@link Hierarchy} maintains which GUI * components are in or out of scope of the runner. The {@link AWTFixtureHelper} * will be used to restore state if {@link #terminate()} is called. */ public StepRunner(AWTFixtureHelper helper) { this.helper = helper; this.catcher = new EDTExceptionCatcher(); catcher.install(); hierarchy = new TestHierarchy(); } /** * @return The designated hierarchy for this <code>StepRunner</code>, * or <code>null</code> if none. */ public Hierarchy getHierarchy() { Hierarchy h = currentContext != null && currentContext.isLaunched() ? currentContext.getHierarchy() : hierarchy; return h; } public UIContext getCurrentContext() { return currentContext; } public void setStopOnFailure(boolean stop) { stopOnFailure = stop; } public void setStopOnError(boolean stop) { stopOnError = stop; } public boolean getStopOnFailure() { return stopOnFailure; } public boolean getStopOnError() { return stopOnError; } /** Stop execution of the script after the current step completes. The * launched application will be left in its current state. */ public void stop() { stop(false); } /** Stop execution, indicating whether to terminate the app. */ public void stop(boolean terminate) { stop = true; terminateOnStop = terminate; } /** Return whether the runner has been stopped. */ public boolean stopped() { return stop; } /** Create a security manager to use for the duration of this runner's execution. The default prevents invoked applications from invoking {@link System#exit(int)} and invokes {@link #terminate()} instead. */ protected SecurityManager createSecurityManager() { return new ExitHandler(); } /** Install a security manager to ensure we prevent the AUT from exiting and can clean up when it tries to. */ protected synchronized void installSecurityManager() { String doInstall = System.getProperty("abbot.use_security_manager"); if (System.getSecurityManager() == null && !"false".equals(doInstall)) { // When the application tries to exit, throw control back to the // step runner to dispose of it Log.debug("Installing sm"); System.setSecurityManager(createSecurityManager()); } } protected synchronized void removeSecurityManager() { if (System.getSecurityManager() instanceof ExitHandler) { System.setSecurityManager(null); } } /** If the given context is not the current one, terminate the current one * and set this one as current. */ private void updateContext(UIContext context) { if (!context.equivalent(currentContext)) { Log.debug("current=" + currentContext + ", new=" + context); if (currentContext != null) currentContext.terminate(); currentContext = context; } } /** Run the given step, propagating any failures or errors to * listeners. This method should be used for any execution * that should be treated as a single logical action. * This method is primarily used to execute a script, but may * be used in other circumstances to execute one or more steps * in isolation. * The {@link #terminate()} method will be invoked if the script is * stopped for any reason, unless {@link #setTerminateOnError(boolean)} * has been called with a <code>false</code> argument. Otherwise * {@link #terminate()} will only be called if a * {@link Terminate} step is encountered. * @see #terminate() */ public void run(Step step) throws Throwable { if (SwingUtilities.isEventDispatchThread()) { throw new Error(Strings.get("runner.bad_invocation")); } // Terminate incorrect contexts prior to doing any setup. // Even though a UIContext will invoke terminate on a // non-equivalent context, we need to make it happen // before anything gets run. UIContext context = null; if (step instanceof Script) { context = step instanceof UIContext ? (UIContext)step : ((Script)step).getUIContext(); } else if (step instanceof UIContext) { context = (UIContext)step; } if (context != null) updateContext(context); installSecurityManager(); boolean completed = false; clearErrors(); try { if ((step instanceof Script) && ((Script)step).isForked()) { Log.debug("Forking " + step); StepRunner runner = new ForkedStepRunner(this); runner.listeners.addAll(listeners); try { runner.runStep(step); } finally { errors.putAll(runner.errors); } } else { runStep(step); } completed = !stopped(); } catch(ExitException ee) { // application tried to exit Log.debug("App tried to exit"); terminate(); } finally { if (step instanceof Script) { if (completed && errors.size() == 0) { // Script was run successfully } else if (stopped() && terminateOnStop) { terminate(); } } removeSecurityManager(); } } /** Set whether the application under test should be terminated when an error is encountered and script execution stopped. The default implementation always terminates. */ public void setTerminateOnError(boolean state) { terminateOnError = state; } public boolean getTerminateOnError() { return terminateOnError; } protected void clearErrors() { stop = false; errors.clear(); } /** Throw an exception if the file does not exist. */ protected void checkFile(Script script) throws InvalidScriptException { File file = script.getFile(); if (!file.exists() && !file.getName().startsWith(Script.UNTITLED_FILE)) { String msg = "The script '" + script.getFilename() + "' does not exist at the expected location '" + file.getAbsolutePath() + "'"; throw new InvalidScriptException(msg); } } /** Main run method, which stores any failures or exceptions for later * retrieval. Any step will fire STEP_START events to all registered * {@link StepListener}s on starting, and exactly one * of STEP_END, STEP_FAILURE, or STEP_ERROR upon termination. If * stopOnFailure/stopOnError is set false, then both STEP_FAILURE/ERROR * may be sent in addition to STEP_END. */ protected void runStep(final Step step) throws Throwable { if (step instanceof Script) { checkFile((Script)step); ((Script)step).setHierarchy(getHierarchy()); } Log.debug("Running " + step); fireStepStart(step); // checking for stopped here allows a listener to stop execution on a // particular step in response to its "start" event. if (stopped()) { Log.debug("Already stopped, skipping " + step); } else { Throwable exception = null; long exceptionTime = -1; try { if (step instanceof Launch) { ((Launch)step).setThreadedLaunchListener(new LaunchListener()); } // Recurse into sequences if (step instanceof Sequence) { ((Sequence)step).runStep(this); } else { step.run(); } Log.debug("Finished " + step); if (step instanceof Terminate) { terminate(); } } catch(Throwable e) { exceptionTime = System.currentTimeMillis(); exception = e; } finally { // Cf. ComponentTestFixture.runBare() // Any EDT exception which occurred *prior* to when the // exception on the main thread was thrown should be used // instead. long edtExceptionTime = catcher.getThrowableTime(); Throwable edtException = catcher.getThrowable(); if (edtException != null && (exception == null || edtExceptionTime < exceptionTime)) { exception = edtException; } } if (exception != null) { if (exception instanceof AssertionFailedError) { Log.debug("failure in " + step + ": " + exception); fireStepFailure(step, exception); if (stopOnFailure) { stop(terminateOnError); throw exception; } } else { Log.debug("error in " + step + ": " + exception); fireStepError(step, exception); if (stopOnError) { stop(terminateOnError); throw exception; } } } fireStepEnd(step); } } /** Similar to {@link #run(Step)}, but defers to the {@link Script} * to determine what subset of steps should be run as the UI context. * @param step */ public void launch(Script step) throws Throwable { UIContext ctxt = step.getUIContext(); if (ctxt != null) { ctxt.launch(this); } } /** Dispose of any extant windows and restore any saved environment * state. */ public void terminate() { // Allow the context to do specialized cleanup if (currentContext != null) { currentContext.terminate(); } if (helper != null) { Log.debug("restoring UI state"); helper.restore(); } } protected void setError(Step step, Throwable thr) { if (thr != null) errors.put(step, thr); else errors.remove(step); } public Throwable getError(Step step) { return (Throwable)errors.get(step); } public void addStepListener(StepListener sl) { synchronized(listeners) { listeners.add(sl); } } public void removeStepListener(StepListener sl) { synchronized(listeners) { listeners.remove(sl); } } /** If this is used to propagate a failure/error, be sure to invoke * setError on the step first. */ protected void fireStepEvent(StepEvent event) { Iterator iter; synchronized(listeners) { iter = ((ArrayList)listeners.clone()).iterator(); } while (iter.hasNext()) { StepListener sl = (StepListener)iter.next(); sl.stateChanged(event); } } private void fireStepEvent(Step step, String type, int val, Throwable throwable) { synchronized(listeners) { if (listeners.size() != 0) { StepEvent event = new StepEvent(step, type, val, throwable); fireStepEvent(event); } } } protected void fireStepStart(Step step) { fireStepEvent(step, StepEvent.STEP_START, 0, null); } protected void fireStepProgress(Step step, int val) { fireStepEvent(step, StepEvent.STEP_PROGRESS, val, null); } protected void fireStepEnd(Step step) { fireStepEvent(step, StepEvent.STEP_END, 0, null); } protected void fireStepFailure(Step step, Throwable afe) { setError(step, afe); fireStepEvent(step, StepEvent.STEP_FAILURE, 0, afe); } protected void fireStepError(Step step, Throwable thr) { setError(step, thr); fireStepEvent(step, StepEvent.STEP_ERROR, 0, thr); } private class LaunchListener implements Launch.ThreadedLaunchListener { public void stepFailure(Launch step, AssertionFailedError afe) { fireStepFailure(step, afe); if (stopOnFailure) stop(terminateOnError); } public void stepError(Launch step, Throwable thr) { fireStepError(step, thr); if (stopOnError) stop(terminateOnError); } } protected class ExitHandler extends NoExitSecurityManager { public void checkRead(String file) { // avoid annoying drive a: bug on w32 VM } protected void exitCalled(int status) { Log.debug("Terminating from security manager"); terminate(); } } }