changeset 510:773a450df2e6

Revert ObjectRootsFrameTests fix and change show/hideView() instead. The ObjectRootsFrame is shown if a user right-clicks on a TreeItem in the object browser. SwingUtilities.invokeLater() has been used earlier which causes for VISIBLE events to be still in the event queue. What we really want here is to wait for the VISIBLE event to be processed before we do any further actions. This guarrantees that VISIBLE events are fired only once no matter what the thread scheduling/event queue processing is. This is a better approach, because we don't want VISIBLE events to fire randomly as they might trigger certain actions, such as spawning a thread or starting a timer. EdtHelper and its test has been moved to the swing-components bundle. Reviewed-by: omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-July/002528.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Fri, 27 Jul 2012 14:45:07 +0200
parents 7bab8e5ee4c4
children ce4dcf5ae06f
files client/core/pom.xml client/core/src/main/java/com/redhat/thermostat/client/internal/SwingViewFactory.java client/core/src/main/java/com/redhat/thermostat/client/ui/ClientConfigurationSwing.java client/core/src/main/java/com/redhat/thermostat/client/ui/EdtHelper.java client/core/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java client/core/src/main/java/com/redhat/thermostat/client/ui/MenuHelper.java client/core/src/main/java/com/redhat/thermostat/client/ui/SearchFieldSwingView.java client/core/src/test/java/com/redhat/thermostat/client/ui/EdtHelperTest.java client/heapdumper/src/main/java/com/redhat/thermostat/client/heap/swing/ObjectDetailsPanel.java client/heapdumper/src/main/java/com/redhat/thermostat/client/heap/swing/ObjectRootsFrame.java client/heapdumper/src/test/java/com/redhat/thermostat/client/heap/swing/HeapDetailsSwingTest.java client/heapdumper/src/test/java/com/redhat/thermostat/client/heap/swing/ObjectRootsFrameTest.java client/swing-components/pom.xml client/swing-components/src/main/java/com/redhat/thermostat/swing/EdtHelper.java client/swing-components/src/test/java/com/redhat/thermostat/swing/EdtHelperTest.java
diffstat 15 files changed, 345 insertions(+), 301 deletions(-) [+]
line wrap: on
line diff
--- a/client/core/pom.xml	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/core/pom.xml	Fri Jul 27 14:45:07 2012 +0200
@@ -109,7 +109,12 @@
       <artifactId>org.osgi.compendium</artifactId>
       <scope>provided</scope>
     </dependency>
-    
+
+    <dependency>
+    	<groupId>com.redhat.thermostat</groupId>
+    	<artifactId>thermostat-swing-components</artifactId>
+    	<version>${project.version}</version>
+    </dependency>
   </dependencies>
 
   <build>
--- a/client/core/src/main/java/com/redhat/thermostat/client/internal/SwingViewFactory.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/core/src/main/java/com/redhat/thermostat/client/internal/SwingViewFactory.java	Fri Jul 27 14:45:07 2012 +0200
@@ -45,7 +45,6 @@
 import com.redhat.thermostat.client.ui.AgentConfigurationView;
 import com.redhat.thermostat.client.ui.ClientConfigurationSwing;
 import com.redhat.thermostat.client.ui.ClientConfigurationView;
-import com.redhat.thermostat.client.ui.EdtHelper;
 import com.redhat.thermostat.client.ui.HostCpuPanel;
 import com.redhat.thermostat.client.ui.HostCpuView;
 import com.redhat.thermostat.client.ui.HostInformationPanel;
