/* * 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: *

    *
  1. Override the method createContentPane. All of your logic * for setting up the test environment should go here. This method is * invoked on the EDT. *
  2. Implement the necessary onEDTXX and * onBackgroundThreadXXX methods to do the actual testing. *
  3. Make your main method look like: * new MySwingTestHelper().run(args). This will block * until the test fails or succeeds. *
  4. To use this with jtreg you'll need to have something like: *
     *     @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 methods; // The conditions the background thread is blocked on. private java.util.List conditions; // Whether or not we've installed the AWTEventListener private boolean installedEventListener; /** * Instance of 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 createContentPane. */ 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(); } conditions.add(runnable); } } /** * Runs the test. This method blocks the caller until the test * fails or succeeds. Recognized arguments are: *

* "-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(). HashSet allMethods = new HashSet(); allMethods.addAll(Arrays.asList(getClass().getMethods())); allMethods.addAll(Arrays.asList(getClass().getDeclaredMethods())); methods = new ArrayList(); for (Method method : allMethods) { Test test = method.getAnnotation(Test.class); if (test != null) { methods.add(method); } else if (method.getName().startsWith(ON_EDT_METHOD_NAME)) { methods.add(method); } else if (method.getName().startsWith(IN_BACKGROUND_METHOD_NAME)) { methods.add(method); } } Comparator comparator = new Comparator() { public int compare(Method m1, Method m2) { int index1 = getIndex(m1); int index2 = getIndex(m2); return index1 - index2; } private int getIndex(Method m) { String name = m.getName(); String indexAsString; Test test = m.getAnnotation(Test.class); if (test != null) { return test.value(); } if (name.startsWith(ON_EDT_METHOD_NAME)) { indexAsString = name.substring( ON_EDT_METHOD_NAME.length()); } else { indexAsString = name.substring( IN_BACKGROUND_METHOD_NAME.length()); } if (indexAsString.length() == 0) { System.out.println( "onEDT and onBackgroundThread must be " + "followed by an integer specifying " + "order."); System.exit(0); } return Integer.parseInt(indexAsString); } }; Collections.sort(methods, comparator); } /** * Invoke if the test should be considered to have failed. This will * stop test execution. */ public void fail(String reason) { fail(new RuntimeException(reason)); } /** * Invoke if the test should be considered to have failed. This will * stop test execution. */ public void fail(Throwable error) { synchronized(this) { if (this.error == null) { if (error instanceof InvocationTargetException) { this.error = ((InvocationTargetException)error). getCause(); } else { this.error = error; } this.done = true; notifyAll(); } } } /** * Invoke to prematurely stop test execution while there are remaining * methods. You typically don't invoke this, instead if all methods have * been executed and fail hasn't been invoked, the test is considered to * have passed. */ public void succeeded() { synchronized(this) { this.done = true; notifyAll(); } } /** * Creates and returns the Robot that will be used. You generally don't * need to override this. */ protected JRobot createRobot() { JRobot robot = JRobot.getRobot(false); return robot; } private class FocusListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent e) { if (componentWaitingForFocus != null && "focusOwner".equals(e.getPropertyName()) && componentWaitingForFocus == e.getNewValue()) { synchronized(SwingTestHelper.this) { componentWaitingForFocus = null; SwingTestHelper.this.notifyAll(); } } } } private class EventCondition implements Runnable { private Component component; private int eventID; private boolean received; EventCondition(Component component, int eventID) { this.component = component; this.eventID = eventID; } public int getEventID() { return eventID; } public Component getComponent() { return component; } public void received() { synchronized(SwingTestHelper.this) { this.received = true; SwingTestHelper.this.notifyAll(); } } public boolean isWaiting() { return !received; } public void run() { synchronized(SwingTestHelper.this) { while (!received) { try { SwingTestHelper.this.wait(); } catch (InterruptedException ie) { fail(ie); } } } } } private class FocusCondition implements Runnable { public void run() { synchronized(SwingTestHelper.this) { while (componentWaitingForFocus != null) { try { SwingTestHelper.this.wait(); } catch (InterruptedException ie) { fail(ie); } } } } } private class PauseCondition implements Runnable { private int time; PauseCondition(int time) { this.time = time; } public void run() { try { Thread.sleep(time); } catch (InterruptedException ie) { fail(ie); } } } private class EventListener implements AWTEventListener { public void eventDispatched(AWTEvent ev) { int eventID = ev.getID(); synchronized (SwingTestHelper.this) { for (Runnable condition : conditions) { if (condition instanceof EventCondition) { EventCondition ec = (EventCondition)condition; if (ec.isWaiting()) { if (eventID == ec.getEventID() && (ec.getComponent() == null || ev.getSource() == ec.getComponent())) { ec.received(); } return; } } else { return; } } } } } }