001: package abbot.script;
002:
003: import java.awt.Window;
004: import java.lang.reflect.*;
005: import java.util.Map;
006: import java.util.Iterator;
007:
008: import abbot.finder.Hierarchy;
009: import abbot.*;
010: import abbot.i18n.Strings;
011:
012: /**
013: * Provides scripted static method invocation. Usage:<br>
014: * <blockquote><code>
015: * <launch class="package.class" method="methodName" args="..."
016: * classpath="..." [threaded=true]><br>
017: * </code></blockquote><p>
018: * The args attribute is a comma-separated list of arguments to pass to the
019: * class method, and may use square brackets to denote an array,
020: * e.g. "[one,two,three]" will be interpreted as an array length 3
021: * of String. The square brackets may be escaped ('\[' or '\]') to include
022: * them literally in an argument.
023: * <p>
024: * The class path attribute may use either colon or semicolon as a path
025: * separator, but should preferably use relative paths to avoid making the
026: * containing script platform- and location-dependent.<p>
027: * In most cases, the classes under test will <i>only</i> be found under the
028: * custom class path, and so the parent class loader will fail to find them.
029: * If this is the case then the classes under test will be properly discarded
030: * on each launch when a new class loader is created.
031: * <p>
032: * The 'threaded' attribute is provided in case your code under test requires
033: * GUI event processing prior to returning from its invoked method. An
034: * example might be a main method which invokes dialog and waits for the
035: * response before continuing. In general, it's better to refactor the code
036: * if possible so that the main method turns over control to the event
037: * dispatch thread as soon as possible. Otherwise, if the application under
038: * test is background threaded by the Launch step, any runtime exceptions
039: * thrown from the launch code will cause errors in the launch step out of
040: * sequence with the other script steps. While this won't cause any problems
041: * for the Abbot framework, it can be very confusing for the user.<p>
042: * Note that if the "reload" attribute is set true (i.e. Abbot's class loader
043: * is used to reload code under test), ComponentTester extensions must also be
044: * loaded by that class loader, so the path to extensions should be included
045: * in the Launch class path.<p>
046: */
047: public class Launch extends Call implements UIContext {
048: /** Allow only one active launch at a time. */
049: private static Launch currentLaunch = null;
050:
051: private String classpath = null;
052: private boolean threaded = false;
053: private transient AppClassLoader classLoader;
054: private transient ThreadedLaunchListener listener;
055:
056: private static final String USAGE = "<launch class=\"...\" method=\"...\" args=\"...\" "
057: + "[threaded=true]>";
058:
059: public Launch(Resolver resolver, Map attributes) {
060: super (resolver, attributes);
061: classpath = (String) attributes.get(TAG_CLASSPATH);
062: String thr = (String) attributes.get(TAG_THREADED);
063: if (thr != null)
064: threaded = Boolean.valueOf(thr).booleanValue();
065: }
066:
067: public Launch(Resolver resolver, String description,
068: String className, String methodName, String[] args) {
069: this (resolver, description, className, methodName, args, null,
070: false);
071: }
072:
073: public Launch(Resolver resolver, String description,
074: String className, String methodName, String[] args,
075: String classpath, boolean threaded) {
076: super (resolver, description, className, methodName, args);
077: this .classpath = classpath;
078: this .threaded = threaded;
079: }
080:
081: public String getClasspath() {
082: return classpath;
083: }
084:
085: public void setClasspath(String cp) {
086: classpath = cp;
087: // invalidate class loader
088: classLoader = null;
089: }
090:
091: public boolean isThreaded() {
092: return threaded;
093: }
094:
095: public void setThreaded(boolean thread) {
096: threaded = thread;
097: }
098:
099: protected AppClassLoader createClassLoader() {
100: return new AppClassLoader(classpath);
101: }
102:
103: /** Install the class loader context for the code being launched. The
104: * context class loader for the current thread is modified.
105: */
106: protected void install() {
107: ClassLoader loader = getContextClassLoader();
108: // Everything else loaded on the same thread as this
109: // launch should be loaded by this custom loader.
110: if (loader instanceof AppClassLoader
111: && !((AppClassLoader) loader).isInstalled()) {
112: ((AppClassLoader) loader).install();
113: }
114: }
115:
116: protected void synchronizedRunStep() throws Throwable {
117: // A bug in pre-1.4 VMs locks the toolkit prior to notifying AWT event
118: // listeners. This causes a deadlock when the main method invokes
119: // "show" on a component which triggers AWT events for which there are
120: // listeners. To avoid this, grab the toolkit lock first so that the
121: // locks are acquired in the same order by either sequence.
122: // (Unfortunately, some swing code locks the tree prior to
123: // grabbing the toolkit lock, so there's still opportunity for
124: // deadlock). One alternative (although very heavyweight) is to
125: // always fork a separate VM.
126: //
127: // If threaded, take the danger of deadlock over the possibility that
128: // the main method will never return and leave the lock forever held.
129: // NOTE: this is guaranteed to deadlock if "main" calls
130: // EventQueue.invokeAndWait.
131: if (Platform.JAVA_VERSION < Platform.JAVA_1_4 && !isThreaded()) {
132: synchronized (java.awt.Toolkit.getDefaultToolkit()) {
133: super .runStep();
134: }
135: } else {
136: super .runStep();
137: }
138: }
139:
140: /** Perform steps necessary to remove any setup performed by
141: * this <code>Launch</code> step.
142: */
143: public void terminate() {
144: Log.debug("launch terminate");
145: if (currentLaunch == this ) {
146: // Nothing special to do, dispose windows normally
147: Iterator iter = getHierarchy().getRoots().iterator();
148: while (iter.hasNext())
149: getHierarchy().dispose((Window) iter.next());
150: if (classLoader != null) {
151: classLoader.uninstall();
152: classLoader = null;
153: }
154: currentLaunch = null;
155: }
156: }
157:
158: /** Launches the UI described by this <code>Launch</code> step,
159: * using the given runner as controller/monitor.
160: */
161: public void launch(StepRunner runner) throws Throwable {
162: runner.run(this );
163: }
164:
165: /** @return Whether the code described by this launch step is currently active. */
166: public boolean isLaunched() {
167: return currentLaunch == this ;
168: }
169:
170: public Hierarchy getHierarchy() {
171: return getResolver().getHierarchy();
172: }
173:
174: public void runStep() throws Throwable {
175: if (currentLaunch != null)
176: currentLaunch.terminate();
177: currentLaunch = this ;
178: install();
179: System.setProperty("abbot.framework.launched", "true");
180: if (isThreaded()) {
181: Thread threaded = new Thread("Threaded " + toString()) {
182: public void run() {
183: try {
184: synchronizedRunStep();
185: } catch (AssertionFailedError e) {
186: if (listener != null)
187: listener.stepFailure(Launch.this , e);
188: } catch (Throwable t) {
189: if (listener != null)
190: listener.stepError(Launch.this , t);
191: }
192: }
193: };
194: threaded.setDaemon(true);
195: threaded.setContextClassLoader(classLoader);
196: threaded.start();
197: } else {
198: synchronizedRunStep();
199: }
200: }
201:
202: /** Overrides the default implementation to always use the class loader
203: * defined by this step. This works in cases where the Launch step has
204: * not yet been added to a Script; otherwise the Script will provide an
205: * implementation equivalent to this one.
206: */
207: public Class resolveClass(String className)
208: throws ClassNotFoundException {
209: return Class.forName(className, true, getContextClassLoader());
210: }
211:
212: /** Return the class loader that uses the classpath defined in this
213: * step.
214: */
215: public ClassLoader getContextClassLoader() {
216: if (classLoader == null) {
217: // Use a custom class loader so that we can provide additional
218: // classpath and also optionally reload the class on each run.
219: // FIXME maybe classpath should be relative to the script? In this
220: // case, it's relative to user.dir
221: classLoader = createClassLoader();
222: }
223: return classLoader;
224: }
225:
226: public Class getTargetClass() throws ClassNotFoundException {
227: Class cls = resolveClass(getTargetClassName());
228: Log.debug("Target class is " + cls.getName());
229: return cls;
230: }
231:
232: /** Return the target for the method invocation. All launch invocations
233: * must be static, so this always returns null.
234: */
235: protected Object getTarget(Method m) {
236: return null;
237: }
238:
239: /** Return the method to be used for invocation. */
240: public Method getMethod() throws ClassNotFoundException,
241: NoSuchMethodException {
242: return resolveMethod(getMethodName(), getTargetClass(), null);
243: }
244:
245: public Map getAttributes() {
246: Map map = super .getAttributes();
247: if (classpath != null) {
248: map.put(TAG_CLASSPATH, classpath);
249: }
250: if (threaded) {
251: map.put(TAG_THREADED, "true");
252: }
253: return map;
254: }
255:
256: public String getDefaultDescription() {
257: String desc = Strings.get("launch.desc",
258: new Object[] { getTargetClassName() + "."
259: + getMethodName() + "(" + getEncodedArguments()
260: + ")" });
261: return desc;
262: }
263:
264: public String getUsage() {
265: return USAGE;
266: }
267:
268: public String getXMLTag() {
269: return TAG_LAUNCH;
270: }
271:
272: /** Set a listener to respond to events when the launch step is
273: * threaded.
274: */
275: public void setThreadedLaunchListener(ThreadedLaunchListener l) {
276: listener = l;
277: }
278:
279: public interface ThreadedLaunchListener {
280: public void stepFailure(Launch launch,
281: AssertionFailedError error);
282:
283: public void stepError(Launch launch, Throwable throwable);
284: }
285:
286: /** No two launches are ever considered equivalent. If you want
287: * a shared {@link UIContext}, use a {@link Fixture}.
288: * @see abbot.script.UIContext#equivalent(abbot.script.UIContext)
289: * @see abbot.script.StepRunner#run(Step)
290: */
291: public boolean equivalent(UIContext context) {
292: return false;
293: }
294: }
|