From 7aeae0e777da7e500612b1391b6d4c8c38610eb1 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Mon, 28 Sep 2026 13:44:08 +0200 Subject: [PATCH 1/2] Track the event being dispatched per thread in EventObjectSupplier EventObjectSupplier published the event being delivered in a single instance-wide map keyed by topic, and DIEventHandler.handleEvent added it, resolved the requestor's arguments and removed it again around each delivery. EventAdmin.sendEvent() delivers on the caller's thread, so several threads can run that sequence for the same topic concurrently and interfere with each other: - get() did a check-then-act (containsKey, then get) on that map without holding its monitor. When another thread removed the entry in between, get() returned null although its contract allows only the event or IInjector.NOT_A_VALUE. null counts as resolved, so handlers were invoked with a null event, for example java.lang.NullPointerException: Cannot invoke "org.osgi.service.event.Event.getProperty(String)" because "event" is null at ...perspectiveswitcher.PerspectiveSwitcher.handleLabelEvent On the path where the requested type is not Event, get() dereferenced the removed entry and threw the NPE inside the supplier itself. - One thread's removal could drop the event another thread had just published, so that handler was silently never invoked. - One thread's event could overwrite the event another thread was about to resolve, so that handler was invoked with a foreign event. The event is published, resolved and withdrawn by one and the same thread, so it is now kept in a ThreadLocal and no longer visible to other threads. A stack per topic keeps the outer event current when a handler sends another event of the same topic while being injected, and the removal is done in a finally block so a failing resolution no longer leaves a stale event behind. Also read the current event once instead of three times, compare the desired class null-safely, since getDesiredClass() may return null for types that are neither a Class nor a ParameterizedType, and return null from getTopic() when the descriptor carries no @EventTopic qualifier instead of throwing. EventObjectSupplierRaceTest covers the above. Each delivery is run through the handler the supplier creates for a requestor, so that publishing, resolving and withdrawing happen in the same order as in production, and the requestor asks the supplier for its argument while its arguments are being resolved, which is what InjectorImpl does. The descriptors are read from the parameters of an annotated template method, so the supplier resolves the topic from a real @EventTopic qualifier. The interleavings are forced with latches instead of with concurrent load, so a regression fails reliably rather than occasionally: one test parks a delivery on another thread while this thread runs a complete delivery of the same topic, and then checks that neither of the two saw anything of the other; another one runs a complete nested delivery of the same topic while the outer arguments are being resolved; a third lets the resolution fail and checks that the next delivery still gets its own event. The remaining tests cover the payload of a parameter that is not an Event, a topic that is not being delivered, a parameter without an @EventTopic qualifier and a requestor that has become invalid. The test needs no running framework, it neither subscribes nor uses an EventAdmin. It is registered in CoreTestSuite. Fixes https://github.com/eclipse-platform/eclipse.platform/issues/2974 Assisted-by: Copilot with Claude Opus 5 --- .../META-INF/MANIFEST.MF | 2 +- .../extensions/EventObjectSupplier.java | 69 ++- .../EventObjectSupplierRaceTest.java | 423 ++++++++++++++++++ .../eclipse/e4/core/tests/CoreTestSuite.java | 2 + 4 files changed, 482 insertions(+), 14 deletions(-) create mode 100644 runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/di/extensions/EventObjectSupplierRaceTest.java diff --git a/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF b/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF index 404106b17f8..cd948cf00ae 100644 --- a/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF +++ b/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF @@ -19,7 +19,7 @@ Import-Package: jakarta.annotation;version="[2.0.0,4.0.0)", org.osgi.service.log;version="[1.4.0,2)", org.osgi.util.tracker;version="[1.5.3,2.0.0)" Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.e4.core.di.internal.extensions;version="0.15.0";x-friends:="org.eclipse.e4.ui.di", +Export-Package: org.eclipse.e4.core.di.internal.extensions;version="0.15.0";x-friends:="org.eclipse.e4.ui.di,org.eclipse.e4.core.tests", org.eclipse.e4.core.di.internal.extensions.util;version="0.15.0";x-internal:=true Automatic-Module-Name: org.eclipse.e4.core.di.extensions.supplier Service-Component: OSGI-INF/org.eclipse.e4.core.di.internal.extensions.EventObjectSupplier.xml, diff --git a/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/src/org/eclipse/e4/core/di/internal/extensions/EventObjectSupplier.java b/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/src/org/eclipse/e4/core/di/internal/extensions/EventObjectSupplier.java index 94b9f2c914c..8bb5040ee1c 100644 --- a/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/src/org/eclipse/e4/core/di/internal/extensions/EventObjectSupplier.java +++ b/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/src/org/eclipse/e4/core/di/internal/extensions/EventObjectSupplier.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010, 2017 IBM Corporation and others. + * Copyright (c) 2010, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -19,7 +19,9 @@ import jakarta.annotation.PreDestroy; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayDeque; import java.util.Collection; +import java.util.Deque; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; @@ -66,7 +68,19 @@ public void setEventAdmin(EventAdmin eventAdmin) { this.eventAdmin = eventAdmin; } - protected Map currentEvents = new HashMap<>(); + /** + * The events currently being dispatched, per thread and topic. + *

