001: package abbot.script;
002:
003: import java.util.*;
004: import java.io.File;
005:
006: import javax.swing.SwingUtilities;
007:
008: import abbot.*;
009: import abbot.finder.Hierarchy;
010: import abbot.finder.TestHierarchy;
011: import abbot.i18n.Strings;
012: import abbot.util.*;
013:
014: /** Provides control and tracking of the execution of a step or series of
015: steps. By default the runner stops execution on the first encountered
016: failure/error. The running environment is preserved to the extent
017: possible, which includes discarding any GUI components created by the
018: code under test.<p>
019: If you wish to preserve the application state when there is an error,
020: you can use the method {@link #setTerminateOnError(boolean)}.
021: */
022: public class StepRunner {
023:
024: private static UIContext currentContext = null;
025:
026: private boolean stopOnFailure = true;
027: private boolean stopOnError = true;
028: /** Whether to terminate the app after an error/failure. */
029: private boolean terminateOnError = true;
030: /** Whether to terminate the app after stopping. */
031: private transient boolean terminateOnStop = false;
032: private ArrayList listeners = new ArrayList();
033: private Map errors = new HashMap();
034: /** Whether to stop running. */
035: private transient boolean stop = false;
036: /** Use this to catch event dispatch exceptions. */
037: private EDTExceptionCatcher catcher;
038: protected AWTFixtureHelper helper;
039: protected Hierarchy hierarchy;
040:
041: /** This ctor uses a new instance of TestHierarchy as the
042: * default Hierarchy. Note that any existing GUI components at the time
043: * of this object's creation will be ignored.
044: */
045: public StepRunner() {
046: this (new AWTFixtureHelper());
047: }
048:
049: /** Create a new runner. The given {@link Hierarchy} maintains which GUI
050: * components are in or out of scope of the runner. The {@link AWTFixtureHelper}
051: * will be used to restore state if {@link #terminate()} is called.
052: */
053: public StepRunner(AWTFixtureHelper helper) {
054: this .helper = helper;
055: this .catcher = new EDTExceptionCatcher();
056: catcher.install();
057: hierarchy = helper.getHierarchy();
058: }
059:
060: /**
061: * @return The designated hierarchy for this <code>StepRunner</code>,
062: * or <code>null</code> if none.
063: */
064: public Hierarchy getHierarchy() {
065: Hierarchy h = currentContext != null
066: && currentContext.isLaunched() ? currentContext
067: .getHierarchy() : hierarchy;
068: return h;
069: }
070:
071: public UIContext getCurrentContext() {
072: return currentContext;
073: }
074:
075: public void setStopOnFailure(boolean stop) {
076: stopOnFailure = stop;
077: }
078:
079: public void setStopOnError(boolean stop) {
080: stopOnError = stop;
081: }
082:
083: public boolean getStopOnFailure() {
084: return stopOnFailure;
085: }
086:
087: public boolean getStopOnError() {
088: return stopOnError;
089: }
090:
091: /** Stop execution of the script after the current step completes. The
092: * launched application will be left in its current state.
093: */
094: public void stop() {
095: stop(false);
096: }
097:
098: /** Stop execution, indicating whether to terminate the app. */
099: public void stop(boolean terminate) {
100: stop = true;
101: terminateOnStop = terminate;
102: }
103:
104: /** Return whether the runner has been stopped. */
105: public boolean stopped() {
106: return stop;
107: }
108:
109: /** Create a security manager to use for the duration of this runner's
110: execution. The default prevents invoked applications from
111: invoking {@link System#exit(int)} and invokes {@link #terminate()}
112: instead.
113: */
114: protected SecurityManager createSecurityManager() {
115: return new ExitHandler();
116: }
117:
118: /** Install a security manager to ensure we prevent the AUT from
119: exiting and can clean up when it tries to.
120: */
121: protected synchronized void installSecurityManager() {
122: if (System.getSecurityManager() == null
123: && !Boolean.getBoolean("abbot.no_security_manager")) {
124: // When the application tries to exit, throw control back to the
125: // step runner to dispose of it
126: Log.debug("Installing sm");
127: System.setSecurityManager(createSecurityManager());
128: }
129: }
130:
131: protected synchronized void removeSecurityManager() {
132: if (System.getSecurityManager() instanceof ExitHandler) {
133: System.setSecurityManager(null);
134: }
135: }
136:
137: /** If the given context is not the current one, terminate the current one
138: * and set this one as current.
139: */
140: private void updateContext(UIContext context) {
141: if (!context.equivalent(currentContext)) {
142: Log.debug("current=" + currentContext + ", new=" + context);
143: if (currentContext != null)
144: currentContext.terminate();
145: currentContext = context;
146: }
147: }
148:
149: /** Run the given step, propagating any failures or errors to
150: * listeners. This method should be used for any execution
151: * that should be treated as a single logical action.
152: * This method is primarily used to execute a script, but may
153: * be used in other circumstances to execute one or more steps
154: * in isolation.
155: * The {@link #terminate()} method will be invoked if the script is
156: * stopped for any reason, unless {@link #setTerminateOnError(boolean)}
157: * has been called with a <code>false</code> argument. Otherwise
158: * {@link #terminate()} will only be called if a
159: * {@link Terminate} step is encountered.
160: * @see #terminate()
161: */
162: public void run(Step step) throws Throwable {
163: if (SwingUtilities.isEventDispatchThread()) {
164: throw new Error(Strings.get("runner.bad_invocation"));
165: }
166: // Always clear locking keys before a test, to ensure we have
167: // a consistent state
168: SystemState.clearLockingKeys();
169:
170: // Terminate incorrect contexts prior to doing any setup.
171: // Even though a UIContext will invoke terminate on a
172: // non-equivalent context, we need to make it happen
173: // before anything gets run.
174: UIContext context = null;
175: if (step instanceof Script) {
176: context = step instanceof UIContext ? (UIContext) step
177: : ((Script) step).getUIContext();
178: } else if (step instanceof UIContext) {
179: context = (UIContext) step;
180: }
181: if (context != null) {
182: updateContext(context);
183: }
184:
185: installSecurityManager();
186: boolean completed = false;
187: clearErrors();
188:
189: try {
190: if ((step instanceof Script) && ((Script) step).isForked()) {
191: Log.debug("Forking " + step);
192: StepRunner runner = new ForkedStepRunner(this );
193: runner.listeners.addAll(listeners);
194: try {
195: runner.runStep(step);
196: } finally {
197: errors.putAll(runner.errors);
198: }
199: } else {
200: runStep(step);
201: }
202: completed = !stopped();
203: } catch (ExitException ee) {
204: // application tried to exit
205: Log.debug("App tried to exit");
206: terminate();
207: } finally {
208: if (step instanceof Script) {
209: if (completed && errors.size() == 0) {
210: // Script was run successfully
211: } else if (stopped() && terminateOnStop) {
212: terminate();
213: }
214: }
215: removeSecurityManager();
216: }
217: }
218:
219: /** Set whether the application under test should be terminated when an
220: error is encountered and script execution stopped. The default
221: implementation always terminates.
222: */
223: public void setTerminateOnError(boolean state) {
224: terminateOnError = state;
225: }
226:
227: public boolean getTerminateOnError() {
228: return terminateOnError;
229: }
230:
231: protected void clearErrors() {
232: stop = false;
233: errors.clear();
234: }
235:
236: /** Throw an exception if the file does not exist. */
237: protected void checkFile(Script script)
238: throws InvalidScriptException {
239: File file = script.getFile();
240: if (!file.exists()
241: && !file.getName().startsWith(Script.UNTITLED_FILE)) {
242: String msg = "The script '" + script.getFilename()
243: + "' does not exist at the expected location '"
244: + file.getAbsolutePath() + "'";
245: throw new InvalidScriptException(msg);
246: }
247: }
248:
249: /** Main run method, which stores any failures or exceptions for later
250: * retrieval. Any step will fire STEP_START events to all registered
251: * {@link StepListener}s on starting, and exactly one
252: * of STEP_END, STEP_FAILURE, or STEP_ERROR upon termination. If
253: * stopOnFailure/stopOnError is set false, then both STEP_FAILURE/ERROR
254: * may be sent in addition to STEP_END.
255: */
256: protected void runStep(final Step step) throws Throwable {
257:
258: if (step instanceof Script) {
259: checkFile((Script) step);
260: ((Script) step).setHierarchy(getHierarchy());
261: }
262:
263: Log.debug("Running " + step);
264: fireStepStart(step);
265:
266: // checking for stopped here allows a listener to stop execution on a
267: // particular step in response to its "start" event.
268: if (stopped()) {
269: Log.debug("Already stopped, skipping " + step);
270: } else {
271: Throwable exception = null;
272: long exceptionTime = -1;
273: try {
274: if (step instanceof Launch) {
275: ((Launch) step)
276: .setThreadedLaunchListener(new LaunchListener());
277: }
278: // Recurse into sequences
279: if (step instanceof Sequence) {
280: ((Sequence) step).runStep(this );
281: } else {
282: step.run();
283: }
284: Log.debug("Finished " + step);
285: if (step instanceof Terminate) {
286: terminate();
287: }
288: } catch (Throwable e) {
289: exceptionTime = System.currentTimeMillis();
290: exception = e;
291: } finally {
292: // Cf. ComponentTestFixture.runBare()
293: // Any EDT exception which occurred *prior* to when the
294: // exception on the main thread was thrown should be used
295: // instead.
296: long edtExceptionTime = EDTExceptionCatcher
297: .getThrowableTime();
298: Throwable edtException = EDTExceptionCatcher
299: .getThrowable();
300: if (edtException != null
301: && (exception == null || edtExceptionTime < exceptionTime)) {
302: exception = edtException;
303: }
304: }
305: if (exception != null) {
306: if (exception instanceof junit.framework.AssertionFailedError) {
307: Log.debug("failure in " + step + ": " + exception);
308: fireStepFailure(step, exception);
309: if (stopOnFailure) {
310: stop(terminateOnError);
311: throw exception;
312: }
313: } else {
314: Log.debug("error in " + step + ": " + exception);
315: fireStepError(step, exception);
316: if (stopOnError) {
317: stop(terminateOnError);
318: throw exception;
319: }
320: }
321: }
322:
323: fireStepEnd(step);
324: }
325: }
326:
327: /** Similar to {@link #run(Step)}, but defers to the {@link Script}
328: * to determine what subset of steps should be run as the UI context.
329: * @param step
330: */
331: public void launch(Script step) throws Throwable {
332: UIContext ctxt = step.getUIContext();
333: if (ctxt != null) {
334: ctxt.launch(this );
335: }
336: }
337:
338: /** Dispose of any extant windows and restore any saved environment
339: * state.
340: */
341: public void terminate() {
342: // Allow the context to do specialized cleanup
343: if (currentContext != null) {
344: currentContext.terminate();
345: }
346: if (helper != null) {
347: Log.debug("restoring UI state");
348: helper.restore();
349: }
350: }
351:
352: protected void setError(Step step, Throwable thr) {
353: if (thr != null)
354: errors.put(step, thr);
355: else
356: errors.remove(step);
357: }
358:
359: public Throwable getError(Step step) {
360: return (Throwable) errors.get(step);
361: }
362:
363: public void addStepListener(StepListener sl) {
364: synchronized (listeners) {
365: listeners.add(sl);
366: }
367: }
368:
369: public void removeStepListener(StepListener sl) {
370: synchronized (listeners) {
371: listeners.remove(sl);
372: }
373: }
374:
375: /** If this is used to propagate a failure/error, be sure to invoke
376: * setError on the step first.
377: */
378: protected void fireStepEvent(StepEvent event) {
379: Iterator iter;
380: synchronized (listeners) {
381: iter = ((ArrayList) listeners.clone()).iterator();
382: }
383: while (iter.hasNext()) {
384: StepListener sl = (StepListener) iter.next();
385: sl.stateChanged(event);
386: }
387: }
388:
389: private void fireStepEvent(Step step, String type, int val,
390: Throwable throwable) {
391: synchronized (listeners) {
392: if (listeners.size() != 0) {
393: StepEvent event = new StepEvent(step, type, val,
394: throwable);
395: fireStepEvent(event);
396: }
397: }
398: }
399:
400: protected void fireStepStart(Step step) {
401: fireStepEvent(step, StepEvent.STEP_START, 0, null);
402: }
403:
404: protected void fireStepProgress(Step step, int val) {
405: fireStepEvent(step, StepEvent.STEP_PROGRESS, val, null);
406: }
407:
408: protected void fireStepEnd(Step step) {
409: fireStepEvent(step, StepEvent.STEP_END, 0, null);
410: }
411:
412: protected void fireStepFailure(Step step, Throwable afe) {
413: setError(step, afe);
414: fireStepEvent(step, StepEvent.STEP_FAILURE, 0, afe);
415: }
416:
417: protected void fireStepError(Step step, Throwable thr) {
418: setError(step, thr);
419: fireStepEvent(step, StepEvent.STEP_ERROR, 0, thr);
420: }
421:
422: private class LaunchListener implements
423: Launch.ThreadedLaunchListener {
424: public void stepFailure(Launch step, AssertionFailedError afe) {
425: fireStepFailure(step, afe);
426: if (stopOnFailure)
427: stop(terminateOnError);
428: }
429:
430: public void stepError(Launch step, Throwable thr) {
431: fireStepError(step, thr);
432: if (stopOnError)
433: stop(terminateOnError);
434: }
435: }
436:
437: protected class ExitHandler extends NoExitSecurityManager {
438: public void checkRead(String file) {
439: // avoid annoying drive a: bug on w32 VM
440: }
441:
442: protected void exitCalled(int status) {
443: Log.debug("Terminating from security manager");
444: terminate();
445: }
446: }
447: }
|