Tests Fail due to Java Swing Timers Not Disposed

intellij-community master update has the TestApplicationManager checking for swing timers being disposed on tear down.

That’s all very nice but it throws AssertionFailedError for timers not being disposed which are not part of the plugin as far as I can tell:

Not disposed javax.swing.Timer: Timer (listeners: [com.intellij.codeInsight.hint.HintManagerImpl$$Lambda$1884/0x0000000800b88840@133e019b]) (delayed for 13327ms); queue:TimerQueue (Hint timeout, Hint timeout, Hint timeout, Hint timeout)
Not disposed javax.swing.Timer: Timer (listeners: [com.intellij.codeInsight.hint.HintManagerImpl$$Lambda$1884/0x0000000800b88840@7dac3fd8]) (delayed for 13360ms); queue:TimerQueue (Hint timeout, Hint timeout, Hint timeout)
Not disposed javax.swing.Timer: Timer (listeners: [com.intellij.codeInsight.hint.HintManagerImpl$$Lambda$1884/0x0000000800b88840@425357dd]) (delayed for 13393ms); queue:TimerQueue (Hint timeout, Hint timeout)
Not disposed javax.swing.Timer: Timer (listeners: [com.intellij.codeInsight.hint.HintManagerImpl$$Lambda$1884/0x0000000800b88840@2102a4d5]) (delayed for 14913ms); queue:TimerQueue (Hint timeout)

There should be an option to disable this check on tear down to prevent failing tests which have nothing to do with the plugin and are caused by the test framework or API implementation not disposing of resources.

As a workaround, I converted the Kotlin code used to throw the error to Java and added it to my tear down so I can stop the timers and only print the message to the console, instead of throwing an AssertionFailedError.

class TestCase {
    @After
    public void after() throws Throwable {
        checkJavaSwingTimersAreDisposed();
        tearDown();
    }

