/* * Copyright (c) 2007, 2016, Oracle and/or its affiliates. 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.awt.*; import java.awt.event.*; import java.beans.*; import java.lang.reflect.*; import java.util.*; import javax.swing.*; /** * SwingTestHelper is a utility class for writing AWT/Swing regression * tests that require interacting with the UI. Typically such tests * consist of executing a chunk of code, waiting on an event, executing * more code ... This is painful in that you typically have to use various * invokeLaters and threading to handle that interaction. SwingTestHelper * strealines this process. *
* SwingTestHelper uses reflection to invoke all methods starting with
* the name onEDT
on the EDT and all methods starting with
* onBackgroundThread
on a background thread. Between each method
* invocation all pending events on the EDT are processed. The methods
* are first sorted based on an integer after the method names and invoked
* in that order. For example, the following subclass:
*
* class Test extends SwingTestHelper { * private void onEDT10(); * private void onBackgroundThread20(); * private void onBackgroundThread30(); * private void onEDT40(); * private void onBackgroundThread50(); * } ** Will have the methods invoked in the order
onEDT10
,
* onBackgroundThread20
, onBackgroundThread30
,
* onEDT40
, onBackgroundThread50
.
* * If you're not happy with method mangling you can also use annotations. * The following gives the same result as the previous example: *
* class Test extends SwingTestHelper { * @Test(10) * private void foo(); // Was onEDT10 * * @Test(value=20, onEDT=false) * private void bar(); // Was onBackgroundThread20 * * @Test(value=30, onEDT=false) * private void baz(); // Was onBackgroundThread30 * * @Test(40) * private void zed(); // Was onEDT40 * * @Test(value=50, onEDT=false) * private void onBackgroundThread50(); // Was onBackgroundThread50 * } **
* It is recommended that you increment the value in increments of * 10. This makes it easier to add methods at a later date without * having to change all method names/annotations after the newly added * method. *
* Between each of the methods, all pending events (native and Java) * are processed. *
* Failure of the test is signaled by any method throwing
* an exception, directly invoking fail
or one of the
* assert
variants. If no methods throw an exception the test is
* assumed to have passed.
*
* Often times it is necessary to block until focus has been gained on a
* particular widget. This can be handled by the
* requestAndWaitForFocus
method. It will invoke
* requestFocus
and block the test (not the EDT) until focus
* has been granted to the widget.
*
* Care must be taken when using Robot
directly. For
* example, it's tempting to flood Robot
with events and
* assume they will be received after some delay. Depending upon the
* machine you may need to increase the delay. Instead it's
* preferrable to block test execution until the event has been
* received and processed. This can be done using the method
* waitForEvent
. For example, to block until a key typed
* event has been processed do the following:
*
* private void onEDT() { * robot.moveMouseTo(myComponent); * robot.mousePress(xxx); * robot.mouseRelease(xxx); * waitForEvent(myComponent, MouseEvent.MOUSE_RELEASED); * } **
* Waiting for focus and events are specific examples of a more
* general problem. Often times you need the EDT to continue processing
* events, but want to block test execution until something happens.
* In the case of focus you want to block test execution until focus
* is gained. The method waitForCondition
can be used to
* block test execution until the supplied Runnable
returns. The
* Runnable
is invoked on the background thread.
*
* To use this class you will need to do the following: *
createContentPane
. All of your logic
* for setting up the test environment should go here. This method is
* invoked on the EDT.
* onEDTXX
and
* onBackgroundThreadXXX
methods to do the actual testing.
* main
method look like:
* new MySwingTestHelper().run(args)
. This will block
* until the test fails or succeeds.
* * @library ../../../regtesthelpers * @build Test JRobot Assert SwingTestHelper * @run main MySwingTestHelper * **
* Here's a complete example: *
* public class bug4852305 extends SwingTestHelper { * private JTable table; * * public static void main(String[] args) throws Throwable { * new bug4852305().run(args); * } * * protected Component createContentPane() { * DefaultTableModel model = new DefaultTableModel(1, 2); * model.setValueAt("x", 0, 0); * model.setValueAt("z", 0, 1); * table = new JTable(model); * table.setDefaultEditor(Object.class, new DefaultCellEditor(new JTextField()) { * public boolean isCellEditable(EventObject anEvent) { * if ((anEvent instanceof KeyEvent) || * (anEvent instanceof ActionEvent)) { * return false; * } * return true; * } * }); * return new JScrollPane(table); * } * * private void onEDT10() { * requestAndWaitForFocus(table); * } * * private void onEDT20() { * robot.keyPress(KeyEvent.VK_A); * robot.keyRelease(KeyEvent.VK_A); * waitForEvent(table, KeyEvent.KEY_RELEASED); * } * * private void onEDT30() { * if (table.isEditing()) { * fail("Should not be editing"); * } * } * } ** * * @author Scott Violet */ public abstract class SwingTestHelper { private static final String ON_EDT_METHOD_NAME = "onEDT"; private static final String IN_BACKGROUND_METHOD_NAME = "onBackgroundThread"; // Whether or not we've installed a PropertyChangeListener on the // KeyboardFocusManager private boolean installedFocusListener; // Component currently blocking on until focus has been received. private Component componentWaitingForFocus; // Set to true when done. private boolean done; // If failed, this gives the exception. Only the first exception is // kept. private Throwable error; // List of methods to invoke private java.util.List
Robot
returned from createRobot
.
*/
protected JRobot robot;
/**
* Window
returned from createWindow
.
*/
protected Window window;
// Listens for the first paint event
private AWTEventListener paintListener;
// Whether or not we've received a paint event.
private boolean receivedFirstPaint;
// used if the user wants to slow down method processing
private PauseCondition delay = null;
private boolean showProgress;
private JProgressBar progBar;
public SwingTestHelper() {
paintListener = new AWTEventListener() {
public void eventDispatched(AWTEvent ev) {
if ((ev.getID() & PaintEvent.PAINT) != 0 &&
ev.getSource() == window) {
synchronized(SwingTestHelper.this) {
if (receivedFirstPaint) {
return;
}
receivedFirstPaint = true;
}
Toolkit.getDefaultToolkit().removeAWTEventListener(
paintListener);
startControlLoop();
}
}
};
Toolkit.getDefaultToolkit().addAWTEventListener(
paintListener, AWTEvent.PAINT_EVENT_MASK);
}
/**
* Sets whether SwingTestHelper should use {@code SunToolkit.realSync}
* to wait for events to finish, or {@code Robot.waitForIdle}. The default
* is to use realSync.
* Nov 2014: no realSync any more, just robot.waitForIdle which actually
* _is_ realSync on all platforms but OS X (and thus cannot be used on EDT).
*/
public void setUseRealSync(boolean useRealSync) {
//NOOP
}
/**
* Set the amount of time to delay between invoking methods in
* the control loop. Useful to slow down testing.
*/
protected void setDelay(int delay) {
if (delay <= 0) {
this.delay = null;
} else {
this.delay = new PauseCondition(delay);
}
}
/**
* Sets whether or not progress through the list of methods is
* shown by a progress bar at the bottom of the window created
* by {@code createWindow}.
*/
protected void setShowProgress(boolean showProgress) {
this.showProgress = showProgress;
}
/**
* Creates and returns the Window
for the test. This
* implementation returns a JFrame with a default close operation
* of EXIT_ON_CLOSE
. The Component
* returned from createContentPane
is added the
* JFrame
and the the frame is packed.
*
* Typically you only need override
* "-exit": Causes main to exit when the test is done.
* "-showProg": Indicate the progress of the test with a
* progress bar in the main window. Only works
* if the test hasn't overridden {@code createWindow}.
* "-delay int": Sets the delay between executing methods.
* Useful when you want to slow a test to watch it.
*
* @param args the arguments from main, it's ok to pass in null
*/
protected final void run(String[] args) throws Throwable {
boolean exit = false;
if (args != null) {
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-exit")) {
exit = true;
} else if (args[i].equals("-delay")) {
try {
setDelay(Integer.parseInt(args[++i]));
} catch (NumberFormatException ne) {
throw new RuntimeException("-delay requires an integer value");
} catch (ArrayIndexOutOfBoundsException ae) {
throw new RuntimeException("-delay requires an integer value");
}
} else if (args[i].equals("-showProg")) {
setShowProgress(true);
} else {
throw new RuntimeException("Invalid argument \"" + args[i] + "\"");
}
}
}
createWindow0();
synchronized(this) {
while(!done) {
wait();
}
}
if (exit) {
// Not in harness
if (error != null) {
System.out.println("FAILED: " + error);
error.printStackTrace();
}
System.exit(0);
}
if (error != null) {
throw error;
}
}
/**
* Creates the window, on the EDT.
*/
private void createWindow0() {
EventQueue.invokeLater(new Runnable() {
public void run() {
window = createWindow();
window.show();
}
});
}
/**
* Initializes the progress bar if necessary.
*/
private void initProgressBar(final int size) {
EventQueue.invokeLater(new Runnable() {
public void run() {
if (progBar != null) {
progBar.setMaximum(size);
}
}
});
}
/**
* Starst the control loop.
*/
private void startControlLoop() {
robot = createRobot();
if (robot != null) {
calculateMethods();
initProgressBar(methods.size());
new Thread(new Runnable() {
public void run() {
controlLoop();
}
}).start();
}
}
/**
* Increment the progress bar.
*/
private void nextProgress(final String name) {
EventQueue.invokeLater(new Runnable() {
public void run() {
if (progBar != null) {
progBar.setString(name);
progBar.setValue(progBar.getValue() + 1);
}
}
});
}
private synchronized Runnable currentCondition() {
if (conditions != null && conditions.size() > 0) {
return conditions.get(0);
}
return null;
}
private synchronized Runnable nextCondition() {
return conditions.remove(0);
}
private void controlLoop() {
int methodIndex = 0;
while (methodIndex < methods.size()) {
// Wait for any pending conditions
Runnable condition;
while ((condition = currentCondition()) != null) {
try {
condition.run();
} catch (Exception e) {
fail(e);
return;
}
waitForEDTToFinish();
synchronized(this) {
if (done) {
return;
}
}
// Advance to next condition
nextCondition();
}
// Let all events on the EDT finish
waitForEDTToFinish();
if (delay != null) {
delay.run();
}
// Invoke the next method
Method method = methods.get(methodIndex++);
Test test = method.getAnnotation(Test.class);
boolean onEDT = true;
if (test != null) {
onEDT = test.onEDT();
}
else if (!method.getName().startsWith(ON_EDT_METHOD_NAME)) {
onEDT = false;
}
if (onEDT) {
invokeOnEDT(method);
}
else {
invoke(method);
}
// Let all events on the EDT finish
waitForEDTToFinish();
nextProgress(method.getName());
// If done, stop.
synchronized(this) {
if (done) {
return;
}
}
}
// No more methods, if we get and done isn't true, set it true
// so that the main thread wakes up.
synchronized(this) {
if (!done) {
done = true;
notifyAll();
}
}
}
private void waitForEDTToFinish() {
robot.waitForIdle();
}
private void invokeOnEDT(final Method method) {
try {
EventQueue.invokeAndWait(new Runnable() {
public void run() {
invoke(method);
}
});
} catch (InvocationTargetException ite) {
fail(ite);
} catch (InterruptedException ie) {
fail(ie);
}
}
private void invoke(Method method) {
System.out.println("invoking: " + method.getName());
try {
if (Modifier.isPrivate(method.getModifiers())) {
method.setAccessible(true);
}
method.invoke(this);
} catch (Exception e) {
fail(e);
}
}
// Determines the methods to execute.
private void calculateMethods() {
// Using a Set avoids duplicating methods returned by both
// getMethods() and getDeclaredMethods().
HashSetcreateContentPane
.
*/
protected Window createWindow() {
JFrame frame = new JFrame("Test: " + getClass().getName());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(createContentPane());
if (showProgress) {
progBar = new JProgressBar();
progBar.setString("");
progBar.setStringPainted(true);
frame.add(progBar, BorderLayout.SOUTH);
}
frame.pack();
return frame;
}
/**
* Returns the Component
to place in the frame.
* Override this or the createWindow
method.
*/
protected Component createContentPane() {
return null;
}
/**
* Invokes requestFocus
on the passed in component (assuming
* it doesn't already have focus). Test execution is blocked until focus
* has been gained on the component. This method must be invoked
* on the EDT, if you do not invoke it from the edt the test will fail.
*
* @param c the Component
to wait for focus on
*/
protected void requestAndWaitForFocus(Component c) {
requestAndWaitForFocus(c, true);
}
/**
* Blocks test execution until focus is gained on the component.
* This method must be invoked
* on the EDT, if you do not invoke it from the edt the test will fail.
*/
protected void waitForFocusGained(Component c) {
requestAndWaitForFocus(c, false);
}
private void requestAndWaitForFocus(Component c, boolean requestFocus) {
if (!EventQueue.isDispatchThread()) {
System.out.println(
"requestAndWaitForFocus should be invoked on EDT");
throw new RuntimeException();
}
if (componentWaitingForFocus != null) {
System.out.println("Already waiting for focus");
throw new RuntimeException();
}
if (!installedFocusListener) {
installedFocusListener = true;
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addPropertyChangeListener(new FocusListener());
}
synchronized(this) {
if (c.hasFocus()) {
return;
}
componentWaitingForFocus = c;
}
if (requestFocus) {
c.requestFocus();
}
waitForCondition(new FocusCondition());
}
/**
* Blocks test execution until the specified event has been received.
* This method immediately returns and the EDT will continue to
* process events, but test execution is blocked until
* the event is received.
*
* @param event the event type to wait for
*/
protected void waitForEvent(int event) {
waitForEvent(null, event);
}
/**
* Blocks test execution until the specified event has been received.
* This method immediately returns and the EDT will continue to
* process events, but test execution is blocked until
* the event is received.
*
* @param target the Component
to wait for the event on;
* null
indicates it does not matter which
* component the event is received on
* @param event the event type to wait for
*/
protected void waitForEvent(Component target, int event) {
waitForCondition(new EventCondition(target, event));
if (!installedEventListener) {
installedEventListener = true;
Toolkit.getDefaultToolkit().addAWTEventListener(
new EventListener(), 0xFFFFFFFFFFFFFFFFl);
}
}
/**
* Paused test execution for the specified amount of time. The caller
* immediately returns and the EDT can process events.
*
* @param time the amount of time, in milliseconds, to pause for
*/
protected void pause(int time) {
waitForCondition(new PauseCondition(time));
}
/**
* Schedules a Runnable
that will be processed in the
* background thread. This method immediately returns, and the
* EDT is free to continue processing events. Test execution is
* blocked until the Runnable
completes.
*/
protected void waitForCondition(Runnable runnable) {
synchronized(this) {
if (conditions == null) {
conditions = new LinkedList