// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ide; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.DefaultLogger; import com.intellij.openapi.extensions.ExtensionNotApplicableException; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.util.EmptyRunnable; import com.intellij.testFramework.LightPlatformTestCase; import com.intellij.testFramework.LoggedErrorProcessor; import com.intellij.testFramework.PlatformTestUtil; import com.intellij.tools.ide.metrics.benchmark.Benchmark; import com.intellij.util.ExceptionUtil; import com.intellij.util.ReflectionUtil; import com.intellij.util.TestTimeOut; import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.concurrency.EdtExecutorService; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.InvocationEvent; import java.awt.event.KeyEvent; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class IdeEventQueueTest extends LightPlatformTestCase { public void testManyEventsStress() { int N = 100000; Benchmark.newBenchmark("Event queue dispatch", () -> { UIUtil.dispatchAllInvocationEvents(); AtomicInteger count = new AtomicInteger(); for (int i = 0; i < N; i++) { SwingUtilities.invokeLater(count::incrementAndGet); } UIUtil.dispatchAllInvocationEvents(); assertEquals(N, count.get()); }).start(); } public void testKeyboardEventsAreDetected() throws InterruptedException { assertTrue(EventQueue.isDispatchThread()); IdeEventQueue ideEventQueue = IdeEventQueue.getInstance(); assertSame(ideEventQueue, Toolkit.getDefaultToolkit().getSystemEventQueue()); PlatformTestUtil.dispatchAllEventsInIdeEventQueue(); Set isDispatched = new HashSet<>(); ideEventQueue.addDispatcher(e -> { isDispatched.add(e); LOG.debug("dispatch: "+e); return false; }, getTestRootDisposable()); ideEventQueue.addPostprocessor(e -> { LOG.debug("post dispatch: "+e); return false; }, getTestRootDisposable()); ideEventQueue.addPostEventListener(e -> { LOG.debug("post event hook: "+e); return false; }, getTestRootDisposable()); int posted = ideEventQueue.keyboardEventPosted.get(); int dispatched = ideEventQueue.keyboardEventDispatched.get(); KeyEvent pressX = new KeyEvent(new JLabel("myKeyPress"), KeyEvent.KEY_PRESSED, 1, InputEvent.ALT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK, 11, 'x'); postCarefully(pressX); assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get()); assertEquals(dispatched, ideEventQueue.keyboardEventDispatched.get()); dispatchAllInvocationEventsUntilOtherEvent(); // either it's dispatched by this method or the f*@$ing VCSRefresh activity stomped in, started modal progress and consumed all events via IdeEventQueue.pumpEventsForHierarchy assertTrue(isDispatched.contains(pressX) || isConsumed(pressX)); assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get()); assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get()); // do not react to other events AWTEvent ev2 = new ActionEvent(new JLabel(), ActionEvent.ACTION_PERFORMED, "myCommand"); postCarefully(ev2); assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get()); assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get()); dispatchAllInvocationEventsUntilOtherEvent(); // either it's dispatched by this method or the f*@$ing VCSRefresh activity stomped in, started modal progress and dispatched all events via IdeEventQueue.pumpEventsForHierarchy by itself assertTrue(isDispatched.contains(ev2)); assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get()); assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get()); KeyEvent keyRelease = new KeyEvent(new JLabel("myKeyRelease"), KeyEvent.KEY_RELEASED, 1, InputEvent.ALT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK, 11, 'x'); postCarefully(keyRelease); assertEquals(posted+2, ideEventQueue.keyboardEventPosted.get()); assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get()); dispatchAllInvocationEventsUntilOtherEvent(); // either it's dispatched by this method or the f*@$ing VCSRefresh activity stomped in, started modal progress and consumed all events via IdeEventQueue.pumpEventsForHierarchy assertTrue(isDispatched.contains(keyRelease) || isConsumed(keyRelease)); assertEquals(posted+2, ideEventQueue.keyboardEventPosted.get()); assertEquals(dispatched+2, ideEventQueue.keyboardEventDispatched.get()); } private static void postCarefully(AWTEvent event) { LOG.debug("posting " + event); IdeEventQueue ideEventQueue = IdeEventQueue.getInstance(); boolean posted = ideEventQueue.doPostEvent(event); assertTrue("Was not posted: "+event, posted); boolean mustBeConsumed = event.getID() == ActionEvent.ACTION_PERFORMED; assertEquals(mustBeConsumed, ReflectionUtil.getField(AWTEvent.class, event, boolean.class, "consumed").booleanValue()); assertTrue(ReflectionUtil.getField(AWTEvent.class, event, boolean.class, "isPosted")); } private static boolean isConsumed(InputEvent event) { return event.isConsumed(); } // need this because everybody can post some crazy stuff to IdeEventQueue, so we have to filter InvocationEvents out private static void dispatchAllInvocationEventsUntilOtherEvent() throws InterruptedException { while (true) { AWTEvent event = PlatformTestUtil.dispatchNextEventIfAny(); LOG.debug("event dispatched in dispatchAll() "+event+"; -"+(event instanceof InvocationEvent ? "continuing" : "returning")); if (!(event instanceof InvocationEvent)) break; } } private static class MyException extends RuntimeException { } private static void throwMyException() { throw new MyException(); } private static void checkMyExceptionThrownImmediately() { TestTimeOut t = TestTimeOut.setTimeout(10, TimeUnit.SECONDS); while (true) { try { UIUtil.dispatchAllInvocationEvents(); } catch (Throwable e) { assertTrue(e.toString(), ExceptionUtil.causedBy(e, MyException.class)); break; } assertFalse(t.timedOut()); } } public void testExceptionInInvokeLateredRunnableMustThrowImmediatelyInTests() { SwingUtilities.invokeLater(() -> throwMyException()); checkMyExceptionThrownImmediately(); } public void testAppInvokeLateredRunnableMustThrowImmediatelyInTests() { SwingUtilities.invokeLater(()->ApplicationManager.getApplication().invokeLater(()->throwMyException())); checkMyExceptionThrownImmediately(); } public void testEdtExecutorRunnableMustThrowImmediatelyInTests() { ApplicationManager.getApplication().invokeLater(() -> throwMyException(), ModalityState.nonModal()); checkMyExceptionThrownImmediately(); } public void testEdtScheduledExecutorRunnableMustThrowImmediatelyInTests() { EdtExecutorService.getScheduledExecutorInstance().schedule(()->throwMyException(), 1, TimeUnit.MILLISECONDS); checkMyExceptionThrownImmediately(); } public void testNoExceptionEvenCreatedByThanosExtensionNotApplicableExceptionMustKillEDT() { assertTrue(SwingUtilities.isEventDispatchThread()); DefaultLogger.disableStderrDumping(getTestRootDisposable()); throwInIdeEventQueueDispatch(ExtensionNotApplicableException.create(), null); // ControlFlowException silently ignored throwInIdeEventQueueDispatch(new ProcessCanceledException(), null); // ControlFlowException silently ignored Error error = new Error(); throwInIdeEventQueueDispatch(error, error); } private void throwInIdeEventQueueDispatch(@NotNull Throwable toThrow, Throwable expectedToBeLogged) { AtomicBoolean run = new AtomicBoolean(); InvocationEvent event = new InvocationEvent(this, () -> { run.set(true); ExceptionUtil.rethrow(toThrow); }); Runnable runnable = () -> { IdeEventQueue ideEventQueue = IdeEventQueue.getInstance(); ideEventQueue.executeInProductionModeEvenThoughWeAreInTests(() -> ideEventQueue.dispatchEvent(event)); }; Throwable error; if (expectedToBeLogged != null) { error = LoggedErrorProcessor.executeAndReturnLoggedError(runnable); } else { runnable.run(); error = null; } assertTrue(run.get()); assertSame(expectedToBeLogged, error); } public void testPumpEventsForHierarchyMustExitOnIsFutureDoneCondition() { assertTrue(SwingUtilities.isEventDispatchThread()); var ideEventQueue = IdeEventQueue.getInstance(); var future = new CompletableFuture<>(); var cancelEventTime = TestTimeOut.setTimeout(2, TimeUnit.SECONDS); var component = new JLabel(); AppExecutorUtil.getAppScheduledExecutorService().schedule(() -> SwingUtilities.invokeLater(EmptyRunnable.getInstance()), 100, TimeUnit.MILLISECONDS); var start = System.nanoTime(); ideEventQueue.pumpEventsForHierarchy(component, future, __ -> { if (cancelEventTime.isTimedOut()) { future.complete(null); } // post InvocationEvent to give getNextEvent work to do SwingUtilities.invokeLater(EmptyRunnable.getInstance()); }); var elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); // check that first, we did exit the pumpEventsForHierarchy and second, at the right moment assertTrue(String.valueOf(elapsedMs), cancelEventTime.isTimedOut()); } }