@@ -67,6 +66,7 @@
 import com.redhat.thermostat.common.View;
 import com.redhat.thermostat.common.ViewFactory;
 import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.swing.EdtHelper;
 
 public class SwingViewFactory extends DefaultViewFactory implements ViewFactory {
 
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/ClientConfigurationSwing.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/ClientConfigurationSwing.java	Fri Jul 27 14:45:07 2012 +0200
@@ -54,6 +54,7 @@
 import com.redhat.thermostat.client.locale.Translate;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.swing.EdtHelper;
 
 public class ClientConfigurationSwing implements ClientConfigurationView {
 
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/EdtHelper.java	Tue Jul 31 14:55:59 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat 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 for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.client.ui;
-
-import java.awt.EventQueue;
-import java.lang.reflect.InvocationTargetException;
-import java.util.concurrent.Callable;
-
-public class EdtHelper {
-
-    private static class CallableException extends RuntimeException {
-
-        private CallableException(Exception ex) {
-            super(ex);
-        }
-        
-    }
-
-    private static class CallableWrapper<T> implements Runnable {
-
-        private Callable<T> callable;
-        private T result;
-
-        private CallableWrapper(Callable<T> c) {
-            callable = c;
-        }
-
-        @Override
-        public void run() {
-            try {
-                result = callable.call();
-            } catch (Exception ex) {
-                throw new CallableException(ex);
-            }
-        }
-
-        private T getResult() {
-            return result;
-        }
-    }
-
-    public void callAndWait(Runnable r) throws InvocationTargetException, InterruptedException {
-        if (EventQueue.isDispatchThread()) {
-            try {
-                r.run();
-            } catch (Exception ex) {
-                throw new InvocationTargetException(ex);
-            }
-        } else {
-            EventQueue.invokeAndWait(r);
-        }
-    }
-
-    public <T> T callAndWait(Callable<T> c) throws InvocationTargetException, InterruptedException {
-        CallableWrapper<T> w = new CallableWrapper<>(c);
-        callAndWait(w);
-        return w.getResult();
-    }
-
-}
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java	Fri Jul 27 14:45:07 2012 +0200
@@ -107,6 +107,7 @@
 import com.redhat.thermostat.common.dao.Ref;
 import com.redhat.thermostat.common.dao.VmRef;
 import com.redhat.thermostat.common.utils.StringUtils;
+import com.redhat.thermostat.swing.EdtHelper;
 
 public class MainWindow extends JFrame implements MainView {
 
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/MenuHelper.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/MenuHelper.java	Fri Jul 27 14:45:07 2012 +0200
@@ -52,6 +52,7 @@
 import com.redhat.thermostat.client.osgi.service.MenuAction;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.common.utils.StringUtils;
+import com.redhat.thermostat.swing.EdtHelper;
 
 public class MenuHelper {
 
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/SearchFieldSwingView.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/SearchFieldSwingView.java	Fri Jul 27 14:45:07 2012 +0200
@@ -61,6 +61,7 @@
 import com.redhat.thermostat.client.locale.Translate;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
+import com.redhat.thermostat.swing.EdtHelper;
 
 public class SearchFieldSwingView extends JPanel implements SearchFieldView {
 
--- a/client/core/src/test/java/com/redhat/thermostat/client/ui/EdtHelperTest.java	Tue Jul 31 14:55:59 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,194 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat 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 for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.client.ui;
-
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import java.awt.EventQueue;
-import java.lang.reflect.InvocationTargetException;
-import java.util.concurrent.Callable;
-
-import javax.swing.SwingUtilities;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class EdtHelperTest {
-
-    private class ExceptionCallable implements Callable<Object>  {
-
-        @Override
-        public Object call() throws Exception {
-            throw new Exception("fluff");
-        }
-        
-    }
-
-    private class ResultCallable implements Callable<Object> {
-    
-        private Object result;
-        private ResultCallable(Object r) {
-            result = r;
-        }
-        @Override
-        public Object call() throws Exception {
-            // By waiting here, we make sure the EDTHelper actually waits for the call.
-            try {
-                Thread.sleep(100);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-            if (EventQueue.isDispatchThread()) {
-                calledOnEDT = true;
-            }
-            return result;
-        }
-        
-    }
-
-    private class TestRunnable implements Runnable {
-
-        @Override
-        public void run() {
-            // By waiting here, we make sure the EDTHelper actually waits for the call.
-            try {
-                Thread.sleep(100);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
-            if (EventQueue.isDispatchThread()) {
-                calledOnEDT = true;
-            }
-        }
-        
-    }
-
-    private volatile boolean calledOnEDT;
-
-    @Before
-    public void setUp() {
-        calledOnEDT = false;
-    }
-
-    @After
-    public void tearDown() {
-        calledOnEDT = false;
-    }
-
-    @Test
-    public void testCallRunnableFromNonEDT() throws InvocationTargetException, InterruptedException {
-        Runnable r = new TestRunnable();
-        new EdtHelper().callAndWait(r);
-        assertTrue(calledOnEDT);
-    }
-
-    @Test
-    public void testCallRunnableFromEDT() throws InvocationTargetException, InterruptedException {
-        final Runnable r = new TestRunnable();
-        SwingUtilities.invokeAndWait(new Runnable() {
-            
-            @Override
-            public void run() {
-                try {
-                    new EdtHelper().callAndWait(r);
-                } catch (InvocationTargetException e) {
-                    throw new RuntimeException(e);
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                }
-            }
-        });
-        assertTrue(calledOnEDT);
-    }
-
-    @Test
-    public void testCallCallableFromNoEDT() throws InvocationTargetException, InterruptedException {
-        final Object expected = new Object();
-        Callable<Object> c = new ResultCallable(expected);
-        Object result = new EdtHelper().callAndWait(c);
-        assertTrue(calledOnEDT);
-        assertSame(expected, result);
-    }
-
-    @Test
-    public void testCallCallableFromEDT() throws InvocationTargetException, InterruptedException {
-        final Object expected = new Object();
-        final Callable<Object> c = new ResultCallable(expected);
-        final Object[] result = new Object[1];
-        SwingUtilities.invokeAndWait(new Runnable() {
-            
-            @Override
-            public void run() {
-                try {
-                    result[0] = new EdtHelper().callAndWait(c);
-                } catch (InvocationTargetException | InterruptedException e) {
-                    throw new RuntimeException();
-                }
-            }
-        });
-        assertTrue(calledOnEDT);
-        assertSame(expected, result[0]);
-    }
-
-    @Test(expected=InvocationTargetException.class)
-    public void testCallCallableFromNoEDTThrowingException() throws InvocationTargetException, InterruptedException {
-        Callable<Object> c = new ExceptionCallable();
-        new EdtHelper().callAndWait(c);
-    }
-
-    @Test
-    public void testCallCallableFromEDTThrowingException() throws InvocationTargetException, InterruptedException {
-        final boolean[] exceptionThrown = new boolean[1];
-        final Callable<Object> c = new ExceptionCallable();
-        SwingUtilities.invokeAndWait(new Runnable() {
-            
-            @Override
-            public void run() {
-                try {
-                    new EdtHelper().callAndWait(c);
-                } catch (InvocationTargetException e) {
-                    exceptionThrown[0] = true;
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                }
-            }
-        });
-        assertTrue(exceptionThrown[0]);
-    }
-}
--- a/client/heapdumper/src/main/java/com/redhat/thermostat/client/heap/swing/ObjectDetailsPanel.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/heapdumper/src/main/java/com/redhat/thermostat/client/heap/swing/ObjectDetailsPanel.java	Fri Jul 27 14:45:07 2012 +0200
@@ -58,7 +58,6 @@
 import com.redhat.thermostat.client.heap.LocaleResources;
 import com.redhat.thermostat.client.heap.ObjectDetailsView;
 import com.redhat.thermostat.client.heap.Translate;
-import com.redhat.thermostat.client.ui.EdtHelper;
 import com.redhat.thermostat.client.ui.SearchFieldSwingView;
 import com.redhat.thermostat.client.ui.SearchFieldView.SearchAction;
 import com.redhat.thermostat.client.ui.SwingComponent;
@@ -66,6 +65,7 @@
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.common.BasicView;
+import com.redhat.thermostat.swing.EdtHelper;
 import com.sun.tools.hat.internal.model.JavaHeapObject;
 
 import javax.swing.LayoutStyle.ComponentPlacement;
--- a/client/heapdumper/src/main/java/com/redhat/thermostat/client/heap/swing/ObjectRootsFrame.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/heapdumper/src/main/java/com/redhat/thermostat/client/heap/swing/ObjectRootsFrame.java	Fri Jul 27 14:45:07 2012 +0200
@@ -38,8 +38,10 @@
 
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Callable;
 
 import javax.swing.JFrame;
 import javax.swing.JScrollPane;
@@ -52,6 +54,9 @@
 import com.redhat.thermostat.client.heap.Translate;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
+import com.redhat.thermostat.swing.EdtHelper;
+import com.sun.jdi.event.EventQueue;
+
 import javax.swing.GroupLayout;
 import javax.swing.GroupLayout.Alignment;
 import javax.swing.JLabel;
@@ -151,24 +156,40 @@
 
     @Override
     public void showView() {
-        SwingUtilities.invokeLater(new Runnable() {
+        Callable<Boolean> hideViewRunnable = new Callable<Boolean>() {
             @Override
-            public void run() {
+            public Boolean call() {
                 pack();
                 setVisible(true);
+                return new Boolean(true);
             }
-        });
+        };
+        try {
+            new EdtHelper().callAndWait(hideViewRunnable);
+        } catch (InvocationTargetException | InterruptedException e) {
+            InternalError error = new InternalError();
+            error.initCause(e);
+            throw error;
+        }
     }
 
     @Override
     public void hideView() {
-        SwingUtilities.invokeLater(new Runnable() {
+        Callable<Boolean> hideViewRunnable = new Callable<Boolean>() {
             @Override
-            public void run() {
+            public Boolean call() {
                 setVisible(false);
                 dispose();
+                return new Boolean(true);
             }
-        });
+        };
+        try {
+            new EdtHelper().callAndWait(hideViewRunnable);
+        } catch (InvocationTargetException | InterruptedException e) {
+            InternalError error = new InternalError();
+            error.initCause(e);
+            throw error;
+        }
     }
 
     @Override
--- a/client/heapdumper/src/test/java/com/redhat/thermostat/client/heap/swing/HeapDetailsSwingTest.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/heapdumper/src/test/java/com/redhat/thermostat/client/heap/swing/HeapDetailsSwingTest.java	Fri Jul 27 14:45:07 2012 +0200
@@ -60,7 +60,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import com.redhat.thermostat.client.ui.EdtHelper;
+import com.redhat.thermostat.swing.EdtHelper;
 
 @RunWith(CacioFESTRunner.class)
 public class HeapDetailsSwingTest {
--- a/client/heapdumper/src/test/java/com/redhat/thermostat/client/heap/swing/ObjectRootsFrameTest.java	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/heapdumper/src/test/java/com/redhat/thermostat/client/heap/swing/ObjectRootsFrameTest.java	Fri Jul 27 14:45:07 2012 +0200
@@ -38,9 +38,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.atLeastOnce;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -97,6 +95,7 @@
     public void tearDown() {
         frameFixture.cleanUp();
         frameFixture = null;
+        frame.hideView();
     }
 
     @GUITest
@@ -143,6 +142,7 @@
         return paths.toArray(new TreePath[0]);
     }
 
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     @GUITest
     @Test
     public void verifyTreeInteraction() {
@@ -161,9 +161,14 @@
         tree.selectRow(0);
 
         ArgumentCaptor<ActionEvent> argCaptor = ArgumentCaptor.forClass(ActionEvent.class);
-        verify(listener, atLeastOnce()).actionPerformed(argCaptor.capture());
+        // We really want to verify that there's exactly one actionPerformend() call.
+        // If there are more events (such as VISIBLE events) fired, this may be a recipe
+        // for desaster if visble events trigger other actions, such as spawning a thread or
+        // starting/stopping a timer.
+        verify(listener).actionPerformed(argCaptor.capture());
 
         assertEquals(frame, argCaptor.getValue().getSource());
+        
         assertEquals(Action.OBJECT_SELECTED, argCaptor.getValue().getActionId());
         assertEquals(path.get(0), argCaptor.getValue().getPayload());
     }
--- a/client/swing-components/pom.xml	Tue Jul 31 14:55:59 2012 +0200
+++ b/client/swing-components/pom.xml	Fri Jul 27 14:45:07 2012 +0200
@@ -35,6 +35,12 @@
       <artifactId>jfreechart</artifactId>
     </dependency>
     
+    <dependency>
+      <groupId>net.java.openjdk.cacio</groupId>
+      <artifactId>cacio-tta</artifactId>
+      <scope>test</scope>
+    </dependency>
+    
   </dependencies>
       
   <build>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/EdtHelper.java	Fri Jul 27 14:45:07 2012 +0200
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.swing;
+
+import java.awt.EventQueue;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.Callable;
+
+public class EdtHelper {
+
+    @SuppressWarnings("serial")
+    private static class CallableException extends RuntimeException {
+
+        private CallableException(Exception ex) {
+            super(ex);
+        }
+        
+    }
+
+    private static class CallableWrapper<T> implements Runnable {
+
+        private Callable<T> callable;
+        private T result;
+
+        private CallableWrapper(Callable<T> c) {
+            callable = c;
+        }
+
+        @Override
+        public void run() {
+            try {
+                result = callable.call();
+            } catch (Exception ex) {
+                throw new CallableException(ex);
+            }
+        }
+
+        private T getResult() {
+            return result;
+        }
+    }
+
+    public void callAndWait(Runnable r) throws InvocationTargetException, InterruptedException {
+        if (EventQueue.isDispatchThread()) {
+            try {
+                r.run();
+            } catch (Exception ex) {
+                throw new InvocationTargetException(ex);
+            }
+        } else {
+            EventQueue.invokeAndWait(r);
+        }
+    }
+
+    public <T> T callAndWait(Callable<T> c) throws InvocationTargetException, InterruptedException {
+        CallableWrapper<T> w = new CallableWrapper<>(c);
+        callAndWait(w);
+        return w.getResult();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing-components/src/test/java/com/redhat/thermostat/swing/EdtHelperTest.java	Fri Jul 27 14:45:07 2012 +0200
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.swing;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.awt.EventQueue;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.Callable;
+
+import javax.swing.SwingUtilities;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.swing.EdtHelper;
+
+public class EdtHelperTest {
+
+    private class ExceptionCallable implements Callable<Object>  {
+
+        @Override
+        public Object call() throws Exception {
+            throw new Exception("fluff");
+        }
+        
+    }
+
+    private class ResultCallable implements Callable<Object> {
+    
+        private Object result;
+        private ResultCallable(Object r) {
+            result = r;
+        }
+        @Override
+        public Object call() throws Exception {
+            // By waiting here, we make sure the EDTHelper actually waits for the call.
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            if (EventQueue.isDispatchThread()) {
+                calledOnEDT = true;
+            }
+            return result;
+        }
+        
+    }
+
+    private class TestRunnable implements Runnable {
+
+        @Override
+        public void run() {
+            // By waiting here, we make sure the EDTHelper actually waits for the call.
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            if (EventQueue.isDispatchThread()) {
+                calledOnEDT = true;
+            }
+        }
+        
+    }
+
+    private volatile boolean calledOnEDT;
+
+    @Before
+    public void setUp() {
+        calledOnEDT = false;
+    }
+
+    @After
+    public void tearDown() {
+        calledOnEDT = false;
+    }
+
+    @Test
+    public void testCallRunnableFromNonEDT() throws InvocationTargetException, InterruptedException {
+        Runnable r = new TestRunnable();
+        new EdtHelper().callAndWait(r);
+        assertTrue(calledOnEDT);
+    }
+
+    @Test
+    public void testCallRunnableFromEDT() throws InvocationTargetException, InterruptedException {
+        final Runnable r = new TestRunnable();
+        SwingUtilities.invokeAndWait(new Runnable() {
+            
+            @Override
+            public void run() {
+                try {
+                    new EdtHelper().callAndWait(r);
+                } catch (InvocationTargetException e) {
+                    throw new RuntimeException(e);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        });
+        assertTrue(calledOnEDT);
+    }
+
+    @Test
+    public void testCallCallableFromNoEDT() throws InvocationTargetException, InterruptedException {
+        final Object expected = new Object();
+        Callable<Object> c = new ResultCallable(expected);
+        Object result = new EdtHelper().callAndWait(c);
+        assertTrue(calledOnEDT);
+        assertSame(expected, result);
+    }
+
+    @Test
+    public void testCallCallableFromEDT() throws InvocationTargetException, InterruptedException {
+        final Object expected = new Object();
+        final Callable<Object> c = new ResultCallable(expected);
+        final Object[] result = new Object[1];
+        SwingUtilities.invokeAndWait(new Runnable() {
+            
+            @Override
+            public void run() {
+                try {
+                    result[0] = new EdtHelper().callAndWait(c);
+                } catch (InvocationTargetException | InterruptedException e) {
+                    throw new RuntimeException();
+                }
+            }
+        });
+        assertTrue(calledOnEDT);
+        assertSame(expected, result[0]);
+    }
+
+    @Test(expected=InvocationTargetException.class)
+    public void testCallCallableFromNoEDTThrowingException() throws InvocationTargetException, InterruptedException {
+        Callable<Object> c = new ExceptionCallable();
+        new EdtHelper().callAndWait(c);
+    }
+
+    @Test
+    public void testCallCallableFromEDTThrowingException() throws InvocationTargetException, InterruptedException {
+        final boolean[] exceptionThrown = new boolean[1];
+        final Callable<Object> c = new ExceptionCallable();
+        SwingUtilities.invokeAndWait(new Runnable() {
+            
+            @Override
+            public void run() {
+                try {
+                    new EdtHelper().callAndWait(c);
+                } catch (InvocationTargetException e) {
+                    exceptionThrown[0] = true;
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        });
+        assertTrue(exceptionThrown[0]);
+    }
+}