This is an automated email from the ASF dual-hosted git repository.

jsedding pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git


The following commit(s) were added to refs/heads/master by this push:
     new 170d3a7  SLING-12019 - Avoid duplicate ResourceResolverFactory 
registrations (#104)
170d3a7 is described below

commit 170d3a7b415a3c147b2381c8919fd9dfa7eb427c
Author: Julian Sedding <jsedd...@apache.org>
AuthorDate: Mon Sep 18 13:04:50 2023 +0200

    SLING-12019 - Avoid duplicate ResourceResolverFactory registrations (#104)
    
    - reworked synchronization model from relying on worker queue
      to locks for "configuration" and an AtomicReference for the 
factoryRegistration
    - removed unnecessary null checks for factoryRegistration, simplifying the 
code
    - increased test coverage, particularly for concurrent scenarios
    - switched to junit-platform to allow mixing junit4 and junit5 tests
    - enabled parallel test execution for junit-platform
---
 pom.xml                                            |  36 +++-
 .../impl/FactoryRegistrationHandler.java           | 127 ++++++++----
 .../impl/ResourceResolverFactoryActivator.java     |  16 +-
 .../impl/FactoryRegistrationHandlerTest.java       | 230 ++++++++++++---------
 .../resourceresolver/util/CustomMatchers.java      | 123 +++++++++++
 .../util/events/AbstractAwaitingListener.java      |  75 +++++++
 .../util/events/RecordingListener.java             |  99 +++++++++
 .../util/events/ServiceEventUtil.java              | 105 ++++++++++
 src/test/resources/junit-platform.properties       |  19 ++
 9 files changed, 683 insertions(+), 147 deletions(-)

diff --git a/pom.xml b/pom.xml
index 75d9d34..302d3de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -188,20 +188,32 @@
 
         <!-- Testing -->
         <dependency>
-            <groupId>org.hamcrest</groupId>
-            <artifactId>hamcrest-library</artifactId>
-            <version>1.3</version>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.9.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.9.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.vintage</groupId>
+            <artifactId>junit-vintage-engine</artifactId>
+            <version>5.9.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <version>2.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>junit-addons</groupId>
-            <artifactId>junit-addons</artifactId>
-            <version>1.4</version>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -216,10 +228,16 @@
             <version>5.5.0</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.osgi-mock.junit5</artifactId>
+            <version>3.3.8</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.testing.osgi-mock.junit4</artifactId>
-            <version>3.2.2</version>
+            <version>3.3.8</version>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git 
a/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
 
b/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
index 94d8002..1227f3c 100644
--- 
a/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
+++ 
b/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
@@ -30,45 +30,78 @@ import org.slf4j.LoggerFactory;
 
 import java.util.Dictionary;
 import java.util.Hashtable;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
 
 public class FactoryRegistrationHandler implements AutoCloseable {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(FactoryRegistrationHandler.class);
 
-    private final ResourceResolverFactoryActivator activator;
+    private final ExecutorService factoryRegistrationWorker;
+
+    /**
+     * The configurationLock serializes access to the fields {@code activator}
+     * and  {@code factoryPreconditions}. Each access to these fields must be
+     * guarded by this lock.
+     */
+    private final ReentrantLock configurationLock = new ReentrantLock();
 
-    private final FactoryPreconditions factoryPreconditions;
+    /** Access only when holding configurationLock */
+    private ResourceResolverFactoryActivator activator;
 
-    private final ExecutorService factoryRegistrationWorker;
+    /** Access only when holding configurationLock */
+    private FactoryPreconditions factoryPreconditions;
 
-    @SuppressWarnings("java:S3077") // The field is only ever set to null or 
to a new FactoryRegistration instance, which is safe.
-    private volatile FactoryRegistration factoryRegistration;
+    private final AtomicReference<FactoryRegistration> factoryRegistration = 
new AtomicReference<>(null);
 
-    public FactoryRegistrationHandler(
-            ResourceResolverFactoryActivator activator, FactoryPreconditions 
factoryPreconditions) {
-        this.factoryPreconditions = factoryPreconditions;
-        this.activator = activator;
+    public FactoryRegistrationHandler() {
         this.factoryRegistrationWorker = Executors.newSingleThreadExecutor(
                 r -> new Thread(r, 
ResourceResolverFactory.class.getSimpleName() + " 
registration/deregistration"));
     }
 
+    public void configure(ResourceResolverFactoryActivator activator, 
FactoryPreconditions factoryPreconditions) {
+        checkClosed();
+
+        boolean reRegister;
+        try {
+            configurationLock.lock();
+            reRegister = this.activator != activator || 
!Objects.equals(this.factoryPreconditions, factoryPreconditions);
+            LOG.debug("activator differs = {}, factoryPreconditions differ = 
{}", this.activator != activator, !Objects.equals(this.factoryPreconditions, 
factoryPreconditions));
+            LOG.debug("factoryPreconditions {} vs {}", 
this.factoryPreconditions, factoryPreconditions);
+            this.factoryPreconditions = factoryPreconditions;
+            this.activator = activator;
+        } finally {
+            configurationLock.unlock();
+        }
+
+        if (reRegister) {
+            unregisterFactory();
+            maybeRegisterFactory(null, null);
+        }
+    }
+
+    private void checkClosed() {
+        if (factoryRegistrationWorker.isShutdown()) {
+            throw new IllegalStateException("FactoryRegistrationHandler is 
already closed");
+        }
+    }
+
     @Override
     public void close() {
-        unregisterFactory();
         factoryRegistrationWorker.shutdown();
         try {
-            if (!factoryRegistrationWorker.awaitTermination(5, 
TimeUnit.SECONDS)) {
+            if (!factoryRegistrationWorker.awaitTermination(1, 
TimeUnit.MINUTES)) {
                 factoryRegistrationWorker.shutdownNow();
-                // make sure everything is unregistered, even if
-                // the factoryRegistrationWorker did not complete
-                doUnregisterFactory();
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
+        runWithThreadName("deregistration on close", 
this::doUnregisterFactory);
     }
 
     /**
@@ -77,15 +110,23 @@ public class FactoryRegistrationHandler implements 
AutoCloseable {
     void maybeRegisterFactory(final String unavailableName, final String 
unavailableServicePid) {
         if (!factoryRegistrationWorker.isShutdown()) {
             LOG.debug("submitting maybeRegisterFactory");
-            factoryRegistrationWorker.submit(() -> {
-                final boolean preconditionsOk = 
factoryPreconditions.checkPreconditions(unavailableName, unavailableServicePid);
-                if ( preconditionsOk && this.factoryRegistration == null ) {
+            factoryRegistrationWorker.execute(() -> {
+                final boolean preconditionsOk;
+                final ResourceResolverFactoryActivator localActivator;
+                try {
+                    configurationLock.lock();
+                    preconditionsOk = 
factoryPreconditions.checkPreconditions(unavailableName, unavailableServicePid);
+                    localActivator = activator;
+                } finally {
+                    configurationLock.unlock();
+                }
+                if (preconditionsOk) {
+                    final Bundle systemBundle = 
localActivator.getBundleContext().getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
                     // check system bundle state - if stopping, don't register 
new factory
-                    final Bundle systemBundle = 
activator.getBundleContext().getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
-                    if ( systemBundle != null && systemBundle.getState() != 
Bundle.STOPPING ) {
-                        runWithThreadName("registration", 
this::doRegisterFactory);
+                    if (systemBundle != null && systemBundle.getState() != 
Bundle.STOPPING) {
+                        runWithThreadName("registration", () -> 
this.doRegisterFactory(localActivator));
                     }
-                } else if ( !preconditionsOk && this.factoryRegistration != 
null ) {
+                } else {
                     LOG.debug("performing unregisterFactory via 
maybeRegisterFactory");
                     runWithThreadName("deregistration", 
this::doUnregisterFactory);
                 }
@@ -94,35 +135,40 @@ public class FactoryRegistrationHandler implements 
AutoCloseable {
     }
 
     void unregisterFactory() {
-        LOG.debug("submitting unregisterFactory");
-        factoryRegistrationWorker.submit(() -> 
runWithThreadName("deregistration",
-                this::doUnregisterFactory));
+        if (!factoryRegistrationWorker.isShutdown()) {
+            LOG.debug("submitting unregisterFactory");
+            factoryRegistrationWorker.execute(
+                    () -> runWithThreadName("deregistration", 
this::doUnregisterFactory));
+        }
     }
 
     /**
      * Register the factory.
      */
-    private void doRegisterFactory() {
-        LOG.debug("performing registerFactory, factoryRegistration == {}", 
factoryRegistration);
-        if (this.factoryRegistration == null) {
-            this.factoryRegistration = new 
FactoryRegistration(activator.getBundleContext(), activator);
-        }
-        LOG.debug("finished performing registerFactory, factoryRegistration == 
{}", factoryRegistration);
+    private void doRegisterFactory(ResourceResolverFactoryActivator activator) 
{
+        final FactoryRegistration newRegistration = 
factoryRegistration.updateAndGet(oldRegistration -> {
+            LOG.debug("performing registerFactory, factoryRegistration == {}", 
oldRegistration);
+            return Objects.requireNonNullElseGet(oldRegistration,
+                    () -> new 
FactoryRegistration(activator.getBundleContext(), activator));
+        });
+        LOG.debug("finished performing registerFactory, factoryRegistration == 
{}", newRegistration);
     }
 
     /**
      * Unregister the factory (if registered).
      */
     private void doUnregisterFactory() {
-        LOG.debug("performing unregisterFactory, factoryRegistration == {}", 
factoryRegistration);
-        if (this.factoryRegistration != null) {
-            this.factoryRegistration.unregister();
-            this.factoryRegistration = null;
-        }
-        LOG.debug("finished performing unregisterFactory, factoryRegistration 
== {}", factoryRegistration);
+        final FactoryRegistration newRegistration = 
factoryRegistration.updateAndGet(oldRegistration -> {
+            LOG.debug("performing unregisterFactory, factoryRegistration == 
{}", factoryRegistration);
+            Optional.ofNullable(oldRegistration)
+                    .ifPresent(FactoryRegistration::unregister);
+            LOG.debug("setting factoryRegistration = null");
+            return null;
+        });
+        LOG.debug("finished performing unregisterFactory, factoryRegistration 
== {}", newRegistration);
     }
 
-    private static void runWithThreadName(String threadNameSuffix, Runnable 
task) {
+    private void runWithThreadName(String threadNameSuffix, Runnable task) {
         final String name = Thread.currentThread().getName();
         try {
             
Thread.currentThread().setName(ResourceResolverFactory.class.getSimpleName() + 
" " + threadNameSuffix);
@@ -168,9 +214,16 @@ public class FactoryRegistrationHandler implements 
AutoCloseable {
         }
 
         void unregister() {
+            LOG.debug("Unregister runtimeRegistration");
             runtimeRegistration.unregister();
+
+            LOG.debug("Unregister factoryRegistration");
             factoryRegistration.unregister();
+
+            LOG.debug("Unregister commonFactory");
             commonFactory.deactivate();
+                        
+            LOG.debug("Unregister completed");
         }
     }
 }
diff --git 
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
 
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
index 1651e06..39bc17c 100644
--- 
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
+++ 
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
@@ -124,8 +124,7 @@ public class ResourceResolverFactoryActivator {
     /** Observation paths */
     private volatile Path[] observationPaths;
 
-    @SuppressWarnings("java:S3077")
-    private volatile FactoryRegistrationHandler factoryRegistrationHandler;
+    private final FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler();
 
     private final VanityPathConfigurer vanityPathConfigurer = new 
VanityPathConfigurer();
     {
@@ -313,8 +312,7 @@ public class ResourceResolverFactoryActivator {
                 resourceProviderTracker,
                 requiredResourceProviderNames,
                 requiredResourceProvidersLegacy);
-        factoryRegistrationHandler = new FactoryRegistrationHandler(this, 
factoryPreconditions);
-        factoryRegistrationHandler.maybeRegisterFactory(null, null);
+        factoryRegistrationHandler.configure(this, factoryPreconditions);
 
         if (!hasPreRegisteredResourceProviderTracker) {
             this.resourceProviderTracker.activate(this.bundleContext, 
this.eventAdmin,
@@ -343,7 +341,7 @@ public class ResourceResolverFactoryActivator {
     protected void modified(final BundleContext bundleContext,
         final ResourceResolverFactoryConfig config,
         final VanityPathConfigurer.DeprecatedVanityConfig 
deprecatedVanityConfig) {
-        this.deactivate();
+        this.deactivateInternal();
         this.activate(bundleContext, config, deprecatedVanityConfig);
     }
 
@@ -352,11 +350,13 @@ public class ResourceResolverFactoryActivator {
      */
     @Deactivate
     protected void deactivate() {
-        this.factoryRegistrationHandler.close();
-        this.factoryRegistrationHandler = null;
-
         // factoryRegistrationHandler must be closed before bundleContext is 
set to null
+        this.factoryRegistrationHandler.close();
         this.bundleContext = null;
+        deactivateInternal();
+    }
+
+    private void deactivateInternal() {
         this.config = DEFAULT_CONFIG;
         this.vanityPathConfigurer.setConfiguration(DEFAULT_CONFIG, null);
         this.changeListenerWhiteboard.deactivate();
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
 
b/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
index 0f66b7f..04fb408 100644
--- 
a/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
+++ 
b/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
@@ -22,160 +22,204 @@ import 
org.apache.commons.collections4.bidimap.TreeBidiMap;
 import org.apache.sling.api.resource.ResourceResolverFactory;
 import 
org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import 
org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.resourceresolver.util.events.RecordingListener;
+import 
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.ServiceEventDTO;
 import org.apache.sling.serviceusermapping.ServiceUserMapper;
 import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
+import org.hamcrest.Matcher;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mockito;
 import org.mockito.internal.stubbing.defaultanswers.ReturnsSmartNulls;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceEvent;
-import org.osgi.framework.ServiceListener;
-import org.osgi.framework.ServiceReference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Predicate;
-
-import static org.junit.Assert.assertTrue;
+import static org.apache.sling.resourceresolver.util.CustomMatchers.allOf;
+import static org.apache.sling.resourceresolver.util.CustomMatchers.hasItem;
+import static 
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.registration;
+import static 
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.unregistration;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
 
-public class FactoryRegistrationHandlerTest {
+@ExtendWith(OsgiContextExtension.class)
+class FactoryRegistrationHandlerTest {
+
+    private static final int DEFAULT_TEST_ITERATIONS = 20;
+
+    private static final @NotNull Matcher<Iterable<? extends ServiceEventDTO>> 
RRF_REGISTRATION = allOf(
+            hasSize(4),
+            hasItem(registration(ResourceResolverFactory.class))
+    );
 
-    private static final Logger LOG = 
LoggerFactory.getLogger(FactoryRegistrationHandlerTest.class);
+    private static final @NotNull Matcher<Iterable<? extends ServiceEventDTO>> 
RRF_UNREGISTRATION = allOf(
+            hasSize(4),
+            hasItem(unregistration(ResourceResolverFactory.class))
+    );
 
-    public static final ReturnsSmartNulls DEFAULT_ANSWER = new 
ReturnsSmartNulls();
+    private static final @NotNull Matcher<Iterable<? extends ServiceEventDTO>> 
RRF_REREGISTRATION = allOf(
+            hasSize(8),
+            hasItem(unregistration(ResourceResolverFactory.class)),
+            hasItem(registration(ResourceResolverFactory.class))
+    );
 
-    @Rule
-    public OsgiContext osgi = new OsgiContext();
+    OsgiContext osgi = new OsgiContext();
 
     private ResourceResolverFactoryActivator activator;
 
-    @Before
-    public void setUp() {
+    @BeforeEach
+    void setUp() {
         final ResourceProviderTracker resourceProviderTracker = 
mock(ResourceProviderTracker.class);
         
doReturn(mock(ResourceProviderStorage.class)).when(resourceProviderTracker).getResourceProviderStorage();
 
+        final VanityPathConfigurer vanityPathConfigurer = 
mock(VanityPathConfigurer.class);
+        doReturn(false).when(vanityPathConfigurer).isVanityPathEnabled();
+
         activator = mock(ResourceResolverFactoryActivator.class);
         doReturn(osgi.bundleContext()).when(activator).getBundleContext();
         
doReturn(resourceProviderTracker).when(activator).getResourceProviderTracker();
         
doReturn(mock(ServiceUserMapper.class)).when(activator).getServiceUserMapper();
         doReturn(new TreeBidiMap<>()).when(activator).getVirtualURLMap();
         doReturn(null).when(activator).getEventAdmin();
-
+        
doReturn(vanityPathConfigurer).when(activator).getVanityPathConfigurer();
+        doCallRealMethod().when(activator).getRuntimeService();
     }
 
     private static <T> T mock(Class<T> classToMock) {
-        return Mockito.mock(classToMock, DEFAULT_ANSWER);
+        return Mockito.mock(classToMock, new ReturnsSmartNulls());
     }
 
-    @Test
-    public void testFactoryRegistration() throws InterruptedException {
+    @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+    void testFactoryRegistrationDeregistration() throws InterruptedException {
+        final BundleContext bundleContext = osgi.bundleContext();
         final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
         when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
 
-        try (AwaitingListener unregistration =
-                     AwaitingListener.unregistration(osgi.bundleContext(), 
ResourceResolverFactory.class)) {
-            try (FactoryRegistrationHandler factoryRegistrationHandler =
-                         new FactoryRegistrationHandler(activator, 
preconditions);
-                 AwaitingListener registration = 
AwaitingListener.registration(osgi.bundleContext(), 
ResourceResolverFactory.class)) {
+        try (FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler()) {
+
+            try (RecordingListener listener = 
RecordingListener.of(bundleContext)) {
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(RRF_REGISTRATION);
+            }
+
+            try (RecordingListener listener = 
RecordingListener.of(bundleContext)) {
+                factoryRegistrationHandler.unregisterFactory();
+                listener.assertRecorded(RRF_UNREGISTRATION);
+            }
+
+            try (RecordingListener listener = 
RecordingListener.of(bundleContext)) {
                 factoryRegistrationHandler.maybeRegisterFactory(null, null);
-                assertTrue("Expected ResourceResolverFactory service to be 
registered",
-                        registration.await(5, TimeUnit.SECONDS));
+                listener.assertRecorded(RRF_REGISTRATION);
             }
-            assertTrue("Expected ResourceResolverFactory service to be 
unregistered",
-                    unregistration.await(5, TimeUnit.SECONDS));
         }
     }
 
-    @Test
-    public void testFactoryDeregistrationWhenConditionsUnsatisfied() throws 
InterruptedException {
+    @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+    void testConditionChangeLeadingToUnregistration() throws 
InterruptedException {
         final BundleContext ctx = osgi.bundleContext();
         final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
-        try (FactoryRegistrationHandler factoryRegistrationHandler =
-                     new FactoryRegistrationHandler(activator, preconditions);
-             AwaitingListener registration = 
AwaitingListener.registration(ctx, ResourceResolverFactory.class);
-             AwaitingListener unregistration = 
AwaitingListener.unregistration(ctx, ResourceResolverFactory.class)) {
 
-            when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
-            factoryRegistrationHandler.maybeRegisterFactory(null, null);
-            assertTrue("Expected ResourceResolverFactory service to be 
registered",
-                    registration.await(5, TimeUnit.SECONDS));
+        try (FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler()) {
+            try (final RecordingListener listener = RecordingListener.of(ctx)) 
{
+                when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(RRF_REGISTRATION);
+            }
 
-            when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(false);
-            factoryRegistrationHandler.maybeRegisterFactory(null, null);
-            assertTrue("Expected ResourceResolverFactory service to be 
unregistered",
-                    unregistration.await(5, TimeUnit.SECONDS));
+            try (final RecordingListener listener = RecordingListener.of(ctx)) 
{
+                when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(false);
+                factoryRegistrationHandler.maybeRegisterFactory(null, null);
+                listener.assertRecorded(RRF_UNREGISTRATION);
+            }
         }
     }
 
-    private static class AwaitingListener implements ServiceListener, 
AutoCloseable {
 
-        private final BundleContext bundleContext;
-
-        private final Predicate<ServiceEvent> expectedEventPredicate;
+    @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+    void testReconfigurationLeadingToUnregsitration() throws 
InterruptedException {
+        final BundleContext ctx = osgi.bundleContext();
+        final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
+        when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
 
-        private final CountDownLatch latch;
+        try (final FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler()) {
+            try (RecordingListener listener = RecordingListener.of(ctx)) {
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(RRF_REGISTRATION);
+            }
 
-        public AwaitingListener(BundleContext ctx, Predicate<ServiceEvent> 
expectedEventPredicate) {
-            this(ctx, expectedEventPredicate, 1);
+            try (RecordingListener listener = RecordingListener.of(ctx)) {
+                final FactoryPreconditions failingPreconditions = 
mock(FactoryPreconditions.class); // new instance
+                when(failingPreconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(false);
+                factoryRegistrationHandler.configure(activator, 
failingPreconditions);
+                listener.assertRecorded(RRF_UNREGISTRATION);
+            }
         }
+    }
+    @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+    void testReconfigurationWithNoChanges() throws InterruptedException {
+        final BundleContext ctx = osgi.bundleContext();
+        final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
+        when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
 
-        public AwaitingListener(BundleContext ctx, Predicate<ServiceEvent> 
expectedEventPredicate, int count) {
-            this.bundleContext = ctx;
-            this.expectedEventPredicate = expectedEventPredicate;
-            this.latch = new CountDownLatch(count);
-            this.bundleContext.addServiceListener(this);
-        }
+        try (final FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler()) {
+            try (RecordingListener listener = RecordingListener.of(ctx)) {
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(RRF_REGISTRATION);
+            }
 
-        public static AwaitingListener registration(BundleContext 
bundleContext, Class<?> clazz) {
-            return new AwaitingListener(bundleContext, serviceEvent -> {
-                if (serviceEvent.getType() == ServiceEvent.REGISTERED) {
-                    return isInstance(bundleContext, 
serviceEvent.getServiceReference(), clazz);
-                }
-                return false;
-            });
+            try (RecordingListener listener = RecordingListener.of(ctx)) {
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(empty());
+            }
         }
+    }
 
-        public static AwaitingListener unregistration(BundleContext 
bundleContext, Class<?> clazz) {
-            return new AwaitingListener(bundleContext, serviceEvent -> {
-                if (serviceEvent.getType() == ServiceEvent.UNREGISTERING) {
-                    return isInstance(bundleContext, 
serviceEvent.getServiceReference(), clazz);
-                }
-                return false;
-            });
-        }
+    @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+    void testReconfigurationLeadingToReregistration() throws 
InterruptedException {
+        final BundleContext ctx = osgi.bundleContext();
 
-        private static boolean isInstance(BundleContext bundleContext, 
ServiceReference<?> ref, Class<?> clazz) {
-            try {
-                final Object service = bundleContext.getService(ref);
-                final boolean isInstance = clazz.isInstance(service);
-                LOG.info("{} == isInstance({}, {})", isInstance, 
service.getClass(), clazz);
-                return isInstance;
-            } finally {
-                bundleContext.ungetService(ref);
+        try (final FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler()) {
+            try (RecordingListener listener = RecordingListener.of(ctx)) {
+                final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
+                when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(RRF_REGISTRATION);
             }
-        }
 
-        @Override
-        public void serviceChanged(ServiceEvent event) {
-            if (expectedEventPredicate.test(event)) {
-                latch.countDown();
+            try (RecordingListener listener = RecordingListener.of(ctx)) {
+                final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
+                when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
+                factoryRegistrationHandler.configure(activator, preconditions);
+                listener.assertRecorded(allOf(RRF_REREGISTRATION));
             }
         }
+    }
 
-        public boolean await(long timeout, TimeUnit timeUnit) throws 
InterruptedException {
-            return latch.await(timeout, timeUnit);
+    @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+    void testUnregisterOnClose() throws InterruptedException {
+        final BundleContext ctx = osgi.bundleContext();
+        final FactoryPreconditions preconditions = 
mock(FactoryPreconditions.class);
+        final FactoryRegistrationHandler factoryRegistrationHandler = new 
FactoryRegistrationHandler();
+
+        try (RecordingListener listener = RecordingListener.of(ctx)) {
+            when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(true);
+            factoryRegistrationHandler.configure(activator, preconditions);
+            listener.assertRecorded(RRF_REGISTRATION);
         }
 
-        @Override
-        public void close() {
-            bundleContext.removeServiceListener(this);
+        try (RecordingListener listener = RecordingListener.of(ctx)) {
+            when(preconditions.checkPreconditions(isNull(), 
isNull())).thenReturn(false);
+            factoryRegistrationHandler.close();
+            listener.assertRecorded(allOf(RRF_UNREGISTRATION));
+
+            assertThrows(IllegalStateException.class, () -> 
factoryRegistrationHandler.configure(activator, preconditions),
+                    "Reconfiguration is no longer possible after a 
FactoryRegistrationHandler is closed.");
         }
     }
 }
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/util/CustomMatchers.java 
b/src/test/java/org/apache/sling/resourceresolver/util/CustomMatchers.java
new file mode 100644
index 0000000..1f20694
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/util/CustomMatchers.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.resourceresolver.util;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.StringDescription;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Custom version of Hamcrest matchers that fix the generics to make them 
usable.
+ */
+public class CustomMatchers {
+
+
+    @SafeVarargs
+    @NotNull
+    public static <T> Matcher<T> allOf(Matcher<? extends T> first, Matcher<? 
extends T>... more) {
+        final List<Matcher<? extends T>> matchers = 
Stream.concat(Stream.of(first), Stream.of(more)).collect(Collectors.toList());
+        return new AllOfMatcher<>(matchers);
+    }
+
+    public static <T> Matcher<Iterable<? extends T>> hasItem(T item) {
+        return hasItem(Matchers.equalTo(item));
+    }
+
+    public static <T> Matcher<Iterable<? extends T>> hasItem(Matcher<? extends 
T> elementMatcher) {
+        return new HasItemMatcher<>(elementMatcher);
+    }
+
+
+    private static <T> Stream<? extends T> toStream(Iterable<? extends T> 
iterable) {
+        return StreamSupport.stream(iterable.spliterator(), false);
+    };
+
+    private static class AllOfMatcher<T> extends TypeSafeDiagnosingMatcher<T> {
+        private final Collection<Matcher<? extends T>> matchers;
+
+        AllOfMatcher(Collection<Matcher<? extends T>> matchers) {
+            this.matchers = matchers;
+        }
+
+        @Override
+        protected boolean matchesSafely(T item, Description 
mismatchDescription) {
+            return matchers.stream()
+                    .filter(matcher -> !matcher.matches(item))
+                    .peek(matcher -> {
+                        
mismatchDescription.appendDescriptionOf(matcher).appendText(" ");
+                        matcher.describeMismatch(item, mismatchDescription);
+                    })
+                    .findFirst()
+                    .isEmpty();
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendList("(", " " + "and" + " ", ")", matchers);
+        }
+    }
+
+    private static class HasItemMatcher<T> extends 
TypeSafeDiagnosingMatcher<Iterable<? extends T>> {
+
+        private final Matcher<? extends T> elementMatcher;
+
+        public HasItemMatcher(Matcher<? extends T> elementMatcher) {
+            this.elementMatcher = elementMatcher;
+        }
+
+        @Override
+        protected boolean matchesSafely(Iterable<? extends T> iterable, 
Description mismatchDescription) {
+            if (toStream(iterable).limit(1).count() == 0) {
+                mismatchDescription.appendText("was empty");
+                return false;
+            }
+
+            if (toStream(iterable).anyMatch(elementMatcher::matches)) {
+                return true;
+            }
+
+            mismatchDescription.appendText(
+                    toStream(iterable)
+                            .map(item -> describeItemMismatch(elementMatcher, 
item))
+                            .collect(Collectors.joining(", ", "mismatches 
were: [", "]")));
+            return false;
+        }
+
+        @NotNull
+        private String describeItemMismatch(Matcher<? extends T> 
elementMatcher1, T item) {
+            final StringDescription desc = new StringDescription();
+            elementMatcher1.describeMismatch(item, desc);
+            return desc.toString();
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("an iterable containing 
").appendDescriptionOf(elementMatcher);
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/util/events/AbstractAwaitingListener.java
 
b/src/test/java/org/apache/sling/resourceresolver/util/events/AbstractAwaitingListener.java
new file mode 100644
index 0000000..6a03afb
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/resourceresolver/util/events/AbstractAwaitingListener.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.resourceresolver.util.events;
+
+import 
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.ServiceEventDTO;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+abstract class AbstractAwaitingListener implements ServiceListener, 
AutoCloseable {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AbstractAwaitingListener.class);
+
+    private final BundleContext bundleContext;
+
+    private final CountDownLatch latch;
+
+    public AbstractAwaitingListener(BundleContext ctx, int count) {
+        this.bundleContext = ctx;
+        this.latch = new CountDownLatch(count);
+    }
+
+    protected final void startListening() {
+        this.bundleContext.addServiceListener(this);
+        LOG.info("addServiceListener({})", this);
+    }
+
+    @Override
+    public void serviceChanged(ServiceEvent event) {
+        final ServiceEventDTO eventDTO = ServiceEventDTO.create(event);
+        final boolean matchingServiceEvent = isMatchingServiceEvent(event);
+        LOG.info("serviceChanged({}, {}) => {} | {}", eventDTO.getEventType(), 
eventDTO.getClasses(), matchingServiceEvent, this);
+        if (matchingServiceEvent) {
+            latch.countDown();
+        }
+    }
+
+    protected abstract boolean isMatchingServiceEvent(ServiceEvent 
serviceEvent);
+
+    protected boolean await(long timeout, TimeUnit timeUnit) throws 
InterruptedException {
+        return latch.await(timeout, timeUnit);
+    }
+
+    @Override
+    public void close() {
+        stopListening();
+    }
+
+    protected void stopListening() {
+        bundleContext.removeServiceListener(this);
+        LOG.info("removeServiceListener({})", this);
+    }
+
+}
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/util/events/RecordingListener.java
 
b/src/test/java/org/apache/sling/resourceresolver/util/events/RecordingListener.java
new file mode 100644
index 0000000..dbeb60f
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/resourceresolver/util/events/RecordingListener.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.resourceresolver.util.events;
+
+import 
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.ServiceEventDTO;
+import org.hamcrest.Matcher;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceRegistration;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class RecordingListener extends AbstractAwaitingListener {
+
+    private final Collection<ServiceEventDTO> serviceEvents = new 
ConcurrentLinkedQueue<ServiceEventDTO>();
+
+    private ServiceRegistration<RecordingListener> signalRegistration;
+
+    private final Predicate<ServiceEvent> recordingFilter;
+
+    public static RecordingListener of(BundleContext ctx) {
+        final RecordingListener recordingListener = new RecordingListener(ctx, 
serviceEvent -> true);
+        recordingListener.startListening();
+        return recordingListener;
+    }
+
+    public static RecordingListener of(BundleContext ctx, 
Predicate<ServiceEvent> recordingFilter) {
+        final RecordingListener recordingListener = new RecordingListener(ctx, 
recordingFilter);
+        recordingListener.startListening();
+        return recordingListener;
+    }
+
+    private RecordingListener(BundleContext ctx, Predicate<ServiceEvent> 
recordingFilter) {
+        super(ctx, 1);
+        this.recordingFilter = recordingFilter;
+        this.signalRegistration = ctx.registerService(RecordingListener.class, 
this,
+                new Hashtable<>(Map.of("::class::", RecordingListener.class)));
+    }
+
+    @Override
+    protected boolean isMatchingServiceEvent(ServiceEvent serviceEvent) {
+        return signalRegistration != null // is null when registering "this"
+                && serviceEvent.getServiceReference() == 
signalRegistration.getReference();
+    }
+
+    @Override
+    public void serviceChanged(ServiceEvent serviceEvent) {
+        super.serviceChanged(serviceEvent);
+        if (!isInternalEvent(serviceEvent) && 
recordingFilter.test(serviceEvent)) {
+            serviceEvents.add(ServiceEventDTO.create(serviceEvent));
+        }
+    }
+
+    private boolean isInternalEvent(ServiceEvent serviceEvent) {
+        return 
Objects.equals(serviceEvent.getServiceReference().getProperty("::class::"), 
RecordingListener.class);
+    }
+
+    public void assertRecorded(Matcher<? super Collection<? extends 
ServiceEventDTO>> serviceEventDTOMatcher) throws InterruptedException {
+        if (signalRegistration != null) {
+            final long deadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(1);
+            while (!serviceEventDTOMatcher.matches(serviceEvents) && 
System.nanoTime() < deadline) {
+                // give other threads a chance
+                Thread.yield();
+            }
+            signalRegistration.unregister();
+            if (!await(1, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("Unregistration event not 
observed within 1 second.");
+            }
+            signalRegistration = null;
+            stopListening();
+        }
+
+        assertThat("Expected ServiceEvents", serviceEvents, 
serviceEventDTOMatcher);
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/util/events/ServiceEventUtil.java
 
b/src/test/java/org/apache/sling/resourceresolver/util/events/ServiceEventUtil.java
new file mode 100644
index 0000000..16b6262
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/resourceresolver/util/events/ServiceEventUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.resourceresolver.util.events;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.osgi.framework.ServiceEvent;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class ServiceEventUtil {
+
+    public static Matcher<ServiceEventDTO> registration(Class<?> clazz) {
+        return 
Matchers.equalTo(ServiceEventDTO.create(ServiceEvent.REGISTERED, clazz));
+    }
+
+    public static Matcher<ServiceEventDTO> unregistration(Class<?> clazz) {
+        return 
Matchers.equalTo(ServiceEventDTO.create(ServiceEvent.UNREGISTERING, clazz));
+    }
+
+    public static class ServiceEventDTO {
+
+        private static final String[] SERVICE_EVENT_TYPES = new String[9];
+        static {
+            SERVICE_EVENT_TYPES[ServiceEvent.REGISTERED] = "REGISTERED";
+            SERVICE_EVENT_TYPES[ServiceEvent.UNREGISTERING] = "UNREGISTERING";
+            SERVICE_EVENT_TYPES[ServiceEvent.MODIFIED] = "MODIFIED";
+            SERVICE_EVENT_TYPES[ServiceEvent.MODIFIED_ENDMATCH] = 
"MODIFIED_ENDMATCH";
+        }
+
+        private final int eventType;
+
+        private final Set<String> classNames;
+
+        private ServiceEventDTO(int eventType, Set<String> classNames) {
+            this.eventType = eventType;
+            this.classNames = Collections.unmodifiableSet(classNames);
+        }
+
+        public static ServiceEventDTO create(int eventType, Class<?>... 
classes) {
+            final Set<String> classNames = 
Stream.of(classes).map(Class::getName).collect(Collectors.toCollection(TreeSet::new));
+            return new ServiceEventDTO(eventType, classNames);
+        }
+
+        public static ServiceEventDTO create(ServiceEvent event) {
+            final int eventType = event.getType();
+            final Object objectClass = 
event.getServiceReference().getProperty("objectClass");
+            final Set<String> classNames;
+            if (objectClass instanceof String[]) {
+                classNames = new TreeSet<>(Arrays.asList((String[]) 
objectClass));
+            } else {
+                classNames = Collections.emptySet();
+            }
+            return new ServiceEventDTO(eventType, classNames);
+        }
+
+        public String getEventType() {
+            return SERVICE_EVENT_TYPES[eventType];
+        }
+
+        public Set<String> getClasses() {
+            return classNames;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            ServiceEventDTO that = (ServiceEventDTO) o;
+            return eventType == that.eventType && Objects.equals(classNames, 
that.classNames);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(eventType, classNames);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("ServiceEvent(%s, %s)", getEventType(), 
classNames);
+        }
+    }
+}
diff --git a/src/test/resources/junit-platform.properties 
b/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000..07cb911
--- /dev/null
+++ b/src/test/resources/junit-platform.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+junit.jupiter.execution.parallel.enabled = true
+junit.jupiter.execution.parallel.mode.default = concurrent
+junit.jupiter.execution.parallel.mode.classes.default = concurrent
\ No newline at end of file

Reply via email to