    void checkJavaSwingTimersAreDisposed() {
        // NOTE: added this otherwise plugin tests fail due to swing timers not being disposed which have nothing to do with the plugin test
        try {
            Class<?> timerQueueClass = Class.forName("javax.swing.TimerQueue");
            Method sharedInstance = timerQueueClass.getMethod("sharedInstance");
            sharedInstance.setAccessible(true);
            Object timerQueue = sharedInstance.invoke(null);
            DelayQueue<?> delayQueue = ReflectionUtil.getField(timerQueueClass, timerQueue, DelayQueue.class, "queue");
            while (true) {
                Delayed timer = delayQueue.peek();

                if (timer == null) {
                    return;
                }

                int delay = Math.toIntExact(timer.getDelay(TimeUnit.MILLISECONDS));
                String text = "(delayed for " + delay + "ms)";
                Method getTimer = ReflectionUtil.getDeclaredMethod(timer.getClass(), "getTimer");
                Timer swingTimer = (Timer) getTimer.invoke(timer);
                text = "Timer (listeners: " + Arrays.toString(swingTimer.getActionListeners()) + ") " + text;
                try {
                    System.out.println("Not disposed javax.swing.Timer: " + text + "; queue:" + timerQueue);
                } finally {
                    swingTimer.stop();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

 

2
5 comments

Hi Vladimir,

I had the same issue a couple weeks ago and the problem is due to a bug in older JDKs: https://bugs.openjdk.java.net/browse/JDK-8161664

The solution is to switch to a newer JDK version. Yann Cébron suggested to use Jetbrains' bundled JBR, because it's the only one they guarantee to be compatible with.

Ciao, Victor.

0

I am running with latest JetBrains JDK. The issue is caused by plugin code showing an editor tooltip with a 15sec timeout delay. So the timers with remaining time out is normal at the end of the test.

The plugin code does not dispose of these and does not need to in normal operation. The destruction of these during tests should be part of the test framework not the plugin.

IntelliJ IDEA 2020.1 Snapshot (Community Edition)
Build #IC-193.6015.16, built on January 4, 2020
Runtime version: 11.0.5+9-b670.1 x86_64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.14.6
GC: ParNew, ConcurrentMarkSweep
Memory: 6023M
Cores: 16
Registry: ide.ui.tree.indent=1, ide.mac.boldEditorTabs=true, show.diff.preview.as.editor.tab=true, gui.form.edit.property.keys=true, ide.popup.resizable.border.sensitivity=6, uast.java.use.psi.type.precheck=false, undo.documentUndoLimit=200, debugger.show.values.inplace=true, undo.globalUndoLimit=50, debugger.watches.in.variables=false, debugger.single.smart.step.force=false, focus.fix.lost.cursor=true, debugger.valueTooltipAutoShowOnSelection=true, vcs.non.modal.commit.split.horizontal.if.no.diff.preview=true, editor.caret.width=1, ide.new.editor.tabs.selection.color=0,136,238, vcs.non.modal.commit.toggle.ui=false, editor.new.mouse.hover.popups=false
Non-Bundled Plugins: Switch Structure, CMD Support, PlantUML integration, PsiViewer, String Manipulation, com.bulenkov.intellij.png.optimizer, com.dmarcotte.handlebars, com.vladsch.git-file-case-fixer, de.docksnet.puml.syntaxchecker, org.jetbrains.idea.grammar, org.jetbrains.kannotator, Osmorc, nb-mind-map-idea, org.jetbrains.kotlin, MetricsReloaded, com.jetbrains.space, com.karateca.jstoolbox, com.lid.intellij.translateme, com.ultrahob.zerolength.plugin, com.vladsch.MissingInActions, com.vladsch.idea.multimarkdown, com.vladsch.PluginDevelopersToolbox, com.vladsch.plugins.consoleFileCaddy, com.vladsch.plugins.touchTypistsCompletionCaddy, de.juserv.intellij-propertiessort, mobi.hsz.idea.gitignore, nl.jworks.intellij.bootstrap3, org.intellij.RegexpTester, tanvd.grazi

 

0

Hi Vladimir,

I see, but then I'd like to challenge the premise that the tooltip should be triggered in a test at all. If you're calling platform code directly to trigger the tooltip, you could introduce a service instead that abstracts the functionality. In a test, you could then replace it with a dummy implementation (the service extension points provide this already) that either does nothing or allows you to verify that a tooltip was requested.

Ciao, Victor.

0

Victor, I prefer to keep knowledge of plugin code running under test out of the code as much as possible. I do all unit testing outside the platform test because platform tests run at glacial speed. The platform tests for me are the final "integration" test for all the features.

What you are proposing is that I add unnecessary complexity of another service and extension point to the plugin. Then add a separate implementation for the test. All that work to avoid the assertion failure at the end of the test in the framework which is meaningless except in very specific test cases.

Not to mention that the test will not be running real code that triggers an editor tooltip but some mock version of it which will not match the API implementation unless I take the trouble to maintain it in sync with the API implementation. I got more than enough real code to maintain in sync with the plentiful API changes without adding unnecessary mock code to the list. 

My solution of terminating timers in the tear down is preferred because it adds code to the test framework, fixing the cause of the problem and not to the plugin where it would be fixing a symptom caused by the test framework.

What is the issue of swing timers not having stopped before the test ends? All it means is that the timer has not had a chance to time out. Which is to be expected for UI timers triggered in a test.

There are so many things going on during a platform test that it is impossible to have any assurance of which background processes run, when they run or the order in which they run. Including those that use swing timers.

If this validation is needed for some tests in the test framework or plugins, then it should be an optional test failure not one that is assumed to be part of the light platform test. In worst case make the method, which does the validation, accessible to the test case with an argument to not throw exception but just clean up the timers. That way it can be added to the tear down instead of having to hunt through the code to find the cause then have to copy the implementation to my test so I can run it before the framework tear down to disable the failure.

 

0

I just had the same problem with my tests. I am testing plugin inspections highlighting with CodeInsightFixtureTestCase.java.
Fix from Vladimir helped - thx my dude C:

My specs:

IntelliJ IDEA 2019.3.3 (Ultimate Edition)
Build #IU-193.6494.35, built on February 11, 2020
Runtime version: 11.0.5+10-b520.38 x86_64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.15.2
GC: ParNew, ConcurrentMarkSweep
Memory: 1981M
Cores: 8
Registry: java.completion.argument.hints.internal=false
Non-Bundled Plugins: IdeaVIM, PsiViewer, com.jetbrains.php, org.jetbrains.kotlin

 

0

Please sign in to leave a comment.