+ * Events may be dispatched concurrently by several threads (and even + * re-entrantly on the same thread if an event handler sends another event). + * Since the event is published here only to be picked up by + * {@link #get(IObjectDescriptor, IRequestor, boolean, boolean)} while the + * arguments of the requestor are resolved - which happens on the very same + * thread - the events are tracked per thread. A stack is used per topic to + * support nested dispatching of the same topic. + *

+ */ + private final ThreadLocal>> currentEvents = new ThreadLocal<>(); class DIEventHandler implements EventHandler { @@ -86,8 +100,11 @@ public void handleEvent(Event event) { } addCurrentEvent(topic, event); - requestor.resolveArguments(false); - removeCurrentEvent(topic); + try { + requestor.resolveArguments(false); + } finally { + removeCurrentEvent(topic); + } requestor.execute(); } @@ -136,15 +153,40 @@ public boolean equals(Object obj) { private final Map> registrations = new HashMap<>(); protected void addCurrentEvent(String topic, Event event) { - synchronized (currentEvents) { - currentEvents.put(topic, event); + Map> events = currentEvents.get(); + if (events == null) { + events = new HashMap<>(2); + currentEvents.set(events); } + // the stack grows at the head: the innermost delivery is the current one + events.computeIfAbsent(topic, key -> new ArrayDeque<>(1)).addFirst(event); } protected void removeCurrentEvent(String topic) { - synchronized (currentEvents) { - currentEvents.remove(topic); + Map> events = currentEvents.get(); + if (events == null) { + return; + } + Deque stack = events.get(topic); + if (stack != null) { + stack.pollFirst(); + if (stack.isEmpty()) { + events.remove(topic); + } + } + if (events.isEmpty()) { + // don't hold on to the map, this thread may not dispatch events again + currentEvents.remove(); + } + } + + private Event getCurrentEvent(String topic) { + Map> events = currentEvents.get(); + if (events == null) { + return null; } + Deque stack = events.get(topic); + return stack == null ? null : stack.peekFirst(); } @Override @@ -163,17 +205,18 @@ public Object get(IObjectDescriptor descriptor, IRequestor requestor, boolean tr unsubscribe(requestor); } - if (!currentEvents.containsKey(topic)) { + Event currentEvent = getCurrentEvent(topic); + if (currentEvent == null) { return IInjector.NOT_A_VALUE; } // convert to fit destination Class descriptorsClass = getDesiredClass(descriptor.getDesiredType()); - if (descriptorsClass.equals(Event.class)) { - return currentEvents.get(topic); + if (Event.class.equals(descriptorsClass)) { + return currentEvent; } - return currentEvents.get(topic).getProperty(DATA); + return currentEvent.getProperty(DATA); } private void subscribe(String topic, IRequestor requestor) { @@ -211,7 +254,7 @@ protected String getTopic(IObjectDescriptor descriptor) { return null; } EventTopic qualifier = descriptor.getQualifier(EventTopic.class); - return qualifier.value(); + return qualifier == null ? null : qualifier.value(); } protected void unsubscribe(IRequestor requestor) { diff --git a/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/di/extensions/EventObjectSupplierRaceTest.java b/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/di/extensions/EventObjectSupplierRaceTest.java new file mode 100644 index 00000000000..9d448bfa360 --- /dev/null +++ b/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/di/extensions/EventObjectSupplierRaceTest.java @@ -0,0 +1,423 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.e4.core.internal.tests.di.extensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.e4.core.di.IInjector; +import org.eclipse.e4.core.di.InjectionException; +import org.eclipse.e4.core.di.extensions.EventTopic; +import org.eclipse.e4.core.di.internal.extensions.EventObjectSupplier; +import org.eclipse.e4.core.di.suppliers.IObjectDescriptor; +import org.eclipse.e4.core.di.suppliers.IRequestor; +import org.eclipse.e4.core.di.suppliers.PrimaryObjectSupplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventHandler; + +/** + * Verifies that {@link EventObjectSupplier} hands an event to the injector if and only if the + * calling thread is the one currently delivering it. + *

+ * The supplier is a singleton OSGi service shared by all contexts and all threads, and it has no + * way to pass the event down into the injector other than parking it for the duration of + * {@link IRequestor#resolveArguments(boolean)}. Because {@code EventAdmin.sendEvent(Event)} + * delivers on the caller's thread, several threads can be inside that window at the same time, and + * a handler may even re-enter it on the same thread while its arguments are being computed. + *

+ *

+ * The deliveries below are driven through the handler the supplier creates for a requestor, so that + * publishing, resolving and withdrawing happen in the same order as in production. The + * interleavings are forced with latches instead of with concurrent load, so a regression fails + * reliably rather than occasionally. + *

+ */ +@Timeout(value = 60, unit = TimeUnit.SECONDS) +public class EventObjectSupplierRaceTest { + + private static final String TOPIC = "e4/test/supplier/delivery"; + + private static final String OTHER_TOPIC = "e4/test/supplier/other"; + + private static final long AWAIT_SECONDS = 30; + + /** + * Template for the descriptors used below. The qualifiers are read from these parameters, so + * the supplier's own {@code getTopic(IObjectDescriptor)} is exercised instead of being stubbed. + */ + @SuppressWarnings("unused") + private static void handlerParameters( + @EventTopic(TOPIC) Event event, + @EventTopic(TOPIC) String payload, + @EventTopic(OTHER_TOPIC) Event otherEvent, + Event unqualified) { + // never called, only its parameters are looked at + } + + private static final IObjectDescriptor EVENT_OF_TOPIC = parameterDescriptor(0); + + private static final IObjectDescriptor PAYLOAD_OF_TOPIC = parameterDescriptor(1); + + private static final IObjectDescriptor EVENT_OF_OTHER_TOPIC = parameterDescriptor(2); + + private static final IObjectDescriptor UNQUALIFIED_EVENT = parameterDescriptor(3); + + private TestableSupplier supplier; + + @BeforeEach + public void createSupplier() { + supplier = new TestableSupplier(); + } + + /** + * While a delivery is in progress the event belongs to the arguments being resolved, afterwards + * nothing is current anymore. + */ + @Test + public void eventIsResolvedWhileItIsDeliveredAndNotAfterwards() { + Event event = event(TOPIC, "payload"); + ResolvingRequestor requestor = new ResolvingRequestor(EVENT_OF_TOPIC); + + deliver(TOPIC, event, requestor); + + assertSame(event, requestor.resolved(), "the delivered event was not resolved"); + assertTrue(requestor.wasExecuted(), "the requestor was not executed after its arguments were resolved"); + assertSame(IInjector.NOT_A_VALUE, probe(EVENT_OF_TOPIC), + "the event is still current although its delivery has finished"); + } + + /** A parameter that is not an {@code Event} receives the payload the event carries. */ + @Test + public void payloadIsResolvedForParametersThatAreNotEvents() { + ResolvingRequestor requestor = new ResolvingRequestor(PAYLOAD_OF_TOPIC); + + deliver(TOPIC, event(TOPIC, "payload"), requestor); + + assertEquals("payload", requestor.resolved(), "the payload of the delivered event was not resolved"); + } + + /** Deliveries of different topics must not interfere with each other. */ + @Test + public void eventOfOneTopicIsNotResolvedForAnother() { + ResolvingRequestor requestor = new ResolvingRequestor(EVENT_OF_OTHER_TOPIC); + + deliver(TOPIC, event(TOPIC, "payload"), requestor); + + assertSame(IInjector.NOT_A_VALUE, requestor.resolved(), + "an event was resolved for a topic that is not being delivered"); + } + + /** A parameter without an {@code @EventTopic} qualifier has no topic and therefore no value. */ + @Test + public void parameterWithoutTopicIsNotAValue() { + assertSame(IInjector.NOT_A_VALUE, probe(UNQUALIFIED_EVENT), + "a parameter without @EventTopic must not be resolved"); + } + + /** An obsolete requestor is dropped instead of being resolved and executed. */ + @Test + public void invalidRequestorIsNeitherResolvedNorExecuted() { + ResolvingRequestor requestor = new ResolvingRequestor(EVENT_OF_TOPIC); + requestor.invalidate(); + + deliver(TOPIC, event(TOPIC, "payload"), requestor); + + assertFalse(requestor.wasResolved(), "an invalid requestor had its arguments resolved"); + assertFalse(requestor.wasExecuted(), "an invalid requestor was executed"); + } + + /** + * Two threads delivering the same topic at the same time must not see each other's event: the + * thread that is not delivering gets nothing, and the thread that is delivering gets its own + * event, no matter what the other one published or withdrew in the meantime. + */ + @Test + public void concurrentDeliveriesOfTheSameTopicAreIndependent() throws InterruptedException { + Event deliveredElsewhere = event(TOPIC, "from the other thread"); + Event deliveredHere = event(TOPIC, "from this thread"); + + CountDownLatch otherThreadIsDelivering = new CountDownLatch(1); + CountDownLatch thisThreadIsDone = new CountDownLatch(1); + + // resolves only after this thread has run a complete delivery of the same topic + ResolvingRequestor otherRequestor = new ResolvingRequestor(EVENT_OF_TOPIC, () -> { + otherThreadIsDelivering.countDown(); + await(thisThreadIsDone); + }); + + AtomicReference otherThreadFailure = new AtomicReference<>(); + Thread otherThread = new Thread(() -> deliver(TOPIC, deliveredElsewhere, otherRequestor), "other-delivery"); + otherThread.setUncaughtExceptionHandler((thread, thrown) -> otherThreadFailure.set(thrown)); + otherThread.start(); + try { + await(otherThreadIsDelivering); + + assertSame(IInjector.NOT_A_VALUE, probe(EVENT_OF_TOPIC), + "the event delivered by another thread was resolved on this one"); + + ResolvingRequestor ownRequestor = new ResolvingRequestor(EVENT_OF_TOPIC); + deliver(TOPIC, deliveredHere, ownRequestor); + assertSame(deliveredHere, ownRequestor.resolved(), + "the event delivered by this thread was not the one resolved here"); + } finally { + thisThreadIsDone.countDown(); + otherThread.join(); + } + + rethrow(otherThreadFailure.get()); + assertSame(deliveredElsewhere, otherRequestor.resolved(), + "the delivery on the other thread lost its event to this thread"); + } + + /** + * Resolving the arguments of a handler runs arbitrary code, which may send another event of the + * same topic before the outer delivery is finished. The inner delivery must leave the outer one + * untouched. + */ + @Test + public void nestedDeliveryOfTheSameTopicKeepsTheOuterEventCurrent() { + Event outerEvent = event(TOPIC, "outer"); + Event innerEvent = event(TOPIC, "inner"); + + ResolvingRequestor innerRequestor = new ResolvingRequestor(EVENT_OF_TOPIC); + // a complete delivery of the same topic, run while the outer arguments are being resolved + ResolvingRequestor outerRequestor = new ResolvingRequestor(EVENT_OF_TOPIC, + () -> deliver(TOPIC, innerEvent, innerRequestor)); + + deliver(TOPIC, outerEvent, outerRequestor); + + assertSame(innerEvent, innerRequestor.resolved(), "the nested delivery did not resolve its own event"); + assertSame(outerEvent, outerRequestor.resolved(), "the nested delivery invalidated the outer event"); + assertSame(IInjector.NOT_A_VALUE, probe(EVENT_OF_TOPIC), + "an event is still current although both deliveries have finished"); + } + + /** A delivery that fails while resolving must not leave its event behind for the next one. */ + @Test + public void failedDeliveryDoesNotLeaveItsEventCurrent() { + ResolvingRequestor brokenRequestor = new ResolvingRequestor(EVENT_OF_TOPIC, () -> { + throw new InjectionException("cannot resolve the other arguments of this handler"); + }); + + assertThrows(InjectionException.class, () -> deliver(TOPIC, event(TOPIC, "failing"), brokenRequestor)); + + assertSame(IInjector.NOT_A_VALUE, probe(EVENT_OF_TOPIC), + "the event of the failed delivery is still current"); + + Event next = event(TOPIC, "next"); + ResolvingRequestor nextRequestor = new ResolvingRequestor(EVENT_OF_TOPIC); + deliver(TOPIC, next, nextRequestor); + assertSame(next, nextRequestor.resolved(), "a later delivery did not get its own event"); + } + + /** Runs one complete delivery, exactly as EventAdmin does when it calls the supplier's handler. */ + private void deliver(String topic, Event event, IRequestor requestor) { + supplier.handlerFor(topic, requestor).handleEvent(event); + } + + /** Asks for a value outside of any delivery, which is what an ordinary injection does. */ + private Object probe(IObjectDescriptor descriptor) { + return supplier.resolve(descriptor, null); + } + + private static Event event(String topic, String payload) { + return new Event(topic, Map.of(EventObjectSupplier.DATA, payload)); + } + + private static void await(CountDownLatch latch) { + try { + assertTrue(latch.await(AWAIT_SECONDS, TimeUnit.SECONDS), "timed out waiting for the other thread"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + private static void rethrow(Throwable thrown) { + if (thrown != null) { + throw new AssertionError("the delivery on the other thread failed", thrown); + } + } + + private static IObjectDescriptor parameterDescriptor(int index) { + try { + Parameter parameter = EventObjectSupplierRaceTest.class + .getDeclaredMethod("handlerParameters", Event.class, String.class, Event.class, Event.class) + .getParameters()[index]; + return new ParameterDescriptor(parameter); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + /** Gives the test access to the members the injector and EventAdmin use. */ + private static final class TestableSupplier extends EventObjectSupplier { + + TestableSupplier() { + // get() only verifies that an EventAdmin is around, it never talks to it + setEventAdmin(new UnusableEventAdmin()); + } + + EventHandler handlerFor(String topic, IRequestor requestor) { + return makeHandler(topic, requestor); + } + + Object resolve(IObjectDescriptor descriptor, IRequestor requestor) { + // no tracking: subscribing needs a running framework and is not what is tested here + return get(descriptor, requestor, false, false); + } + } + + /** + * Stands for one injected method: it asks the supplier for its argument while its arguments are + * being resolved, which is what {@code InjectorImpl} does, and optionally runs other work first. + */ + private final class ResolvingRequestor implements IRequestor { + + private final IObjectDescriptor descriptor; + + private final Runnable whileResolving; + + private volatile Object resolved; + + private volatile boolean resolveCalled; + + private volatile boolean executeCalled; + + private volatile boolean valid = true; + + ResolvingRequestor(IObjectDescriptor descriptor) { + this(descriptor, null); + } + + ResolvingRequestor(IObjectDescriptor descriptor, Runnable whileResolving) { + this.descriptor = descriptor; + this.whileResolving = whileResolving; + } + + @Override + public void resolveArguments(boolean initial) { + resolveCalled = true; + if (whileResolving != null) { + whileResolving.run(); + } + resolved = supplier.resolve(descriptor, this); + } + + @Override + public Object execute() { + executeCalled = true; + return null; + } + + Object resolved() { + assertNotNull(resolved, "the arguments of this requestor were never resolved"); + return resolved; + } + + boolean wasResolved() { + return resolveCalled; + } + + boolean wasExecuted() { + return executeCalled; + } + + void invalidate() { + valid = false; + } + + @Override + public boolean isValid() { + return valid; + } + + @Override + public Object getRequestingObject() { + return null; + } + + @Override + public Class getRequestingObjectClass() { + return null; + } + + @Override + public void disposed(PrimaryObjectSupplier objectSupplier) { + // nothing to dispose + } + + @Override + public boolean uninject(Object object, PrimaryObjectSupplier objectSupplier) { + return false; + } + } + + /** Describes one parameter of {@link EventObjectSupplierRaceTest#handlerParameters}. */ + private static final class ParameterDescriptor implements IObjectDescriptor { + + private final Parameter parameter; + + ParameterDescriptor(Parameter parameter) { + this.parameter = parameter; + } + + @Override + public Type getDesiredType() { + return parameter.getParameterizedType(); + } + + @Override + public boolean hasQualifier(Class clazz) { + return parameter.getAnnotation(clazz) != null; + } + + @Override + public T getQualifier(Class clazz) { + return parameter.getAnnotation(clazz); + } + + @Override + public Annotation[] getQualifiers() { + return parameter.getAnnotations(); + } + } + + /** The supplier must not use the EventAdmin while a value is resolved. */ + private static final class UnusableEventAdmin implements EventAdmin { + + @Override + public void postEvent(Event event) { + throw new AssertionError("the supplier posted an event: " + event); + } + + @Override + public void sendEvent(Event event) { + throw new AssertionError("the supplier sent an event: " + event); + } + } +} diff --git a/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java b/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java index 7ea73890c46..d37a6bc6ddb 100644 --- a/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java +++ b/runtime/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java @@ -56,6 +56,7 @@ import org.eclipse.e4.core.internal.tests.di.InvokeTest; import org.eclipse.e4.core.internal.tests.di.InvokeTestMissingAnnotation; import org.eclipse.e4.core.internal.tests.di.RecursiveObjectCreationTest; +import org.eclipse.e4.core.internal.tests.di.extensions.EventObjectSupplierRaceTest; import org.eclipse.e4.core.internal.tests.di.extensions.ExtendedSupplierInjectionTests; import org.eclipse.e4.core.internal.tests.di.extensions.InjectionEventTest; import org.eclipse.e4.core.internal.tests.di.extensions.InjectionMixedSuppliersTest; @@ -74,6 +75,7 @@ InjectionPreferencesTest.class, InjectionMixedSuppliersTest.class, InjectionEventTest.class, + EventObjectSupplierRaceTest.class, InjectionOSGiTest.class, InjectionOSGiHandlerTest.class, ServiceSupplierTestCase.class, From 5bdebfc204b18c86c2de5f9ead418ce712ed833c Mon Sep 17 00:00:00 2001 From: Eclipse Platform Bot Date: Mon, 28 Sep 2026 13:18:29 +0000 Subject: [PATCH 2/2] Version bump(s) for 4.42 stream --- .../META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF b/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF index cd948cf00ae..b6affcbaf67 100644 --- a/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF +++ b/runtime/bundles/org.eclipse.e4.core.di.extensions.supplier/META-INF/MANIFEST.MF @@ -3,7 +3,7 @@ Bundle-ManifestVersion: 2 Bundle-Name: %Bundle-Name Bundle-Vendor: %Bundle-Vendor Bundle-SymbolicName: org.eclipse.e4.core.di.extensions.supplier -Bundle-Version: 0.17.1200.qualifier +Bundle-Version: 0.17.1300.qualifier Bundle-RequiredExecutionEnvironment: JavaSE-17 Require-Capability: osgi.extender; filter:="(&(osgi.extender=osgi.component)(version>=1.3)(!(version>=2.0)))"