/* * 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. * <p> * SwingTestHelper uses reflection to invoke all methods starting with * the name <code>onEDT</code> on the EDT and all methods starting with * <code>onBackgroundThread</code> 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: * <pre> * class Test extends SwingTestHelper { * private void onEDT10(); * private void onBackgroundThread20(); * private void onBackgroundThread30(); * private void onEDT40(); * private void onBackgroundThread50(); * } * </pre> * Will have the methods invoked in the order <code>onEDT10</code>, * <code>onBackgroundThread20</code>, <code>onBackgroundThread30</code>, * <code>onEDT40</code>, <code>onBackgroundThread50</code>. * <p> * If you're not happy with method mangling you can also use annotations. * The following gives the same result as the previous example: * <pre> * 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 * } * </pre> * <p> * 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. * <p> * Between each of the methods, all pending events (native and Java) * are processed. * <p> * Failure of the test is signaled by any method throwing * an exception, directly invoking <code>fail</code> or one of the * <code>assert</code> variants. If no methods throw an exception the test is * assumed to have passed. * <p> * Often times it is necessary to block until focus has been gained on a * particular widget. This can be handled by the * <code>requestAndWaitForFocus</code> method. It will invoke * <code>requestFocus</code> and block the test (not the EDT) until focus * has been granted to the widget. * <p> * Care must be taken when using <code>Robot</code> directly. For * example, it's tempting to flood <code>Robot</code> 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 * <code>waitForEvent</code>. For example, to block until a key typed * event has been processed do the following: * <pre> * private void onEDT() { * robot.moveMouseTo(myComponent); * robot.mousePress(xxx); * robot.mouseRelease(xxx); * waitForEvent(myComponent, MouseEvent.MOUSE_RELEASED); * } * </pre> * <p> * 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 <code>waitForCondition</code> can be used to * block test execution until the supplied <code>Runnable</code> returns. The * <code>Runnable</code> is invoked on the background thread. * <p> * To use this class you will need to do the following: * <ol> * <li>Override the method <code>createContentPane</code>. All of your logic * for setting up the test environment should go here. This method is * invoked on the EDT. * <li>Implement the necessary <code>onEDTXX</code> and * <code>onBackgroundThreadXXX</code> methods to do the actual testing. * <li>Make your <code>main</code> method look like: * <code>new MySwingTestHelper().run(args)</code>. This will block * until the test fails or succeeds. * <li>To use this with jtreg you'll need to have something like: * <pre> * @library ../../../regtesthelpers * @build Test JRobot Assert SwingTestHelper * @run main MySwingTestHelper * * </pre> * </ol> * <p> * Here's a complete example: * <pre> * 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"); * } * } * } * </pre> * * * @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<Method> methods; // The conditions the background thread is blocked on. private java.util.List<Runnable> conditions; // Whether or not we've installed the AWTEventListener private boolean installedEventListener; /** * Instance of <code>Robot</code> returned from <code>createRobot</code>. */ protected JRobot robot; /** * <code>Window</code> returned from <code>createWindow</code>. */ 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 <code>Window</code> for the test. This * implementation returns a JFrame with a default close operation * of <code>EXIT_ON_CLOSE</code>. The <code>Component</code> * returned from <code>createContentPane</code> is added the * <code>JFrame</code> and the the frame is packed. * <p> * Typically you only need override <code>createContentPane</code>. */ 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 <code>Component</code> to place in the frame. * Override this or the <code>createWindow</code> method. */ protected Component createContentPane() { return null; } /** * Invokes <code>requestFocus</code> 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 <b>must</b> be invoked * on the EDT, if you do not invoke it from the edt the test will fail. * * @param c the <code>Component</code> 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 <b>must</b> 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 <code>Component</code> to wait for the event on; * <code>null</code> 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 <code>Runnable</code> 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 <code>Runnable</code> completes. */ protected void waitForCondition(Runnable runnable) { synchronized(this) { if (conditions == null) { conditions = new LinkedList<Runnable>(); } conditions.add(runnable); } } /** * Runs the test. This method blocks the caller until the test * fails or succeeds. Recognized arguments are: * <p> * "-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<Method> allMethods = new HashSet<Method>(); allMethods.addAll(Arrays.asList(getClass().getMethods())); allMethods.addAll(Arrays.asList(getClass().getDeclaredMethods())); methods = new ArrayList<Method>(); 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<Method> comparator = new Comparator<Method>() { 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; } } } } } }