You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2022/07/03 00:09:10 UTC

[commons-pool] 02/04: Clean ups.

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

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-pool.git

commit 4163d8a6e611329546e7dd2ece2d46a46e090599
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Sat Jul 2 19:18:51 2022 -0400

    Clean ups.
    
    - Remove end of line whitespace
    - PMD UselessParentheses
    - Use final
    - Use diamonds
---
 .../java/org/apache/commons/pool2/PoolUtils.java   |    2 +-
 .../commons/pool2/impl/DefaultEvictionPolicy.java  |  108 +-
 .../apache/commons/pool2/impl/EvictionTimer.java   |    4 +-
 .../commons/pool2/impl/GenericKeyedObjectPool.java |   14 +-
 .../commons/pool2/impl/GenericObjectPool.java      |   20 +-
 .../pool2/impl/SoftReferenceObjectPool.java        |    2 +-
 .../apache/commons/pool2/TestBaseObjectPool.java   |  562 +-
 .../apache/commons/pool2/TestKeyedObjectPool.java  | 1540 ++---
 .../org/apache/commons/pool2/WaiterFactory.java    |  434 +-
 .../pool2/impl/TestGenericKeyedObjectPool.java     |   92 +-
 .../commons/pool2/impl/TestGenericObjectPool.java  | 6022 ++++++++++----------
 11 files changed, 4400 insertions(+), 4400 deletions(-)

diff --git a/src/main/java/org/apache/commons/pool2/PoolUtils.java b/src/main/java/org/apache/commons/pool2/PoolUtils.java
index 4c732579..d94e4849 100644
--- a/src/main/java/org/apache/commons/pool2/PoolUtils.java
+++ b/src/main/java/org/apache/commons/pool2/PoolUtils.java
@@ -99,7 +99,7 @@ public final class PoolUtils {
             idleHighWaterMark = Math.max(idle, idleHighWaterMark);
             final float maxInterval = 15f;
             final float minutes = maxInterval +
-                    ((1f - maxInterval) / idleHighWaterMark) * idle;
+                    (1f - maxInterval) / idleHighWaterMark * idle;
             nextShrinkMillis = nowMillis + (long) (minutes * 60000f * factor);
         }
     }
diff --git a/src/main/java/org/apache/commons/pool2/impl/DefaultEvictionPolicy.java b/src/main/java/org/apache/commons/pool2/impl/DefaultEvictionPolicy.java
index caf5dfea..5af41b18 100644
--- a/src/main/java/org/apache/commons/pool2/impl/DefaultEvictionPolicy.java
+++ b/src/main/java/org/apache/commons/pool2/impl/DefaultEvictionPolicy.java
@@ -1,54 +1,54 @@
-/*
- * 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.commons.pool2.impl;
-
-import org.apache.commons.pool2.PooledObject;
-
-/**
- * Provides the default implementation of {@link EvictionPolicy} used by the pools.
- * <p>
- * Objects will be evicted if the following conditions are met:
- * </p>
- * <ul>
- * <li>The object has been idle longer than
- *     {@link GenericObjectPool#getMinEvictableIdleDuration()} /
- *     {@link GenericKeyedObjectPool#getMinEvictableIdleDuration()}</li>
- * <li>There are more than {@link GenericObjectPool#getMinIdle()} /
- *     {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} idle objects in
- *     the pool and the object has been idle for longer than
- *     {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} /
- *     {@link GenericKeyedObjectPool#getSoftMinEvictableIdleDuration()}
- * </ul>
- * <p>
- * This class is immutable and thread-safe.
- * </p>
- *
- * @param <T> the type of objects in the pool.
- *
- * @since 2.0
- */
-public class DefaultEvictionPolicy<T> implements EvictionPolicy<T> {
-
-    @Override
-    public boolean evict(final EvictionConfig config, final PooledObject<T> underTest, final int idleCount) {
-        // @formatter:off
-        return (config.getIdleSoftEvictDuration().compareTo(underTest.getIdleDuration()) < 0 &&
-                config.getMinIdle() < idleCount) ||
-                config.getIdleEvictDuration().compareTo(underTest.getIdleDuration()) < 0;
-        // @formatter:on
-    }
-}
+/*
+ * 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.commons.pool2.impl;
+
+import org.apache.commons.pool2.PooledObject;
+
+/**
+ * Provides the default implementation of {@link EvictionPolicy} used by the pools.
+ * <p>
+ * Objects will be evicted if the following conditions are met:
+ * </p>
+ * <ul>
+ * <li>The object has been idle longer than
+ *     {@link GenericObjectPool#getMinEvictableIdleDuration()} /
+ *     {@link GenericKeyedObjectPool#getMinEvictableIdleDuration()}</li>
+ * <li>There are more than {@link GenericObjectPool#getMinIdle()} /
+ *     {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} idle objects in
+ *     the pool and the object has been idle for longer than
+ *     {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} /
+ *     {@link GenericKeyedObjectPool#getSoftMinEvictableIdleDuration()}
+ * </ul>
+ * <p>
+ * This class is immutable and thread-safe.
+ * </p>
+ *
+ * @param <T> the type of objects in the pool.
+ *
+ * @since 2.0
+ */
+public class DefaultEvictionPolicy<T> implements EvictionPolicy<T> {
+
+    @Override
+    public boolean evict(final EvictionConfig config, final PooledObject<T> underTest, final int idleCount) {
+        // @formatter:off
+        return config.getIdleSoftEvictDuration().compareTo(underTest.getIdleDuration()) < 0 &&
+                config.getMinIdle() < idleCount ||
+                config.getIdleEvictDuration().compareTo(underTest.getIdleDuration()) < 0;
+        // @formatter:on
+    }
+}
diff --git a/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java b/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java
index 24e162df..b99e47e7 100644
--- a/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java
+++ b/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java
@@ -126,7 +126,7 @@ class EvictionTimer {
 
     /** Keys are weak references to tasks, values are runners managed by executor. */
     private static final HashMap<
-        WeakReference<BaseGenericObjectPool<?, ?>.Evictor>, 
+        WeakReference<BaseGenericObjectPool<?, ?>.Evictor>,
         WeakRunner<BaseGenericObjectPool<?, ?>.Evictor>> TASK_MAP = new HashMap<>(); // @GuardedBy("EvictionTimer.class")
 
     /**
@@ -159,7 +159,7 @@ class EvictionTimer {
 
     /**
      * For testing only.
-     * 
+     *
      * @return The executor.
      */
     static ScheduledThreadPoolExecutor getExecutor() {
diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
index 8c4e6646..55ad8881 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
@@ -416,8 +416,8 @@ public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGener
         assertOpen();
 
         final AbandonedConfig ac = this.abandonedConfig;
-        if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) &&
-                (getNumActive() > getMaxTotal() - 3)) {
+        if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 &&
+                getNumActive() > getMaxTotal() - 3) {
             removeAbandoned(ac);
         }
 
@@ -444,7 +444,7 @@ public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGener
                 if (blockWhenExhausted) {
                     if (p == null) {
                         try {
-                            p = borrowMaxWaitMillis < 0 ? objectDeque.getIdleObjects().takeFirst() : 
+                            p = borrowMaxWaitMillis < 0 ? objectDeque.getIdleObjects().takeFirst() :
                                 objectDeque.getIdleObjects().pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
                         } catch (InterruptedException e) {
                             throw cast(e);
@@ -604,7 +604,7 @@ public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGener
      * @param key the key to clear
      * @param reuseCapacity whether or not to reuse freed capacity
      */
-    public void clear(final K key, boolean reuseCapacity) {
+    public void clear(final K key, final boolean reuseCapacity) {
         final ObjectDeque<T> objectDeque = register(key);
         int freedCapacity = 0;
         try {
@@ -653,7 +653,7 @@ public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGener
 
         // Now iterate created map and kill the first 15% plus one to account
         // for zero
-        int itemsToRemove = ((int) (map.size() * 0.15)) + 1;
+        int itemsToRemove = (int) (map.size() * 0.15) + 1;
         final Iterator<Entry<PooledObject<T>, K>> iter = map.entrySet().iterator();
 
         while (iter.hasNext() && itemsToRemove > 0) {
@@ -1237,7 +1237,7 @@ public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGener
         if (numTests >= 0) {
             return Math.min(numTests, totalIdle);
         }
-        return (int) (Math.ceil(totalIdle / Math.abs((double) numTests)));
+        return (int) Math.ceil(totalIdle / Math.abs((double) numTests));
     }
 
     /**
@@ -1617,7 +1617,7 @@ public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGener
      *
      * @param newCapacity number of new instances to attempt to create.
      */
-    private void reuseCapacity(int newCapacity) {
+    private void reuseCapacity(final int newCapacity) {
         for (int i = 0; i < newCapacity; i++) {
             reuseCapacity();
         }
diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java
index 1eb4bb22..1e105cc2 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java
@@ -275,8 +275,8 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
         assertOpen();
 
         final AbandonedConfig ac = this.abandonedConfig;
-        if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) &&
-                (getNumActive() > getMaxTotal() - 3)) {
+        if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 &&
+                getNumActive() > getMaxTotal() - 3) {
             removeAbandoned(ac);
         }
 
@@ -305,7 +305,7 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
                     } catch (final InterruptedException e) {
                         // Don't surface exception type of internal locking mechanism.
                         throw cast(e);
-                    }  
+                    }
                 }
                 if (p == null) {
                     throw new NoSuchElementException(appendStats(
@@ -547,8 +547,8 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
 
             // Do not block more if maxWaitTimeMillis is set.
             if (create == null &&
-                (localMaxWaitTimeMillis > 0 &&
-                 System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) {
+                localMaxWaitTimeMillis > 0 &&
+                 System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis) {
                 create = Boolean.FALSE;
             }
         }
@@ -620,7 +620,7 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
      * @throws E if the factory's makeObject throws
      */
     private void ensureIdle(final int idleCount, final boolean always) throws E {
-        if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
+        if (idleCount < 1 || isClosed() || !always && !idleObjects.hasTakeWaiters()) {
             return;
         }
 
@@ -865,8 +865,8 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
         if (numTestsPerEvictionRun >= 0) {
             return Math.min(numTestsPerEvictionRun, idleObjects.size());
         }
-        return (int) (Math.ceil(idleObjects.size() /
-                Math.abs((double) numTestsPerEvictionRun)));
+        return (int) Math.ceil(idleObjects.size() /
+                Math.abs((double) numTestsPerEvictionRun));
     }
 
     /**
@@ -918,7 +918,7 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
      * Activation of this method decrements the active count and attempts to destroy the instance, using the provided
      * {@link DestroyMode}.
      * </p>
-     * 
+     *
      * @throws E if an exception occurs destroying the object
      * @throws IllegalStateException if obj does not belong to this pool
      * @since 2.9.0
@@ -1177,5 +1177,5 @@ public class GenericObjectPool<T, E extends Exception> extends BaseGenericObject
             getPooledObject(pooledObject).use();
         }
     }
-    
+
 }
diff --git a/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java
index 5e72b09a..bc5ad180 100644
--- a/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java
@@ -354,7 +354,7 @@ public class SoftReferenceObjectPool<T, E extends Exception> extends BaseObjectP
         removeClearedReferences(idleReferences.iterator());
         removeClearedReferences(allReferences.iterator());
         while (refQueue.poll() != null) {
-            // empty
+            // draining queue
         }
     }
 
diff --git a/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java b/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java
index fb20afda..0d034c7e 100644
--- a/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java
@@ -1,281 +1,281 @@
-/*
- * 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.commons.pool2;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-import org.junit.jupiter.api.Test;
-
-/**
- */
-public class TestBaseObjectPool extends TestObjectPool {
-    private static class TestObjectPool extends BaseObjectPool<Object, RuntimeException> {
-        
-        @Override
-        public Object borrowObject() {
-            return null;
-        }
-        
-        @Override
-        public void invalidateObject(final Object obj) {
-        }
-        
-        @Override
-        public void returnObject(final Object obj) {
-        }
-    }
-
-    private ObjectPool<String, RuntimeException> pool;
-
-    /**
-     * @param n Ignored by this implemented. Used by sub-classes.
-     *
-     * @return the Nth object (zero indexed)
-     */
-    protected Object getNthObject(final int n) {
-        if (this.getClass() != TestBaseObjectPool.class) {
-            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
-        }
-        throw new UnsupportedOperationException("BaseObjectPool isn't a complete implementation.");
-    }
-
-    protected boolean isFifo() {
-        if (this.getClass() != TestBaseObjectPool.class) {
-            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
-        }
-        return false;
-    }
-
-    protected boolean isLifo() {
-        if (this.getClass() != TestBaseObjectPool.class) {
-            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
-        }
-        return false;
-    }
-
-    /**
-     * @param minCapacity Ignored by this implemented. Used by sub-classes.
-     *
-     * @return A newly created empty pool
-     */
-    protected <E extends Exception> ObjectPool<String, E> makeEmptyPool(final int minCapacity) {
-        if (this.getClass() != TestBaseObjectPool.class) {
-            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
-        }
-        throw new UnsupportedOperationException("BaseObjectPool isn't a complete implementation.");
-    }
-
-    @Override
-    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> factory) {
-        if (this.getClass() != TestBaseObjectPool.class) {
-            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
-        }
-        throw new UnsupportedOperationException("BaseObjectPool isn't a complete implementation.");
-    }
-
-    @Test
-    public void testBaseAddObject() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        try {
-            assertEquals(0, pool.getNumIdle());
-            assertEquals(0, pool.getNumActive());
-            pool.addObject();
-            assertEquals(1, pool.getNumIdle());
-            assertEquals(0, pool.getNumActive());
-            final String obj = pool.borrowObject();
-            assertEquals(getNthObject(0), obj);
-            assertEquals(0, pool.getNumIdle());
-            assertEquals(1, pool.getNumActive());
-            pool.returnObject(obj);
-            assertEquals(1, pool.getNumIdle());
-            assertEquals(0, pool.getNumActive());
-        } catch(final UnsupportedOperationException e) {
-            return; // skip this test if one of those calls is unsupported
-        } finally {
-            pool.close();
-        }
-    }
-
-    @Test
-    public void testBaseBorrow() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        assertEquals(getNthObject(0), pool.borrowObject());
-        assertEquals(getNthObject(1), pool.borrowObject());
-        assertEquals(getNthObject(2), pool.borrowObject());
-        pool.close();
-    }
-
-    @Test
-    public void testBaseBorrowReturn() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        String obj0 = pool.borrowObject();
-        assertEquals(getNthObject(0), obj0);
-        String obj1 = pool.borrowObject();
-        assertEquals(getNthObject(1), obj1);
-        String obj2 = pool.borrowObject();
-        assertEquals(getNthObject(2), obj2);
-        pool.returnObject(obj2);
-        obj2 = pool.borrowObject();
-        assertEquals(getNthObject(2), obj2);
-        pool.returnObject(obj1);
-        obj1 = pool.borrowObject();
-        assertEquals(getNthObject(1), obj1);
-        pool.returnObject(obj0);
-        pool.returnObject(obj2);
-        obj2 = pool.borrowObject();
-        if (isLifo()) {
-            assertEquals(getNthObject(2),obj2);
-        }
-        if (isFifo()) {
-            assertEquals(getNthObject(0),obj2);
-        }
-
-        obj0 = pool.borrowObject();
-        if (isLifo()) {
-            assertEquals(getNthObject(0),obj0);
-        }
-        if (isFifo()) {
-            assertEquals(getNthObject(2),obj0);
-        }
-        pool.close();
-    }
-
-    @Test
-    public void testBaseClear() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch (final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        assertEquals(0, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        final String obj0 = pool.borrowObject();
-        final String obj1 = pool.borrowObject();
-        assertEquals(2, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        pool.returnObject(obj1);
-        pool.returnObject(obj0);
-        assertEquals(0, pool.getNumActive());
-        assertEquals(2, pool.getNumIdle());
-        pool.clear();
-        assertEquals(0, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        final Object obj2 = pool.borrowObject();
-        assertEquals(getNthObject(2), obj2);
-        pool.close();
-    }
-
-    @Test
-    public void testBaseClosePool() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        final String obj = pool.borrowObject();
-        pool.returnObject(obj);
-
-        pool.close();
-        assertThrows(IllegalStateException.class, pool::borrowObject);
-    }
-
-    @Test
-    public void testBaseInvalidateObject() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch (final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        assertEquals(0, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        final String obj0 = pool.borrowObject();
-        final String obj1 = pool.borrowObject();
-        assertEquals(2, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        pool.invalidateObject(obj0);
-        assertEquals(1, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        pool.invalidateObject(obj1);
-        assertEquals(0, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        pool.close();
-    }
-
-    @Test
-    public void testBaseNumActiveNumIdle() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch (final UnsupportedOperationException e) {
-            return; // skip this test if unsupported
-        }
-        assertEquals(0, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        final String obj0 = pool.borrowObject();
-        assertEquals(1, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        final String obj1 = pool.borrowObject();
-        assertEquals(2, pool.getNumActive());
-        assertEquals(0, pool.getNumIdle());
-        pool.returnObject(obj1);
-        assertEquals(1, pool.getNumActive());
-        assertEquals(1, pool.getNumIdle());
-        pool.returnObject(obj0);
-        assertEquals(0, pool.getNumActive());
-        assertEquals(2, pool.getNumIdle());
-        pool.close();
-    }
-
-    @Test
-    public void testClose() {
-        @SuppressWarnings("resource")
-        final ObjectPool<Object, RuntimeException> pool = new TestObjectPool();
-
-        pool.close();
-        pool.close(); // should not error as of Pool 2.0.
-    }
-
-    // tests
-    @Test
-    public void testUnsupportedOperations() {
-        if (!getClass().equals(TestBaseObjectPool.class)) {
-            return; // skip redundant tests
-        }
-        try (final ObjectPool<Object,RuntimeException> pool = new TestObjectPool()) {
-
-            assertTrue( pool.getNumIdle() < 0,"Negative expected.");
-            assertTrue( pool.getNumActive() < 0,"Negative expected.");
-
-            assertThrows(UnsupportedOperationException.class, pool::clear);
-            assertThrows(UnsupportedOperationException.class, pool::addObject);
-        }
-    }
-}
+/*
+ * 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.commons.pool2;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ */
+public class TestBaseObjectPool extends TestObjectPool {
+    private static class TestObjectPool extends BaseObjectPool<Object, RuntimeException> {
+
+        @Override
+        public Object borrowObject() {
+            return null;
+        }
+
+        @Override
+        public void invalidateObject(final Object obj) {
+        }
+
+        @Override
+        public void returnObject(final Object obj) {
+        }
+    }
+
+    private ObjectPool<String, RuntimeException> pool;
+
+    /**
+     * @param n Ignored by this implemented. Used by sub-classes.
+     *
+     * @return the Nth object (zero indexed)
+     */
+    protected Object getNthObject(final int n) {
+        if (this.getClass() != TestBaseObjectPool.class) {
+            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
+        }
+        throw new UnsupportedOperationException("BaseObjectPool isn't a complete implementation.");
+    }
+
+    protected boolean isFifo() {
+        if (this.getClass() != TestBaseObjectPool.class) {
+            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
+        }
+        return false;
+    }
+
+    protected boolean isLifo() {
+        if (this.getClass() != TestBaseObjectPool.class) {
+            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
+        }
+        return false;
+    }
+
+    /**
+     * @param minCapacity Ignored by this implemented. Used by sub-classes.
+     *
+     * @return A newly created empty pool
+     */
+    protected <E extends Exception> ObjectPool<String, E> makeEmptyPool(final int minCapacity) {
+        if (this.getClass() != TestBaseObjectPool.class) {
+            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
+        }
+        throw new UnsupportedOperationException("BaseObjectPool isn't a complete implementation.");
+    }
+
+    @Override
+    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> factory) {
+        if (this.getClass() != TestBaseObjectPool.class) {
+            fail("Subclasses of TestBaseObjectPool must reimplement this method.");
+        }
+        throw new UnsupportedOperationException("BaseObjectPool isn't a complete implementation.");
+    }
+
+    @Test
+    public void testBaseAddObject() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        try {
+            assertEquals(0, pool.getNumIdle());
+            assertEquals(0, pool.getNumActive());
+            pool.addObject();
+            assertEquals(1, pool.getNumIdle());
+            assertEquals(0, pool.getNumActive());
+            final String obj = pool.borrowObject();
+            assertEquals(getNthObject(0), obj);
+            assertEquals(0, pool.getNumIdle());
+            assertEquals(1, pool.getNumActive());
+            pool.returnObject(obj);
+            assertEquals(1, pool.getNumIdle());
+            assertEquals(0, pool.getNumActive());
+        } catch(final UnsupportedOperationException e) {
+            return; // skip this test if one of those calls is unsupported
+        } finally {
+            pool.close();
+        }
+    }
+
+    @Test
+    public void testBaseBorrow() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        assertEquals(getNthObject(0), pool.borrowObject());
+        assertEquals(getNthObject(1), pool.borrowObject());
+        assertEquals(getNthObject(2), pool.borrowObject());
+        pool.close();
+    }
+
+    @Test
+    public void testBaseBorrowReturn() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        String obj0 = pool.borrowObject();
+        assertEquals(getNthObject(0), obj0);
+        String obj1 = pool.borrowObject();
+        assertEquals(getNthObject(1), obj1);
+        String obj2 = pool.borrowObject();
+        assertEquals(getNthObject(2), obj2);
+        pool.returnObject(obj2);
+        obj2 = pool.borrowObject();
+        assertEquals(getNthObject(2), obj2);
+        pool.returnObject(obj1);
+        obj1 = pool.borrowObject();
+        assertEquals(getNthObject(1), obj1);
+        pool.returnObject(obj0);
+        pool.returnObject(obj2);
+        obj2 = pool.borrowObject();
+        if (isLifo()) {
+            assertEquals(getNthObject(2),obj2);
+        }
+        if (isFifo()) {
+            assertEquals(getNthObject(0),obj2);
+        }
+
+        obj0 = pool.borrowObject();
+        if (isLifo()) {
+            assertEquals(getNthObject(0),obj0);
+        }
+        if (isFifo()) {
+            assertEquals(getNthObject(2),obj0);
+        }
+        pool.close();
+    }
+
+    @Test
+    public void testBaseClear() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch (final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        assertEquals(0, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        final String obj0 = pool.borrowObject();
+        final String obj1 = pool.borrowObject();
+        assertEquals(2, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        pool.returnObject(obj1);
+        pool.returnObject(obj0);
+        assertEquals(0, pool.getNumActive());
+        assertEquals(2, pool.getNumIdle());
+        pool.clear();
+        assertEquals(0, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        final Object obj2 = pool.borrowObject();
+        assertEquals(getNthObject(2), obj2);
+        pool.close();
+    }
+
+    @Test
+    public void testBaseClosePool() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        final String obj = pool.borrowObject();
+        pool.returnObject(obj);
+
+        pool.close();
+        assertThrows(IllegalStateException.class, pool::borrowObject);
+    }
+
+    @Test
+    public void testBaseInvalidateObject() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch (final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        assertEquals(0, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        final String obj0 = pool.borrowObject();
+        final String obj1 = pool.borrowObject();
+        assertEquals(2, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        pool.invalidateObject(obj0);
+        assertEquals(1, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        pool.invalidateObject(obj1);
+        assertEquals(0, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        pool.close();
+    }
+
+    @Test
+    public void testBaseNumActiveNumIdle() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch (final UnsupportedOperationException e) {
+            return; // skip this test if unsupported
+        }
+        assertEquals(0, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        final String obj0 = pool.borrowObject();
+        assertEquals(1, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        final String obj1 = pool.borrowObject();
+        assertEquals(2, pool.getNumActive());
+        assertEquals(0, pool.getNumIdle());
+        pool.returnObject(obj1);
+        assertEquals(1, pool.getNumActive());
+        assertEquals(1, pool.getNumIdle());
+        pool.returnObject(obj0);
+        assertEquals(0, pool.getNumActive());
+        assertEquals(2, pool.getNumIdle());
+        pool.close();
+    }
+
+    @Test
+    public void testClose() {
+        @SuppressWarnings("resource")
+        final ObjectPool<Object, RuntimeException> pool = new TestObjectPool();
+
+        pool.close();
+        pool.close(); // should not error as of Pool 2.0.
+    }
+
+    // tests
+    @Test
+    public void testUnsupportedOperations() {
+        if (!getClass().equals(TestBaseObjectPool.class)) {
+            return; // skip redundant tests
+        }
+        try (final ObjectPool<Object,RuntimeException> pool = new TestObjectPool()) {
+
+            assertTrue( pool.getNumIdle() < 0,"Negative expected.");
+            assertTrue( pool.getNumActive() < 0,"Negative expected.");
+
+            assertThrows(UnsupportedOperationException.class, pool::clear);
+            assertThrows(UnsupportedOperationException.class, pool::addObject);
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java
index 7f558fb5..aec7c0fb 100644
--- a/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java
@@ -1,770 +1,770 @@
-/*
- * 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.commons.pool2;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.fail;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import org.apache.commons.pool2.impl.DefaultPooledObject;
-import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Test;
-
-/**
- * Abstract test case for {@link ObjectPool} implementations.
- */
-public abstract class TestKeyedObjectPool {
-
-    protected static class FailingKeyedPooledObjectFactory implements KeyedPooledObjectFactory<Object, Object, PrivateException> {
-        private final List<MethodCall> methodCalls = new ArrayList<>();
-        private int count;
-        private boolean makeObjectFail;
-        private boolean activateObjectFail;
-        private boolean validateObjectFail;
-        private boolean passivateObjectFail;
-        private boolean destroyObjectFail;
-
-        public FailingKeyedPooledObjectFactory() {
-        }
-
-        @Override
-        public void activateObject(final Object key, final PooledObject<Object> obj) {
-            methodCalls.add(new MethodCall("activateObject", key, obj.getObject()));
-            if (activateObjectFail) {
-                throw new PrivateException("activateObject");
-            }
-        }
-
-        @Override
-        public void destroyObject(final Object key, final PooledObject<Object> obj) {
-            methodCalls.add(new MethodCall("destroyObject", key, obj.getObject()));
-            if (destroyObjectFail) {
-                throw new PrivateException("destroyObject");
-            }
-        }
-
-        public int getCurrentCount() {
-            return count;
-        }
-
-        public List<MethodCall> getMethodCalls() {
-            return methodCalls;
-        }
-
-        public boolean isActivateObjectFail() {
-            return activateObjectFail;
-        }
-
-        public boolean isDestroyObjectFail() {
-            return destroyObjectFail;
-        }
-
-        public boolean isMakeObjectFail() {
-            return makeObjectFail;
-        }
-
-        public boolean isPassivateObjectFail() {
-            return passivateObjectFail;
-        }
-
-        public boolean isValidateObjectFail() {
-            return validateObjectFail;
-        }
-
-        @Override
-        public PooledObject<Object> makeObject(final Object key) {
-            final MethodCall call = new MethodCall("makeObject", key);
-            methodCalls.add(call);
-            final int originalCount = this.count++;
-            if (makeObjectFail) {
-                throw new PrivateException("makeObject");
-            }
-            // Deliberate choice to create new object in case future unit test
-            // checks for a specific object
-            final Integer obj = Integer.valueOf(originalCount);
-            call.setReturned(obj);
-            return new DefaultPooledObject<>(obj);
-        }
-
-        @Override
-        public void passivateObject(final Object key, final PooledObject<Object> obj) {
-            methodCalls.add(new MethodCall("passivateObject", key, obj.getObject()));
-            if (passivateObjectFail) {
-                throw new PrivateException("passivateObject");
-            }
-        }
-
-        public void reset() {
-            count = 0;
-            getMethodCalls().clear();
-            setMakeObjectFail(false);
-            setActivateObjectFail(false);
-            setValidateObjectFail(false);
-            setPassivateObjectFail(false);
-            setDestroyObjectFail(false);
-        }
-
-        public void setActivateObjectFail(final boolean activateObjectFail) {
-            this.activateObjectFail = activateObjectFail;
-        }
-
-        public void setCurrentCount(final int count) {
-            this.count = count;
-        }
-
-        public void setDestroyObjectFail(final boolean destroyObjectFail) {
-            this.destroyObjectFail = destroyObjectFail;
-        }
-
-        public void setMakeObjectFail(final boolean makeObjectFail) {
-            this.makeObjectFail = makeObjectFail;
-        }
-
-        public void setPassivateObjectFail(final boolean passivateObjectFail) {
-            this.passivateObjectFail = passivateObjectFail;
-        }
-
-        public void setValidateObjectFail(final boolean validateObjectFail) {
-            this.validateObjectFail = validateObjectFail;
-        }
-
-        @Override
-        public boolean validateObject(final Object key, final PooledObject<Object> obj) {
-            final MethodCall call = new MethodCall("validateObject", key, obj.getObject());
-            methodCalls.add(call);
-            if (validateObjectFail) {
-                throw new PrivateException("validateObject");
-            }
-            final boolean r = true;
-            call.returned(Boolean.valueOf(r));
-            return r;
-        }
-    }
-
-    private static class TestFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
-        @Override
-        public Object create(final Object key) {
-            return new Object();
-        }
-        @Override
-        public PooledObject<Object> wrap(final Object value) {
-            return new DefaultPooledObject<>(value);
-        }
-    }
-
-    protected static final String KEY = "key";
-
-    private KeyedObjectPool<Object, Object, RuntimeException> pool;
-
-    // Deliberate choice to create a new object in case future unit tests check
-    // for a specific object.
-    private final Integer ZERO = Integer.valueOf(0);
-
-    private final Integer ONE = Integer.valueOf(1);
-
-    private void clear(final FailingKeyedPooledObjectFactory factory, final List<MethodCall> expectedMethods) {
-        factory.getMethodCalls().clear();
-        expectedMethods.clear();
-    }
-
-    /**
-     * Return what we expect to be the n<sup>th</sup>
-     * object (zero indexed) created by the pool
-     * for the given key.
-     * @param key Key for the object to be obtained
-     * @param n   index of the object to be obtained
-     *
-     * @return the requested object
-     */
-    protected abstract Object getNthObject(Object key, int n);
-
-    protected abstract boolean isFifo();
-
-    protected abstract boolean isLifo();
-
-    /**
-     * Creates an {@link KeyedObjectPool} instance
-     * that can contain at least <i>minCapacity</i>
-     * idle and active objects, or
-     * throw {@link IllegalArgumentException}
-     * if such a pool cannot be created.
-     * @param minCapacity Minimum capacity of the pool to create
-     *
-     * @return the newly created keyed object pool
-     */
-    protected abstract <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(int minCapacity);
-
-    /**
-     * Creates an {@code KeyedObjectPool} with the specified factory.
-     * The pool should be in a default configuration and conform to the expected
-     * behaviors described in {@link KeyedObjectPool}.
-     * Generally speaking there should be no limits on the various object counts.
-     *
-     * @param <E> The type of exception thrown by the pool 
-     * @param factory Factory to use to associate with the pool
-     * @return The newly created empty pool
-     */
-    protected abstract <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(KeyedPooledObjectFactory<Object, Object, E> factory);
-
-    protected abstract Object makeKey(int n);
-
-    private <E extends Exception> void reset(final KeyedObjectPool<Object, Object, E> pool, final FailingKeyedPooledObjectFactory factory,
-        final List<MethodCall> expectedMethods) throws E {
-        pool.clear();
-        clear(factory, expectedMethods);
-        factory.reset();
-    }
-
-    @AfterEach
-    public void tearDown() {
-        pool = null;
-    }
-
-    @Test
-    public void testBaseAddObject() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object key = makeKey(0);
-        try {
-            assertEquals(0,pool.getNumIdle());
-            assertEquals(0,pool.getNumActive());
-            assertEquals(0,pool.getNumIdle(key));
-            assertEquals(0,pool.getNumActive(key));
-            pool.addObject(key);
-            assertEquals(1,pool.getNumIdle());
-            assertEquals(0,pool.getNumActive());
-            assertEquals(1,pool.getNumIdle(key));
-            assertEquals(0,pool.getNumActive(key));
-            final Object obj = pool.borrowObject(key);
-            assertEquals(getNthObject(key,0),obj);
-            assertEquals(0,pool.getNumIdle());
-            assertEquals(1,pool.getNumActive());
-            assertEquals(0,pool.getNumIdle(key));
-            assertEquals(1,pool.getNumActive(key));
-            pool.returnObject(key,obj);
-            assertEquals(1,pool.getNumIdle());
-            assertEquals(0,pool.getNumActive());
-            assertEquals(1,pool.getNumIdle(key));
-            assertEquals(0,pool.getNumActive(key));
-        } catch(final UnsupportedOperationException e) {
-            return; // skip this test if one of those calls is unsupported
-        } finally {
-            pool.close();
-        }
-    }
-
-    @Test
-    public void testBaseBorrow() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object keya = makeKey(0);
-        final Object keyb = makeKey(1);
-        assertEquals(getNthObject(keya,0),pool.borrowObject(keya),"1");
-        assertEquals(getNthObject(keyb,0),pool.borrowObject(keyb),"2");
-        assertEquals(getNthObject(keyb,1),pool.borrowObject(keyb),"3");
-        assertEquals(getNthObject(keya,1),pool.borrowObject(keya),"4");
-        assertEquals(getNthObject(keyb,2),pool.borrowObject(keyb),"5");
-        assertEquals(getNthObject(keya,2),pool.borrowObject(keya),"6");
-        pool.close();
-    }
-
-    @Test
-    public void testBaseBorrowReturn() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object keya = makeKey(0);
-        Object obj0 = pool.borrowObject(keya);
-        assertEquals(getNthObject(keya,0),obj0);
-        Object obj1 = pool.borrowObject(keya);
-        assertEquals(getNthObject(keya,1),obj1);
-        Object obj2 = pool.borrowObject(keya);
-        assertEquals(getNthObject(keya,2),obj2);
-        pool.returnObject(keya,obj2);
-        obj2 = pool.borrowObject(keya);
-        assertEquals(getNthObject(keya,2),obj2);
-        pool.returnObject(keya,obj1);
-        obj1 = pool.borrowObject(keya);
-        assertEquals(getNthObject(keya,1),obj1);
-        pool.returnObject(keya,obj0);
-        pool.returnObject(keya,obj2);
-        obj2 = pool.borrowObject(keya);
-        if (isLifo()) {
-            assertEquals(getNthObject(keya,2),obj2);
-        }
-        if (isFifo()) {
-            assertEquals(getNthObject(keya,0),obj2);
-        }
-        obj0 = pool.borrowObject(keya);
-        if (isLifo()) {
-            assertEquals(getNthObject(keya,0),obj0);
-        }
-        if (isFifo()) {
-            assertEquals(getNthObject(keya,2),obj0);
-        }
-        pool.close();
-    }
-
-    @Test
-    public void testBaseClear() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object keya = makeKey(0);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        final Object obj0 = pool.borrowObject(keya);
-        final Object obj1 = pool.borrowObject(keya);
-        assertEquals(2,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        pool.returnObject(keya,obj1);
-        pool.returnObject(keya,obj0);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(2,pool.getNumIdle(keya));
-        pool.clear(keya);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        final Object obj2 = pool.borrowObject(keya);
-        assertEquals(getNthObject(keya,2),obj2);
-        pool.close();
-    }
-
-    @Test
-    public void testBaseInvalidateObject() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object keya = makeKey(0);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        final Object obj0 = pool.borrowObject(keya);
-        final Object obj1 = pool.borrowObject(keya);
-        assertEquals(2,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        pool.invalidateObject(keya,obj0);
-        assertEquals(1,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        pool.invalidateObject(keya,obj1);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        pool.close();
-    }
-
-    @Test
-    public void testBaseNumActiveNumIdle() {
-        try {
-            pool = makeEmptyPool(3);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object keya = makeKey(0);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        final Object obj0 = pool.borrowObject(keya);
-        assertEquals(1,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        final Object obj1 = pool.borrowObject(keya);
-        assertEquals(2,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        pool.returnObject(keya,obj1);
-        assertEquals(1,pool.getNumActive(keya));
-        assertEquals(1,pool.getNumIdle(keya));
-        pool.returnObject(keya,obj0);
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(2,pool.getNumIdle(keya));
-
-        assertEquals(0,pool.getNumActive("xyzzy12345"));
-        assertEquals(0,pool.getNumIdle("xyzzy12345"));
-
-        pool.close();
-    }
-
-    @Test
-    public void testBaseNumActiveNumIdle2() {
-        try {
-            pool = makeEmptyPool(6);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // skip this test if unsupported
-        }
-        final Object keya = makeKey(0);
-        final Object keyb = makeKey(1);
-        assertEquals(0,pool.getNumActive());
-        assertEquals(0,pool.getNumIdle());
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        assertEquals(0,pool.getNumActive(keyb));
-        assertEquals(0,pool.getNumIdle(keyb));
-
-        final Object objA0 = pool.borrowObject(keya);
-        final Object objB0 = pool.borrowObject(keyb);
-
-        assertEquals(2,pool.getNumActive());
-        assertEquals(0,pool.getNumIdle());
-        assertEquals(1,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        assertEquals(1,pool.getNumActive(keyb));
-        assertEquals(0,pool.getNumIdle(keyb));
-
-        final Object objA1 = pool.borrowObject(keya);
-        final Object objB1 = pool.borrowObject(keyb);
-
-        assertEquals(4,pool.getNumActive());
-        assertEquals(0,pool.getNumIdle());
-        assertEquals(2,pool.getNumActive(keya));
-        assertEquals(0,pool.getNumIdle(keya));
-        assertEquals(2,pool.getNumActive(keyb));
-        assertEquals(0,pool.getNumIdle(keyb));
-
-        pool.returnObject(keya,objA0);
-        pool.returnObject(keyb,objB0);
-
-        assertEquals(2,pool.getNumActive());
-        assertEquals(2,pool.getNumIdle());
-        assertEquals(1,pool.getNumActive(keya));
-        assertEquals(1,pool.getNumIdle(keya));
-        assertEquals(1,pool.getNumActive(keyb));
-        assertEquals(1,pool.getNumIdle(keyb));
-
-        pool.returnObject(keya,objA1);
-        pool.returnObject(keyb,objB1);
-
-        assertEquals(0,pool.getNumActive());
-        assertEquals(4,pool.getNumIdle());
-        assertEquals(0,pool.getNumActive(keya));
-        assertEquals(2,pool.getNumIdle(keya));
-        assertEquals(0,pool.getNumActive(keyb));
-        assertEquals(2,pool.getNumIdle(keyb));
-
-        pool.close();
-    }
-
-    @Test
-    public void testClosedPoolBehavior() {
-        final KeyedObjectPool<Object, Object, RuntimeException> pool;
-        try {
-            pool = makeEmptyPool(new TestFactory());
-        } catch(final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-
-        final Object o1 = pool.borrowObject(KEY);
-        final Object o2 = pool.borrowObject(KEY);
-
-        pool.close();
-
-        assertThrows(IllegalStateException.class, () -> pool.addObject(KEY),
-                "A closed pool must throw an IllegalStateException when addObject is called.");
-
-        assertThrows(IllegalStateException.class, () -> pool.borrowObject(KEY),
-                "A closed pool must throw an IllegalStateException when borrowObject is called.");
-
-        // The following should not throw exceptions just because the pool is closed.
-        assertEquals( 0, pool.getNumIdle(KEY),"A closed pool shouldn't have any idle objects.");
-        assertEquals( 0, pool.getNumIdle(),"A closed pool shouldn't have any idle objects.");
-        pool.getNumActive();
-        pool.getNumActive(KEY);
-        pool.returnObject(KEY, o1);
-        assertEquals( 0, pool.getNumIdle(KEY),"returnObject should not add items back into the idle object pool for a closed pool.");
-        assertEquals( 0, pool.getNumIdle(),"returnObject should not add items back into the idle object pool for a closed pool.");
-        pool.invalidateObject(KEY, o2);
-        pool.clear(KEY);
-        pool.clear();
-        pool.close();
-    }
-
-    @Test
-    public void testKPOFAddObjectUsage() {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object, Object, PrivateException> pool;
-        try {
-            pool = makeEmptyPool(factory);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-        final List<MethodCall> expectedMethods = new ArrayList<>();
-
-        // addObject should make a new object, passivate it and put it in the pool
-        pool.addObject(KEY);
-        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ZERO));
-        expectedMethods.add(new MethodCall("passivateObject", KEY, ZERO));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-
-        // Test exception handling of addObject
-        reset(pool, factory, expectedMethods);
-
-        // makeObject Exceptions should be propagated to client code from addObject
-        factory.setMakeObjectFail(true);
-        assertThrows(PrivateException.class, () -> pool.addObject(KEY), "Expected addObject to propagate makeObject exception.");
-        expectedMethods.add(new MethodCall("makeObject", KEY));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-
-        clear(factory, expectedMethods);
-
-        // passivateObject Exceptions should be propagated to client code from addObject
-        factory.setMakeObjectFail(false);
-        factory.setPassivateObjectFail(true);
-        assertThrows(PrivateException.class, () -> pool.addObject(KEY), "Expected addObject to propagate passivateObject exception.");
-        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ONE));
-        expectedMethods.add(new MethodCall("passivateObject", KEY, ONE));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-        pool.close();
-    }
-
-    @Test
-    public void testKPOFBorrowObjectUsages() {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object, Object, PrivateException> pool;
-        try {
-            pool = makeEmptyPool(factory);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-        final List<MethodCall> expectedMethods = new ArrayList<>();
-        Object obj;
-
-        if (pool instanceof GenericKeyedObjectPool) {
-            ((GenericKeyedObjectPool<Object, Object, PrivateException>) pool).setTestOnBorrow(true);
-        }
-
-        // Test correct behavior code paths
-
-        // existing idle object should be activated and validated
-        pool.addObject(KEY);
-        clear(factory, expectedMethods);
-        obj = pool.borrowObject(KEY);
-        expectedMethods.add(new MethodCall("activateObject", KEY, ZERO));
-        expectedMethods.add(new MethodCall("validateObject", KEY, ZERO).returned(Boolean.TRUE));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-        pool.returnObject(KEY, obj);
-
-        // Test exception handling of borrowObject
-        reset(pool, factory, expectedMethods);
-
-        // makeObject Exceptions should be propagated to client code from borrowObject
-        factory.setMakeObjectFail(true);
-        try {
-            obj = pool.borrowObject(KEY);
-            fail("Expected borrowObject to propagate makeObject exception.");
-        } catch (final PrivateException pe) {
-            // expected
-        }
-        expectedMethods.add(new MethodCall("makeObject", KEY));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-
-
-        // when activateObject fails in borrowObject, a new object should be borrowed/created
-        reset(pool, factory, expectedMethods);
-        pool.addObject(KEY);
-        clear(factory, expectedMethods);
-
-        factory.setActivateObjectFail(true);
-        expectedMethods.add(new MethodCall("activateObject", KEY, obj));
-        assertThrows(NoSuchElementException.class, () -> pool.borrowObject(KEY));
-        // After idle object fails validation, new on is created and activation
-        // fails again for the new one.
-        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ONE));
-        expectedMethods.add(new MethodCall("activateObject", KEY, ONE));
-        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls()); // The exact timing of destroyObject is flexible here.
-        assertEquals(expectedMethods, factory.getMethodCalls());
-
-        // when validateObject fails in borrowObject, a new object should be borrowed/created
-        reset(pool, factory, expectedMethods);
-        pool.addObject(KEY);
-        clear(factory, expectedMethods);
-
-        factory.setValidateObjectFail(true);
-        // testOnBorrow is on, so this will throw when the newly created instance
-        // fails validation
-        assertThrows(NoSuchElementException.class, () -> pool.borrowObject(KEY));
-        // Activate, then validate for idle instance
-        expectedMethods.add(new MethodCall("activateObject", KEY, ZERO));
-        expectedMethods.add(new MethodCall("validateObject", KEY, ZERO));
-        // Make new instance, activate succeeds, validate fails
-        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ONE));
-        expectedMethods.add(new MethodCall("activateObject", KEY, ONE));
-        expectedMethods.add(new MethodCall("validateObject", KEY, ONE));
-        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls());
-        assertEquals(expectedMethods, factory.getMethodCalls());
-        pool.close();
-    }
-
-    @Test
-    public void testKPOFClearUsages() {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object, Object, PrivateException> pool;
-        try {
-            pool = makeEmptyPool(factory);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-        final List<MethodCall> expectedMethods = new ArrayList<>();
-
-        // Test correct behavior code paths
-        pool.addObjects(KEY, 5);
-        pool.clear();
-
-        // Test exception handling clear should swallow destroy object failures
-        reset(pool, factory, expectedMethods);
-        factory.setDestroyObjectFail(true);
-        pool.addObjects(KEY, 5);
-        pool.clear();
-        pool.close();
-    }
-
-
-    @Test
-    public void testKPOFCloseUsages() {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        KeyedObjectPool<Object, Object, PrivateException> pool;
-        try {
-            pool = makeEmptyPool(factory);
-        } catch (final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-        final List<MethodCall> expectedMethods = new ArrayList<>();
-
-        // Test correct behavior code paths
-        pool.addObjects(KEY, 5);
-        pool.close();
-
-        // Test exception handling close should swallow failures
-        try (final KeyedObjectPool<Object, Object, PrivateException> pool2 = makeEmptyPool(factory)) {
-            reset(pool2, factory, expectedMethods);
-            factory.setDestroyObjectFail(true);
-            pool2.addObjects(KEY, 5);
-        }
-    }
-
-    @Test
-    public void testKPOFInvalidateObjectUsages() throws InterruptedException {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object, Object, PrivateException> pool;
-        try {
-            pool = makeEmptyPool(factory);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-        final List<MethodCall> expectedMethods = new ArrayList<>();
-        Object obj;
-
-        // Test correct behavior code paths
-
-        obj = pool.borrowObject(KEY);
-        clear(factory, expectedMethods);
-
-        // invalidated object should be destroyed
-        pool.invalidateObject(KEY, obj);
-        expectedMethods.add(new MethodCall("destroyObject", KEY, obj));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-
-        // Test exception handling of invalidateObject
-        reset(pool, factory, expectedMethods);
-        final Object obj2 = pool.borrowObject(KEY);
-        clear(factory, expectedMethods);
-        factory.setDestroyObjectFail(true);
-        assertThrows(PrivateException.class, () -> pool.invalidateObject(KEY, obj2), "Expecting destroy exception to propagate");
-        Thread.sleep(250); // could be defered
-        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls());
-        assertEquals(expectedMethods, factory.getMethodCalls());
-        pool.close();
-    }
-
-    @Test
-    public void testKPOFReturnObjectUsages() {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object, Object, PrivateException> pool;
-        try {
-            pool = makeEmptyPool(factory);
-        } catch(final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-        final List<MethodCall> expectedMethods = new ArrayList<>();
-        Object obj;
-
-        // Test correct behavior code paths
-        obj = pool.borrowObject(KEY);
-        clear(factory, expectedMethods);
-
-        // returned object should be passivated
-        pool.returnObject(KEY, obj);
-        expectedMethods.add(new MethodCall("passivateObject", KEY, obj));
-        assertEquals(expectedMethods, factory.getMethodCalls());
-
-        // Test exception handling of returnObject
-        reset(pool, factory, expectedMethods);
-
-        // passivateObject should swallow exceptions and not add the object to the pool
-        pool.addObject(KEY);
-        pool.addObject(KEY);
-        pool.addObject(KEY);
-        assertEquals(3, pool.getNumIdle(KEY));
-        obj = pool.borrowObject(KEY);
-        obj = pool.borrowObject(KEY);
-        assertEquals(1, pool.getNumIdle(KEY));
-        assertEquals(2, pool.getNumActive(KEY));
-        clear(factory, expectedMethods);
-        factory.setPassivateObjectFail(true);
-        pool.returnObject(KEY, obj);
-        expectedMethods.add(new MethodCall("passivateObject", KEY, obj));
-        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls()); // The exact timing of destroyObject is flexible here.
-        assertEquals(expectedMethods, factory.getMethodCalls());
-        assertEquals(1, pool.getNumIdle(KEY));   // Not added
-        assertEquals(1, pool.getNumActive(KEY)); // But not active
-
-        reset(pool, factory, expectedMethods);
-        obj = pool.borrowObject(KEY);
-        clear(factory, expectedMethods);
-        factory.setPassivateObjectFail(true);
-        factory.setDestroyObjectFail(true);
-        try {
-            pool.returnObject(KEY, obj);
-            if (!(pool instanceof GenericKeyedObjectPool)) { // ugh, 1.3-compat
-                fail("Expecting destroyObject exception to be propagated");
-            }
-        } catch (final PrivateException ex) {
-            // Expected
-        }
-        pool.close();
-    }
-
-    @Test
-    public void testToString() {
-        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        try (final KeyedObjectPool<Object, Object, PrivateException> pool = makeEmptyPool(factory)) {
-            pool.toString();
-        } catch (final UnsupportedOperationException uoe) {
-            return; // test not supported
-        }
-    }
-}
+/*
+ * 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.commons.pool2;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Abstract test case for {@link ObjectPool} implementations.
+ */
+public abstract class TestKeyedObjectPool {
+
+    protected static class FailingKeyedPooledObjectFactory implements KeyedPooledObjectFactory<Object, Object, PrivateException> {
+        private final List<MethodCall> methodCalls = new ArrayList<>();
+        private int count;
+        private boolean makeObjectFail;
+        private boolean activateObjectFail;
+        private boolean validateObjectFail;
+        private boolean passivateObjectFail;
+        private boolean destroyObjectFail;
+
+        public FailingKeyedPooledObjectFactory() {
+        }
+
+        @Override
+        public void activateObject(final Object key, final PooledObject<Object> obj) {
+            methodCalls.add(new MethodCall("activateObject", key, obj.getObject()));
+            if (activateObjectFail) {
+                throw new PrivateException("activateObject");
+            }
+        }
+
+        @Override
+        public void destroyObject(final Object key, final PooledObject<Object> obj) {
+            methodCalls.add(new MethodCall("destroyObject", key, obj.getObject()));
+            if (destroyObjectFail) {
+                throw new PrivateException("destroyObject");
+            }
+        }
+
+        public int getCurrentCount() {
+            return count;
+        }
+
+        public List<MethodCall> getMethodCalls() {
+            return methodCalls;
+        }
+
+        public boolean isActivateObjectFail() {
+            return activateObjectFail;
+        }
+
+        public boolean isDestroyObjectFail() {
+            return destroyObjectFail;
+        }
+
+        public boolean isMakeObjectFail() {
+            return makeObjectFail;
+        }
+
+        public boolean isPassivateObjectFail() {
+            return passivateObjectFail;
+        }
+
+        public boolean isValidateObjectFail() {
+            return validateObjectFail;
+        }
+
+        @Override
+        public PooledObject<Object> makeObject(final Object key) {
+            final MethodCall call = new MethodCall("makeObject", key);
+            methodCalls.add(call);
+            final int originalCount = this.count++;
+            if (makeObjectFail) {
+                throw new PrivateException("makeObject");
+            }
+            // Deliberate choice to create new object in case future unit test
+            // checks for a specific object
+            final Integer obj = Integer.valueOf(originalCount);
+            call.setReturned(obj);
+            return new DefaultPooledObject<>(obj);
+        }
+
+        @Override
+        public void passivateObject(final Object key, final PooledObject<Object> obj) {
+            methodCalls.add(new MethodCall("passivateObject", key, obj.getObject()));
+            if (passivateObjectFail) {
+                throw new PrivateException("passivateObject");
+            }
+        }
+
+        public void reset() {
+            count = 0;
+            getMethodCalls().clear();
+            setMakeObjectFail(false);
+            setActivateObjectFail(false);
+            setValidateObjectFail(false);
+            setPassivateObjectFail(false);
+            setDestroyObjectFail(false);
+        }
+
+        public void setActivateObjectFail(final boolean activateObjectFail) {
+            this.activateObjectFail = activateObjectFail;
+        }
+
+        public void setCurrentCount(final int count) {
+            this.count = count;
+        }
+
+        public void setDestroyObjectFail(final boolean destroyObjectFail) {
+            this.destroyObjectFail = destroyObjectFail;
+        }
+
+        public void setMakeObjectFail(final boolean makeObjectFail) {
+            this.makeObjectFail = makeObjectFail;
+        }
+
+        public void setPassivateObjectFail(final boolean passivateObjectFail) {
+            this.passivateObjectFail = passivateObjectFail;
+        }
+
+        public void setValidateObjectFail(final boolean validateObjectFail) {
+            this.validateObjectFail = validateObjectFail;
+        }
+
+        @Override
+        public boolean validateObject(final Object key, final PooledObject<Object> obj) {
+            final MethodCall call = new MethodCall("validateObject", key, obj.getObject());
+            methodCalls.add(call);
+            if (validateObjectFail) {
+                throw new PrivateException("validateObject");
+            }
+            final boolean r = true;
+            call.returned(Boolean.valueOf(r));
+            return r;
+        }
+    }
+
+    private static class TestFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
+        @Override
+        public Object create(final Object key) {
+            return new Object();
+        }
+        @Override
+        public PooledObject<Object> wrap(final Object value) {
+            return new DefaultPooledObject<>(value);
+        }
+    }
+
+    protected static final String KEY = "key";
+
+    private KeyedObjectPool<Object, Object, RuntimeException> pool;
+
+    // Deliberate choice to create a new object in case future unit tests check
+    // for a specific object.
+    private final Integer ZERO = Integer.valueOf(0);
+
+    private final Integer ONE = Integer.valueOf(1);
+
+    private void clear(final FailingKeyedPooledObjectFactory factory, final List<MethodCall> expectedMethods) {
+        factory.getMethodCalls().clear();
+        expectedMethods.clear();
+    }
+
+    /**
+     * Return what we expect to be the n<sup>th</sup>
+     * object (zero indexed) created by the pool
+     * for the given key.
+     * @param key Key for the object to be obtained
+     * @param n   index of the object to be obtained
+     *
+     * @return the requested object
+     */
+    protected abstract Object getNthObject(Object key, int n);
+
+    protected abstract boolean isFifo();
+
+    protected abstract boolean isLifo();
+
+    /**
+     * Creates an {@link KeyedObjectPool} instance
+     * that can contain at least <i>minCapacity</i>
+     * idle and active objects, or
+     * throw {@link IllegalArgumentException}
+     * if such a pool cannot be created.
+     * @param minCapacity Minimum capacity of the pool to create
+     *
+     * @return the newly created keyed object pool
+     */
+    protected abstract <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(int minCapacity);
+
+    /**
+     * Creates an {@code KeyedObjectPool} with the specified factory.
+     * The pool should be in a default configuration and conform to the expected
+     * behaviors described in {@link KeyedObjectPool}.
+     * Generally speaking there should be no limits on the various object counts.
+     *
+     * @param <E> The type of exception thrown by the pool
+     * @param factory Factory to use to associate with the pool
+     * @return The newly created empty pool
+     */
+    protected abstract <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(KeyedPooledObjectFactory<Object, Object, E> factory);
+
+    protected abstract Object makeKey(int n);
+
+    private <E extends Exception> void reset(final KeyedObjectPool<Object, Object, E> pool, final FailingKeyedPooledObjectFactory factory,
+        final List<MethodCall> expectedMethods) throws E {
+        pool.clear();
+        clear(factory, expectedMethods);
+        factory.reset();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        pool = null;
+    }
+
+    @Test
+    public void testBaseAddObject() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object key = makeKey(0);
+        try {
+            assertEquals(0,pool.getNumIdle());
+            assertEquals(0,pool.getNumActive());
+            assertEquals(0,pool.getNumIdle(key));
+            assertEquals(0,pool.getNumActive(key));
+            pool.addObject(key);
+            assertEquals(1,pool.getNumIdle());
+            assertEquals(0,pool.getNumActive());
+            assertEquals(1,pool.getNumIdle(key));
+            assertEquals(0,pool.getNumActive(key));
+            final Object obj = pool.borrowObject(key);
+            assertEquals(getNthObject(key,0),obj);
+            assertEquals(0,pool.getNumIdle());
+            assertEquals(1,pool.getNumActive());
+            assertEquals(0,pool.getNumIdle(key));
+            assertEquals(1,pool.getNumActive(key));
+            pool.returnObject(key,obj);
+            assertEquals(1,pool.getNumIdle());
+            assertEquals(0,pool.getNumActive());
+            assertEquals(1,pool.getNumIdle(key));
+            assertEquals(0,pool.getNumActive(key));
+        } catch(final UnsupportedOperationException e) {
+            return; // skip this test if one of those calls is unsupported
+        } finally {
+            pool.close();
+        }
+    }
+
+    @Test
+    public void testBaseBorrow() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object keya = makeKey(0);
+        final Object keyb = makeKey(1);
+        assertEquals(getNthObject(keya,0),pool.borrowObject(keya),"1");
+        assertEquals(getNthObject(keyb,0),pool.borrowObject(keyb),"2");
+        assertEquals(getNthObject(keyb,1),pool.borrowObject(keyb),"3");
+        assertEquals(getNthObject(keya,1),pool.borrowObject(keya),"4");
+        assertEquals(getNthObject(keyb,2),pool.borrowObject(keyb),"5");
+        assertEquals(getNthObject(keya,2),pool.borrowObject(keya),"6");
+        pool.close();
+    }
+
+    @Test
+    public void testBaseBorrowReturn() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object keya = makeKey(0);
+        Object obj0 = pool.borrowObject(keya);
+        assertEquals(getNthObject(keya,0),obj0);
+        Object obj1 = pool.borrowObject(keya);
+        assertEquals(getNthObject(keya,1),obj1);
+        Object obj2 = pool.borrowObject(keya);
+        assertEquals(getNthObject(keya,2),obj2);
+        pool.returnObject(keya,obj2);
+        obj2 = pool.borrowObject(keya);
+        assertEquals(getNthObject(keya,2),obj2);
+        pool.returnObject(keya,obj1);
+        obj1 = pool.borrowObject(keya);
+        assertEquals(getNthObject(keya,1),obj1);
+        pool.returnObject(keya,obj0);
+        pool.returnObject(keya,obj2);
+        obj2 = pool.borrowObject(keya);
+        if (isLifo()) {
+            assertEquals(getNthObject(keya,2),obj2);
+        }
+        if (isFifo()) {
+            assertEquals(getNthObject(keya,0),obj2);
+        }
+        obj0 = pool.borrowObject(keya);
+        if (isLifo()) {
+            assertEquals(getNthObject(keya,0),obj0);
+        }
+        if (isFifo()) {
+            assertEquals(getNthObject(keya,2),obj0);
+        }
+        pool.close();
+    }
+
+    @Test
+    public void testBaseClear() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object keya = makeKey(0);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        final Object obj0 = pool.borrowObject(keya);
+        final Object obj1 = pool.borrowObject(keya);
+        assertEquals(2,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        pool.returnObject(keya,obj1);
+        pool.returnObject(keya,obj0);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(2,pool.getNumIdle(keya));
+        pool.clear(keya);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        final Object obj2 = pool.borrowObject(keya);
+        assertEquals(getNthObject(keya,2),obj2);
+        pool.close();
+    }
+
+    @Test
+    public void testBaseInvalidateObject() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object keya = makeKey(0);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        final Object obj0 = pool.borrowObject(keya);
+        final Object obj1 = pool.borrowObject(keya);
+        assertEquals(2,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        pool.invalidateObject(keya,obj0);
+        assertEquals(1,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        pool.invalidateObject(keya,obj1);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        pool.close();
+    }
+
+    @Test
+    public void testBaseNumActiveNumIdle() {
+        try {
+            pool = makeEmptyPool(3);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object keya = makeKey(0);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        final Object obj0 = pool.borrowObject(keya);
+        assertEquals(1,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        final Object obj1 = pool.borrowObject(keya);
+        assertEquals(2,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        pool.returnObject(keya,obj1);
+        assertEquals(1,pool.getNumActive(keya));
+        assertEquals(1,pool.getNumIdle(keya));
+        pool.returnObject(keya,obj0);
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(2,pool.getNumIdle(keya));
+
+        assertEquals(0,pool.getNumActive("xyzzy12345"));
+        assertEquals(0,pool.getNumIdle("xyzzy12345"));
+
+        pool.close();
+    }
+
+    @Test
+    public void testBaseNumActiveNumIdle2() {
+        try {
+            pool = makeEmptyPool(6);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // skip this test if unsupported
+        }
+        final Object keya = makeKey(0);
+        final Object keyb = makeKey(1);
+        assertEquals(0,pool.getNumActive());
+        assertEquals(0,pool.getNumIdle());
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        assertEquals(0,pool.getNumActive(keyb));
+        assertEquals(0,pool.getNumIdle(keyb));
+
+        final Object objA0 = pool.borrowObject(keya);
+        final Object objB0 = pool.borrowObject(keyb);
+
+        assertEquals(2,pool.getNumActive());
+        assertEquals(0,pool.getNumIdle());
+        assertEquals(1,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        assertEquals(1,pool.getNumActive(keyb));
+        assertEquals(0,pool.getNumIdle(keyb));
+
+        final Object objA1 = pool.borrowObject(keya);
+        final Object objB1 = pool.borrowObject(keyb);
+
+        assertEquals(4,pool.getNumActive());
+        assertEquals(0,pool.getNumIdle());
+        assertEquals(2,pool.getNumActive(keya));
+        assertEquals(0,pool.getNumIdle(keya));
+        assertEquals(2,pool.getNumActive(keyb));
+        assertEquals(0,pool.getNumIdle(keyb));
+
+        pool.returnObject(keya,objA0);
+        pool.returnObject(keyb,objB0);
+
+        assertEquals(2,pool.getNumActive());
+        assertEquals(2,pool.getNumIdle());
+        assertEquals(1,pool.getNumActive(keya));
+        assertEquals(1,pool.getNumIdle(keya));
+        assertEquals(1,pool.getNumActive(keyb));
+        assertEquals(1,pool.getNumIdle(keyb));
+
+        pool.returnObject(keya,objA1);
+        pool.returnObject(keyb,objB1);
+
+        assertEquals(0,pool.getNumActive());
+        assertEquals(4,pool.getNumIdle());
+        assertEquals(0,pool.getNumActive(keya));
+        assertEquals(2,pool.getNumIdle(keya));
+        assertEquals(0,pool.getNumActive(keyb));
+        assertEquals(2,pool.getNumIdle(keyb));
+
+        pool.close();
+    }
+
+    @Test
+    public void testClosedPoolBehavior() {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool;
+        try {
+            pool = makeEmptyPool(new TestFactory());
+        } catch(final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+
+        final Object o1 = pool.borrowObject(KEY);
+        final Object o2 = pool.borrowObject(KEY);
+
+        pool.close();
+
+        assertThrows(IllegalStateException.class, () -> pool.addObject(KEY),
+                "A closed pool must throw an IllegalStateException when addObject is called.");
+
+        assertThrows(IllegalStateException.class, () -> pool.borrowObject(KEY),
+                "A closed pool must throw an IllegalStateException when borrowObject is called.");
+
+        // The following should not throw exceptions just because the pool is closed.
+        assertEquals( 0, pool.getNumIdle(KEY),"A closed pool shouldn't have any idle objects.");
+        assertEquals( 0, pool.getNumIdle(),"A closed pool shouldn't have any idle objects.");
+        pool.getNumActive();
+        pool.getNumActive(KEY);
+        pool.returnObject(KEY, o1);
+        assertEquals( 0, pool.getNumIdle(KEY),"returnObject should not add items back into the idle object pool for a closed pool.");
+        assertEquals( 0, pool.getNumIdle(),"returnObject should not add items back into the idle object pool for a closed pool.");
+        pool.invalidateObject(KEY, o2);
+        pool.clear(KEY);
+        pool.clear();
+        pool.close();
+    }
+
+    @Test
+    public void testKPOFAddObjectUsage() {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
+        try {
+            pool = makeEmptyPool(factory);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+        final List<MethodCall> expectedMethods = new ArrayList<>();
+
+        // addObject should make a new object, passivate it and put it in the pool
+        pool.addObject(KEY);
+        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ZERO));
+        expectedMethods.add(new MethodCall("passivateObject", KEY, ZERO));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+
+        // Test exception handling of addObject
+        reset(pool, factory, expectedMethods);
+
+        // makeObject Exceptions should be propagated to client code from addObject
+        factory.setMakeObjectFail(true);
+        assertThrows(PrivateException.class, () -> pool.addObject(KEY), "Expected addObject to propagate makeObject exception.");
+        expectedMethods.add(new MethodCall("makeObject", KEY));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+
+        clear(factory, expectedMethods);
+
+        // passivateObject Exceptions should be propagated to client code from addObject
+        factory.setMakeObjectFail(false);
+        factory.setPassivateObjectFail(true);
+        assertThrows(PrivateException.class, () -> pool.addObject(KEY), "Expected addObject to propagate passivateObject exception.");
+        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ONE));
+        expectedMethods.add(new MethodCall("passivateObject", KEY, ONE));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+        pool.close();
+    }
+
+    @Test
+    public void testKPOFBorrowObjectUsages() {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
+        try {
+            pool = makeEmptyPool(factory);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+        final List<MethodCall> expectedMethods = new ArrayList<>();
+        Object obj;
+
+        if (pool instanceof GenericKeyedObjectPool) {
+            ((GenericKeyedObjectPool<Object, Object, PrivateException>) pool).setTestOnBorrow(true);
+        }
+
+        // Test correct behavior code paths
+
+        // existing idle object should be activated and validated
+        pool.addObject(KEY);
+        clear(factory, expectedMethods);
+        obj = pool.borrowObject(KEY);
+        expectedMethods.add(new MethodCall("activateObject", KEY, ZERO));
+        expectedMethods.add(new MethodCall("validateObject", KEY, ZERO).returned(Boolean.TRUE));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+        pool.returnObject(KEY, obj);
+
+        // Test exception handling of borrowObject
+        reset(pool, factory, expectedMethods);
+
+        // makeObject Exceptions should be propagated to client code from borrowObject
+        factory.setMakeObjectFail(true);
+        try {
+            obj = pool.borrowObject(KEY);
+            fail("Expected borrowObject to propagate makeObject exception.");
+        } catch (final PrivateException pe) {
+            // expected
+        }
+        expectedMethods.add(new MethodCall("makeObject", KEY));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+
+
+        // when activateObject fails in borrowObject, a new object should be borrowed/created
+        reset(pool, factory, expectedMethods);
+        pool.addObject(KEY);
+        clear(factory, expectedMethods);
+
+        factory.setActivateObjectFail(true);
+        expectedMethods.add(new MethodCall("activateObject", KEY, obj));
+        assertThrows(NoSuchElementException.class, () -> pool.borrowObject(KEY));
+        // After idle object fails validation, new on is created and activation
+        // fails again for the new one.
+        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ONE));
+        expectedMethods.add(new MethodCall("activateObject", KEY, ONE));
+        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls()); // The exact timing of destroyObject is flexible here.
+        assertEquals(expectedMethods, factory.getMethodCalls());
+
+        // when validateObject fails in borrowObject, a new object should be borrowed/created
+        reset(pool, factory, expectedMethods);
+        pool.addObject(KEY);
+        clear(factory, expectedMethods);
+
+        factory.setValidateObjectFail(true);
+        // testOnBorrow is on, so this will throw when the newly created instance
+        // fails validation
+        assertThrows(NoSuchElementException.class, () -> pool.borrowObject(KEY));
+        // Activate, then validate for idle instance
+        expectedMethods.add(new MethodCall("activateObject", KEY, ZERO));
+        expectedMethods.add(new MethodCall("validateObject", KEY, ZERO));
+        // Make new instance, activate succeeds, validate fails
+        expectedMethods.add(new MethodCall("makeObject", KEY).returned(ONE));
+        expectedMethods.add(new MethodCall("activateObject", KEY, ONE));
+        expectedMethods.add(new MethodCall("validateObject", KEY, ONE));
+        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls());
+        assertEquals(expectedMethods, factory.getMethodCalls());
+        pool.close();
+    }
+
+    @Test
+    public void testKPOFClearUsages() {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
+        try {
+            pool = makeEmptyPool(factory);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+        final List<MethodCall> expectedMethods = new ArrayList<>();
+
+        // Test correct behavior code paths
+        pool.addObjects(KEY, 5);
+        pool.clear();
+
+        // Test exception handling clear should swallow destroy object failures
+        reset(pool, factory, expectedMethods);
+        factory.setDestroyObjectFail(true);
+        pool.addObjects(KEY, 5);
+        pool.clear();
+        pool.close();
+    }
+
+
+    @Test
+    public void testKPOFCloseUsages() {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        KeyedObjectPool<Object, Object, PrivateException> pool;
+        try {
+            pool = makeEmptyPool(factory);
+        } catch (final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+        final List<MethodCall> expectedMethods = new ArrayList<>();
+
+        // Test correct behavior code paths
+        pool.addObjects(KEY, 5);
+        pool.close();
+
+        // Test exception handling close should swallow failures
+        try (final KeyedObjectPool<Object, Object, PrivateException> pool2 = makeEmptyPool(factory)) {
+            reset(pool2, factory, expectedMethods);
+            factory.setDestroyObjectFail(true);
+            pool2.addObjects(KEY, 5);
+        }
+    }
+
+    @Test
+    public void testKPOFInvalidateObjectUsages() throws InterruptedException {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
+        try {
+            pool = makeEmptyPool(factory);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+        final List<MethodCall> expectedMethods = new ArrayList<>();
+        Object obj;
+
+        // Test correct behavior code paths
+
+        obj = pool.borrowObject(KEY);
+        clear(factory, expectedMethods);
+
+        // invalidated object should be destroyed
+        pool.invalidateObject(KEY, obj);
+        expectedMethods.add(new MethodCall("destroyObject", KEY, obj));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+
+        // Test exception handling of invalidateObject
+        reset(pool, factory, expectedMethods);
+        final Object obj2 = pool.borrowObject(KEY);
+        clear(factory, expectedMethods);
+        factory.setDestroyObjectFail(true);
+        assertThrows(PrivateException.class, () -> pool.invalidateObject(KEY, obj2), "Expecting destroy exception to propagate");
+        Thread.sleep(250); // could be defered
+        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls());
+        assertEquals(expectedMethods, factory.getMethodCalls());
+        pool.close();
+    }
+
+    @Test
+    public void testKPOFReturnObjectUsages() {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
+        try {
+            pool = makeEmptyPool(factory);
+        } catch(final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+        final List<MethodCall> expectedMethods = new ArrayList<>();
+        Object obj;
+
+        // Test correct behavior code paths
+        obj = pool.borrowObject(KEY);
+        clear(factory, expectedMethods);
+
+        // returned object should be passivated
+        pool.returnObject(KEY, obj);
+        expectedMethods.add(new MethodCall("passivateObject", KEY, obj));
+        assertEquals(expectedMethods, factory.getMethodCalls());
+
+        // Test exception handling of returnObject
+        reset(pool, factory, expectedMethods);
+
+        // passivateObject should swallow exceptions and not add the object to the pool
+        pool.addObject(KEY);
+        pool.addObject(KEY);
+        pool.addObject(KEY);
+        assertEquals(3, pool.getNumIdle(KEY));
+        obj = pool.borrowObject(KEY);
+        obj = pool.borrowObject(KEY);
+        assertEquals(1, pool.getNumIdle(KEY));
+        assertEquals(2, pool.getNumActive(KEY));
+        clear(factory, expectedMethods);
+        factory.setPassivateObjectFail(true);
+        pool.returnObject(KEY, obj);
+        expectedMethods.add(new MethodCall("passivateObject", KEY, obj));
+        TestObjectPool.removeDestroyObjectCall(factory.getMethodCalls()); // The exact timing of destroyObject is flexible here.
+        assertEquals(expectedMethods, factory.getMethodCalls());
+        assertEquals(1, pool.getNumIdle(KEY));   // Not added
+        assertEquals(1, pool.getNumActive(KEY)); // But not active
+
+        reset(pool, factory, expectedMethods);
+        obj = pool.borrowObject(KEY);
+        clear(factory, expectedMethods);
+        factory.setPassivateObjectFail(true);
+        factory.setDestroyObjectFail(true);
+        try {
+            pool.returnObject(KEY, obj);
+            if (!(pool instanceof GenericKeyedObjectPool)) { // ugh, 1.3-compat
+                fail("Expecting destroyObject exception to be propagated");
+            }
+        } catch (final PrivateException ex) {
+            // Expected
+        }
+        pool.close();
+    }
+
+    @Test
+    public void testToString() {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        try (final KeyedObjectPool<Object, Object, PrivateException> pool = makeEmptyPool(factory)) {
+            pool.toString();
+        } catch (final UnsupportedOperationException uoe) {
+            return; // test not supported
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/pool2/WaiterFactory.java b/src/test/java/org/apache/commons/pool2/WaiterFactory.java
index 3a190391..014bf375 100644
--- a/src/test/java/org/apache/commons/pool2/WaiterFactory.java
+++ b/src/test/java/org/apache/commons/pool2/WaiterFactory.java
@@ -1,217 +1,217 @@
-/*
- * 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.commons.pool2;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-import org.apache.commons.pool2.impl.DefaultPooledObject;
-
-/**
- * Object factory with configurable latencies for object lifecycle methods.
- * This factory will also track and enforce maxActive, maxActivePerKey contracts.
- * If the factory's maxActive / maxActivePerKey are set to match those of the
- * pool, makeObject will throw IllegalStateException if the number of makes - destroys
- * (per key) exceeds the configured max.
- * 
- * @param <K> The type of keys managed by this factory.
- */
-public class WaiterFactory<K> implements PooledObjectFactory<Waiter, IllegalStateException>, KeyedPooledObjectFactory<K, Waiter, RuntimeException> {
-
-    /** Latency of activateObject */
-    private final long activateLatency;
-
-    /** Latency of destroyObject */
-    private final long destroyLatency;
-
-    /** Latency of makeObject */
-    private final long makeLatency;
-
-    /** Latency of passivateObject */
-    private final long passivateLatency;
-
-    /** Latency of validateObject */
-    private final long validateLatency;
-
-    /** Latency of doWait for Waiter instances created by this factory */
-    private final long waiterLatency;
-
-    /** Probability that passivation will invalidate Waiter instances */
-    private final double passivateInvalidationProbability;
-
-    /** Count of (makes - destroys) since last reset */
-    private long activeCount;
-
-    /** Count of (makes - destroys) per key since last reset */
-    private final Map<K,Integer> activeCounts = new HashMap<>();
-
-    /** Maximum of (makes - destroys) - if exceeded IllegalStateException */
-    private final long maxActive;  // GKOP 1.x calls this maxTotal
-
-    /** Maximum of (makes - destroys) per key */
-    private final long maxActivePerKey;  // GKOP 1.x calls this maxActive
-
-    public WaiterFactory(final long activateLatency, final long destroyLatency,
-            final long makeLatency, final long passivateLatency, final long validateLatency,
-            final long waiterLatency) {
-        this(activateLatency, destroyLatency, makeLatency, passivateLatency,
-                validateLatency, waiterLatency, Long.MAX_VALUE, Long.MAX_VALUE, 0);
-    }
-
-    public WaiterFactory(final long activateLatency, final long destroyLatency,
-            final long makeLatency, final long passivateLatency, final long validateLatency,
-            final long waiterLatency,final long maxActive) {
-        this(activateLatency, destroyLatency, makeLatency, passivateLatency,
-                validateLatency, waiterLatency, maxActive, Long.MAX_VALUE, 0);
-    }
-
-    public WaiterFactory(final long activateLatency, final long destroyLatency,
-            final long makeLatency, final long passivateLatency, final long validateLatency,
-            final long waiterLatency,final long maxActive, final long maxActivePerKey,
-            final double passivateInvalidationProbability) {
-        this.activateLatency = activateLatency;
-        this.destroyLatency = destroyLatency;
-        this.makeLatency = makeLatency;
-        this.passivateLatency = passivateLatency;
-        this.validateLatency = validateLatency;
-        this.waiterLatency = waiterLatency;
-        this.maxActive = maxActive;
-        this.maxActivePerKey = maxActivePerKey;
-        this.passivateInvalidationProbability = passivateInvalidationProbability;
-    }
-
-    @Override
-    public void activateObject(final K key, final PooledObject<Waiter> obj) {
-        activateObject(obj);
-    }
-
-    @Override
-    public void activateObject(final PooledObject<Waiter> obj) {
-        doWait(activateLatency);
-        obj.getObject().setActive(true);
-    }
-
-    @Override
-    public void destroyObject(final K key,final PooledObject<Waiter> obj) {
-        destroyObject(obj);
-        synchronized (this) {
-            final Integer count = activeCounts.get(key);
-            activeCounts.put(key, Integer.valueOf(count.intValue() - 1));
-        }
-    }
-
-    @Override
-    public void destroyObject(final PooledObject<Waiter> obj) {
-        doWait(destroyLatency);
-        obj.getObject().setValid(false);
-        obj.getObject().setActive(false);
-        // Decrement *after* destroy
-        synchronized (this) {
-            activeCount--;
-        }
-    }
-
-    protected void doWait(final long latency) {
-        if (latency == 0) {
-            return;
-        }
-        Waiter.sleepQuietly(latency);
-    }
-
-    /**
-     * @return the maxActive
-     */
-    public synchronized long getMaxActive() {
-        return maxActive;
-    }
-
-    @Override
-    public PooledObject<Waiter> makeObject() {
-        // Increment and test *before* make
-        synchronized (this) {
-            if (activeCount >= maxActive) {
-                throw new IllegalStateException("Too many active instances: " +
-                activeCount + " in circulation with maxActive = " + maxActive);
-            }
-            activeCount++;
-        }
-        doWait(makeLatency);
-        return new DefaultPooledObject<>(new Waiter(false, true, waiterLatency));
-    }
-
-    @Override
-    public PooledObject<Waiter> makeObject(final K key) {
-        synchronized (this) {
-            Integer count = activeCounts.get(key);
-            if (count == null) {
-                count = Integer.valueOf(1);
-                activeCounts.put(key, count);
-            } else {
-                if (count.intValue() >= maxActivePerKey) {
-                    throw new IllegalStateException("Too many active " +
-                    "instances for key = " + key + ": " + count.intValue() +
-                    " in circulation " + "with maxActivePerKey = " +
-                    maxActivePerKey);
-                }
-                activeCounts.put(key, Integer.valueOf(count.intValue() + 1));
-            }
-        }
-        return makeObject();
-    }
-
-    // KeyedPoolableObjectFactory methods
-
-    @Override
-    public void passivateObject(final K key, final PooledObject<Waiter> obj) {
-        passivateObject(obj);
-    }
-
-    @Override
-    public void passivateObject(final PooledObject<Waiter> obj) {
-        obj.getObject().setActive(false);
-        doWait(passivateLatency);
-        if (Math.random() < passivateInvalidationProbability) {
-            obj.getObject().setValid(false);
-        }
-    }
-
-    public synchronized void reset() {
-        activeCount = 0;
-        if (activeCounts.isEmpty()) {
-            return;
-        }
-        final Iterator<K> it = activeCounts.keySet().iterator();
-        while (it.hasNext()) {
-            final K key = it.next();
-            activeCounts.put(key, Integer.valueOf(0));
-        }
-    }
-
-    @Override
-    public boolean validateObject(final K key, final PooledObject<Waiter> obj) {
-        return validateObject(obj);
-    }
-
-    @Override
-    public boolean validateObject(final PooledObject<Waiter> obj) {
-        doWait(validateLatency);
-        return obj.getObject().isValid();
-    }
-
-}
+/*
+ * 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.commons.pool2;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+
+/**
+ * Object factory with configurable latencies for object lifecycle methods.
+ * This factory will also track and enforce maxActive, maxActivePerKey contracts.
+ * If the factory's maxActive / maxActivePerKey are set to match those of the
+ * pool, makeObject will throw IllegalStateException if the number of makes - destroys
+ * (per key) exceeds the configured max.
+ *
+ * @param <K> The type of keys managed by this factory.
+ */
+public class WaiterFactory<K> implements PooledObjectFactory<Waiter, IllegalStateException>, KeyedPooledObjectFactory<K, Waiter, RuntimeException> {
+
+    /** Latency of activateObject */
+    private final long activateLatency;
+
+    /** Latency of destroyObject */
+    private final long destroyLatency;
+
+    /** Latency of makeObject */
+    private final long makeLatency;
+
+    /** Latency of passivateObject */
+    private final long passivateLatency;
+
+    /** Latency of validateObject */
+    private final long validateLatency;
+
+    /** Latency of doWait for Waiter instances created by this factory */
+    private final long waiterLatency;
+
+    /** Probability that passivation will invalidate Waiter instances */
+    private final double passivateInvalidationProbability;
+
+    /** Count of (makes - destroys) since last reset */
+    private long activeCount;
+
+    /** Count of (makes - destroys) per key since last reset */
+    private final Map<K,Integer> activeCounts = new HashMap<>();
+
+    /** Maximum of (makes - destroys) - if exceeded IllegalStateException */
+    private final long maxActive;  // GKOP 1.x calls this maxTotal
+
+    /** Maximum of (makes - destroys) per key */
+    private final long maxActivePerKey;  // GKOP 1.x calls this maxActive
+
+    public WaiterFactory(final long activateLatency, final long destroyLatency,
+            final long makeLatency, final long passivateLatency, final long validateLatency,
+            final long waiterLatency) {
+        this(activateLatency, destroyLatency, makeLatency, passivateLatency,
+                validateLatency, waiterLatency, Long.MAX_VALUE, Long.MAX_VALUE, 0);
+    }
+
+    public WaiterFactory(final long activateLatency, final long destroyLatency,
+            final long makeLatency, final long passivateLatency, final long validateLatency,
+            final long waiterLatency,final long maxActive) {
+        this(activateLatency, destroyLatency, makeLatency, passivateLatency,
+                validateLatency, waiterLatency, maxActive, Long.MAX_VALUE, 0);
+    }
+
+    public WaiterFactory(final long activateLatency, final long destroyLatency,
+            final long makeLatency, final long passivateLatency, final long validateLatency,
+            final long waiterLatency,final long maxActive, final long maxActivePerKey,
+            final double passivateInvalidationProbability) {
+        this.activateLatency = activateLatency;
+        this.destroyLatency = destroyLatency;
+        this.makeLatency = makeLatency;
+        this.passivateLatency = passivateLatency;
+        this.validateLatency = validateLatency;
+        this.waiterLatency = waiterLatency;
+        this.maxActive = maxActive;
+        this.maxActivePerKey = maxActivePerKey;
+        this.passivateInvalidationProbability = passivateInvalidationProbability;
+    }
+
+    @Override
+    public void activateObject(final K key, final PooledObject<Waiter> obj) {
+        activateObject(obj);
+    }
+
+    @Override
+    public void activateObject(final PooledObject<Waiter> obj) {
+        doWait(activateLatency);
+        obj.getObject().setActive(true);
+    }
+
+    @Override
+    public void destroyObject(final K key,final PooledObject<Waiter> obj) {
+        destroyObject(obj);
+        synchronized (this) {
+            final Integer count = activeCounts.get(key);
+            activeCounts.put(key, Integer.valueOf(count.intValue() - 1));
+        }
+    }
+
+    @Override
+    public void destroyObject(final PooledObject<Waiter> obj) {
+        doWait(destroyLatency);
+        obj.getObject().setValid(false);
+        obj.getObject().setActive(false);
+        // Decrement *after* destroy
+        synchronized (this) {
+            activeCount--;
+        }
+    }
+
+    protected void doWait(final long latency) {
+        if (latency == 0) {
+            return;
+        }
+        Waiter.sleepQuietly(latency);
+    }
+
+    /**
+     * @return the maxActive
+     */
+    public synchronized long getMaxActive() {
+        return maxActive;
+    }
+
+    @Override
+    public PooledObject<Waiter> makeObject() {
+        // Increment and test *before* make
+        synchronized (this) {
+            if (activeCount >= maxActive) {
+                throw new IllegalStateException("Too many active instances: " +
+                activeCount + " in circulation with maxActive = " + maxActive);
+            }
+            activeCount++;
+        }
+        doWait(makeLatency);
+        return new DefaultPooledObject<>(new Waiter(false, true, waiterLatency));
+    }
+
+    @Override
+    public PooledObject<Waiter> makeObject(final K key) {
+        synchronized (this) {
+            Integer count = activeCounts.get(key);
+            if (count == null) {
+                count = Integer.valueOf(1);
+                activeCounts.put(key, count);
+            } else {
+                if (count.intValue() >= maxActivePerKey) {
+                    throw new IllegalStateException("Too many active " +
+                    "instances for key = " + key + ": " + count.intValue() +
+                    " in circulation " + "with maxActivePerKey = " +
+                    maxActivePerKey);
+                }
+                activeCounts.put(key, Integer.valueOf(count.intValue() + 1));
+            }
+        }
+        return makeObject();
+    }
+
+    // KeyedPoolableObjectFactory methods
+
+    @Override
+    public void passivateObject(final K key, final PooledObject<Waiter> obj) {
+        passivateObject(obj);
+    }
+
+    @Override
+    public void passivateObject(final PooledObject<Waiter> obj) {
+        obj.getObject().setActive(false);
+        doWait(passivateLatency);
+        if (Math.random() < passivateInvalidationProbability) {
+            obj.getObject().setValid(false);
+        }
+    }
+
+    public synchronized void reset() {
+        activeCount = 0;
+        if (activeCounts.isEmpty()) {
+            return;
+        }
+        final Iterator<K> it = activeCounts.keySet().iterator();
+        while (it.hasNext()) {
+            final K key = it.next();
+            activeCounts.put(key, Integer.valueOf(0));
+        }
+    }
+
+    @Override
+    public boolean validateObject(final K key, final PooledObject<Waiter> obj) {
+        return validateObject(obj);
+    }
+
+    @Override
+    public boolean validateObject(final PooledObject<Waiter> obj) {
+        doWait(validateLatency);
+        return obj.getObject().isValid();
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java
index 071cabb1..548626b5 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java
@@ -126,11 +126,11 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             this.pool = pool;
             this.key = key;
         }
-        
+
         public boolean complete() {
             return done;
         }
-        
+
         @Override
         public void run() {
             try {
@@ -226,55 +226,55 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             }
             return new DefaultPooledObject<>(out);
         }
-        
+
         @Override
         public void passivateObject(final K key, final PooledObject<String> obj) throws TestException {
             if (exceptionOnPassivate) {
                 throw new TestException();
             }
         }
-        
+
         public void setDestroyLatency(final long destroyLatency) {
             this.destroyLatency = destroyLatency;
         }
-        
+
         void setEvenValid(final boolean valid) {
             evenValid = valid;
         }
-        
+
         public void setMakeLatency(final long makeLatency) {
             this.makeLatency = makeLatency;
         }
-        
+
         public void setMaxTotalPerKey(final int maxTotalPerKey) {
             this.maxTotalPerKey = maxTotalPerKey;
         }
-        
+
         public void setThrowExceptionOnActivate(final boolean b) {
             exceptionOnActivate = b;
         }
-        
+
         public void setThrowExceptionOnDestroy(final boolean b) {
             exceptionOnDestroy = b;
         }
-        
+
         public void setThrowExceptionOnPassivate(final boolean b) {
             exceptionOnPassivate = b;
         }
-        
+
         public void setThrowExceptionOnValidate(final boolean b) {
             exceptionOnValidate = b;
         }
-        
+
         void setValid(final boolean valid) {
             evenValid = valid;
             oddValid = valid;
         }
-        
+
         public void setValidateLatency(final long validateLatency) {
             this.validateLatency = validateLatency;
         }
-        
+
         public void setValidationEnabled(final boolean b) {
             enableValidation = b;
         }
@@ -291,7 +291,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             return valid;
         }
     }
-    
+
     private static class SimplePerKeyFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
         final ConcurrentHashMap<Object, AtomicInteger> map = new ConcurrentHashMap<>();
 
@@ -496,7 +496,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     private static final Integer KEY_TWO = Integer.valueOf(2);
 
     private static final boolean DISPLAY_THREAD_DETAILS=
-        Boolean.parseBoolean(System.getProperty("TestGenericKeyedObjectPool.display.thread.details", "false"));
+    Boolean.getBoolean("TestGenericKeyedObjectPool.display.thread.details");
     // To pass this to a Maven test, use:
     // mvn test -DargLine="-DTestGenericKeyedObjectPool.display.thread.details=true"
     // @see https://issues.apache.org/jira/browse/SUREFIRE-121
@@ -684,7 +684,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             // FIFO - 24, 25, 26
             for (int i = 0; i < 8; i++) {
                 final VisitTracker<Integer> tracker = intPool.borrowObject(KEY_ONE);
-                if ((lifo && tracker.getId() > 1) || (!lifo && tracker.getId() > 2)) {
+                if (lifo && tracker.getId() > 1 || !lifo && tracker.getId() > 2) {
                     assertEquals( 1,
                             tracker.getValidateCount(),"Instance " + tracker.getId() + " visited wrong number of times.");
                 } else {
@@ -738,7 +738,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
                     final int totalInstances = zeroLength + oneLength + twoLength;
 
                     // Number of times evictor should have cycled through pools
-                    final int cycleCount = (runs * intPool.getNumTestsPerEvictionRun()) / totalInstances;
+                    final int cycleCount = runs * intPool.getNumTestsPerEvictionRun() / totalInstances;
 
                     // Look at elements and make sure they are visited cycleCount
                     // or cycleCount + 1 times
@@ -861,7 +861,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             t.start();
         }
         for (final TestThread<T, E> testThread : threads) {
-            while (!(testThread.complete())) {
+            while (!testThread.complete()) {
                 Waiter.sleepQuietly(500L);
             }
             if (testThread.failed()) {
@@ -909,12 +909,12 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     @Test
     public void testAppendStats() {
         assertFalse(gkoPool.getMessageStatistics());
-        assertEquals("foo", (gkoPool.appendStats("foo")));
+        assertEquals("foo", gkoPool.appendStats("foo"));
         try (final GenericKeyedObjectPool<?, ?, TestException> pool = new GenericKeyedObjectPool<>(new SimpleFactory<>())) {
             pool.setMessagesStatistics(true);
-            assertNotEquals("foo", (pool.appendStats("foo")));
+            assertNotEquals("foo", pool.appendStats("foo"));
             pool.setMessagesStatistics(false);
-            assertEquals("foo", (pool.appendStats("foo")));
+            assertEquals("foo", pool.appendStats("foo"));
         }
     }
 
@@ -929,7 +929,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         final long startMillis = System.currentTimeMillis();
         // Needs to be in a separate thread as this will block
         final Runnable simple = new SimpleTestThread<>(gkoPool, "one");
-        (new Thread(simple)).start();
+        new Thread(simple).start();
         // This should be almost instant. If it isn't it means this thread got
         // stuck behind the thread created above which is bad.
         // Give other thread a chance to start
@@ -940,7 +940,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         // If it passes it should be almost instant
         // Use 3000ms as the threshold - should avoid timing issues on most
         // (all? platforms)
-        assertTrue((endMillis - startMillis) < 4000,
+        assertTrue(endMillis - startMillis < 4000,
                 "Elapsed time: " + (endMillis - startMillis) + " should be less than 4000");
 
     }
@@ -995,7 +995,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
         // Wait for threads to finish
         for (int i = 0; i < numThreads; i++) {
-            while (!(threads[i]).complete()) {
+            while (!threads[i].complete()) {
                 Waiter.sleepQuietly(500L);
             }
             if (threads[i].failed()) {
@@ -1099,7 +1099,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
           final String six2 = gkoPool.borrowObject("six");
           Thread.sleep(100);
           // Launch the waiters - all will be blocked waiting
-          for (Thread t : testThreads) {
+          for (final Thread t : testThreads) {
               t.start();
           }
           Thread.sleep(100);
@@ -1120,7 +1120,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
           gkoPool.returnObject("six", six2);
           gkoPool.clear("six");
           Thread.sleep(20);
-          for (Thread t : testThreads) {
+          for (final Thread t : testThreads) {
               assertFalse(t.isAlive());
           }
       }
@@ -1139,52 +1139,52 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         config.setMaxIdlePerKey(-1);
         config.setMaxTotal(-1);
         config.setMaxWait(Duration.ofMillis(5));
-        GenericKeyedObjectPool<Integer, Integer, InterruptedException> testPool = new GenericKeyedObjectPool<>(
+        final GenericKeyedObjectPool<Integer, Integer, InterruptedException> testPool = new GenericKeyedObjectPool<>(
                 new KeyedPooledObjectFactory<Integer, Integer, InterruptedException>() {
                     @Override
-                    public void activateObject(Integer key, PooledObject<Integer> p) {
+                    public void activateObject(final Integer key, final PooledObject<Integer> p) {
                         // do nothing
                     }
 
                     @Override
-                    public void destroyObject(Integer key, PooledObject<Integer> p) throws InterruptedException {
+                    public void destroyObject(final Integer key, final PooledObject<Integer> p) throws InterruptedException {
                         Thread.sleep(500);
                     }
 
                     @Override
-                    public PooledObject<Integer> makeObject(Integer key) {
+                    public PooledObject<Integer> makeObject(final Integer key) {
                         return new DefaultPooledObject<>(10);
                     }
 
                     @Override
-                    public void passivateObject(Integer key, PooledObject<Integer> p) {
+                    public void passivateObject(final Integer key, final PooledObject<Integer> p) {
                         // do nothing
                     }
 
                     @Override
-                    public boolean validateObject(Integer key, PooledObject<Integer> p) {
+                    public boolean validateObject(final Integer key, final PooledObject<Integer> p) {
                         return true;
                     }
                 }, config);
         final int borrowKey = 10;
-        Thread t = new Thread(() -> {
+        final Thread t = new Thread(() -> {
             try {
                 while (true) {
-                    Integer integer = testPool.borrowObject(borrowKey);
+                    final Integer integer = testPool.borrowObject(borrowKey);
                     testPool.returnObject(borrowKey, integer);
                     Thread.sleep(10);
                 }
-            } catch (Exception e) {
+            } catch (final Exception e) {
                 fail();
             }
         });
-        Thread t2 = new Thread(() -> {
+        final Thread t2 = new Thread(() -> {
             try {
                 while (true) {
                     testPool.clear(borrowKey);
                     Thread.sleep(10);
                 }
-            } catch (Exception e) {
+            } catch (final Exception e) {
                 fail();
             }
         });
@@ -1683,7 +1683,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
     @Test
     public void testGetStatsString() {
-        assertNotNull((gkoPool.getStatsString()));
+        assertNotNull(gkoPool.getStatsString());
     }
 
     /**
@@ -1722,8 +1722,8 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     public void testInvalidateFreesCapacityForOtherKeys() throws Exception {
         gkoPool.setMaxTotal(1);
         gkoPool.setMaxWait(Duration.ofMillis(500));
-        Thread borrower = new Thread(new SimpleTestThread<>(gkoPool, "two"));
-        String obj = gkoPool.borrowObject("one");
+        final Thread borrower = new Thread(new SimpleTestThread<>(gkoPool, "two"));
+        final String obj = gkoPool.borrowObject("one");
         borrower.start();  // Will block
         Thread.sleep(100);  // Make sure borrower has started
         gkoPool.invalidateObject("one", obj);  // Should free capacity to serve the other key
@@ -1915,7 +1915,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         for(int i=0;i<100;i++) {
             gkoPool.returnObject("",active[i]);
             assertEquals(99 - i,gkoPool.getNumActive(""));
-            assertEquals((i < 8 ? i+1 : 8),gkoPool.getNumIdle(""));
+            assertEquals(i < 8 ? i+1 : 8,gkoPool.getNumIdle(""));
         }
 
         for(int i=0;i<100;i++) {
@@ -1926,7 +1926,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         for(int i=0;i<100;i++) {
             gkoPool.returnObject("a",active[i]);
             assertEquals(99 - i,gkoPool.getNumActive("a"));
-            assertEquals((i < 8 ? i+1 : 8),gkoPool.getNumIdle("a"));
+            assertEquals(i < 8 ? i+1 : 8,gkoPool.getNumIdle("a"));
         }
 
         // total number of idle instances is twice maxIdle
@@ -2169,7 +2169,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
                         " BorrowTime: " + (wt.postBorrowMillis != 0 ? wt.postBorrowMillis - wt.preBorrowMillis : -1) +
                         " PostReturn: " + (wt.postReturnMillis != 0 ? wt.postReturnMillis - originMillis : -1) +
                         " Ended: " + (wt.endedMillis - originMillis) +
-                        " Key: " + (wt.key) +
+                        " Key: " + wt.key +
                         " ObjId: " + wt.objectId
                         );
             }
@@ -2418,7 +2418,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         gkoPool.setMaxTotal(1);
 
         // Test return object with no take waiters
-        String obj = gkoPool.borrowObject("0");
+        final String obj = gkoPool.borrowObject("0");
         gkoPool.returnObject("0", obj);
 
         // Test return object with a take waiter
@@ -2437,7 +2437,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         gkoPool.setBlockWhenExhausted(false);
 
         // Test return object with no take waiters
-        String obj = gkoPool.borrowObject("0");
+        final String obj = gkoPool.borrowObject("0");
         gkoPool.returnObject("0", obj);
     }
 
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java
index f7cae2bf..7ae0a386 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java
@@ -1,3011 +1,3011 @@
-/*
- * 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.commons.pool2.impl;
-
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.lessThanOrEqualTo;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-import java.lang.management.ManagementFactory;
-import java.lang.ref.WeakReference;
-import java.nio.charset.UnsupportedCharsetException;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-import java.util.Random;
-import java.util.Set;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.management.MBeanServer;
-import javax.management.ObjectName;
-
-import org.apache.commons.pool2.BasePooledObjectFactory;
-import org.apache.commons.pool2.ObjectPool;
-import org.apache.commons.pool2.PoolUtils;
-import org.apache.commons.pool2.PooledObject;
-import org.apache.commons.pool2.PooledObjectFactory;
-import org.apache.commons.pool2.SwallowedExceptionListener;
-import org.apache.commons.pool2.TestBaseObjectPool;
-import org.apache.commons.pool2.TestException;
-import org.apache.commons.pool2.VisitTracker;
-import org.apache.commons.pool2.VisitTrackerFactory;
-import org.apache.commons.pool2.Waiter;
-import org.apache.commons.pool2.WaiterFactory;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-
-/**
- */
-public class TestGenericObjectPool extends TestBaseObjectPool {
-
-    private class ConcurrentBorrowAndEvictThread extends Thread {
-        private final boolean borrow;
-        public String obj;
-
-        public ConcurrentBorrowAndEvictThread(final boolean borrow) {
-            this.borrow = borrow;
-        }
-
-        @Override
-        public void run() {
-            try {
-                if (borrow) {
-                    obj = genericObjectPool.borrowObject();
-                } else {
-                    genericObjectPool.evict();
-                }
-            } catch (final Exception e) {
-                // Ignore.
-            }
-        }
-    }
-
-    private static class CreateErrorFactory extends BasePooledObjectFactory<String, InterruptedException> {
-
-        private final Semaphore semaphore = new Semaphore(0);
-
-        @Override
-        public String create() throws InterruptedException {
-            semaphore.acquire();
-            throw new UnknownError("wiggle");
-        }
-
-        public boolean hasQueuedThreads() {
-            return semaphore.hasQueuedThreads();
-        }
-
-        public void release() {
-            semaphore.release();
-        }
-
-        @Override
-        public PooledObject<String> wrap(final String obj) {
-            return new DefaultPooledObject<>(obj);
-        }
-    }
-
-    private static class CreateFailFactory extends BasePooledObjectFactory<String, InterruptedException> {
-
-        private final Semaphore semaphore = new Semaphore(0);
-
-        @Override
-        public String create() throws InterruptedException {
-            semaphore.acquire();
-            throw new UnsupportedCharsetException("wibble");
-        }
-
-        public boolean hasQueuedThreads() {
-            return semaphore.hasQueuedThreads();
-        }
-
-        public void release() {
-            semaphore.release();
-        }
-
-        @Override
-        public PooledObject<String> wrap(final String obj) {
-            return new DefaultPooledObject<>(obj);
-        }
-    }
-
-    private static final class DummyFactory
-            extends BasePooledObjectFactory<Object, RuntimeException> {
-        @Override
-        public Object create() {
-            return null;
-        }
-        @Override
-        public PooledObject<Object> wrap(final Object value) {
-            return new DefaultPooledObject<>(value);
-        }
-    }
-
-    private static class EvictionThread<T, E extends Exception> extends Thread {
-
-        private final GenericObjectPool<T, E> pool;
-
-        public EvictionThread(final GenericObjectPool<T, E> pool) {
-            this.pool = pool;
-        }
-
-        @Override
-        public void run() {
-            try {
-                pool.evict();
-            } catch (final Exception e) {
-                // Ignore
-            }
-        }
-    }
-
-    /**
-     * Factory that creates HashSets.  Note that this means
-     *  0) All instances are initially equal (not discernible by equals)
-     *  1) Instances are mutable and mutation can cause change in identity / hashcode.
-     */
-    private static final class HashSetFactory
-            extends BasePooledObjectFactory<HashSet<String>, RuntimeException> {
-        @Override
-        public HashSet<String> create() {
-            return new HashSet<>();
-        }
-        @Override
-        public PooledObject<HashSet<String>> wrap(final HashSet<String> value) {
-            return new DefaultPooledObject<>(value);
-        }
-    }
-
-    /**
-     * Attempts to invalidate an object, swallowing IllegalStateException.
-     */
-    static class InvalidateThread implements Runnable {
-        private final String obj;
-        private final ObjectPool<String, ? extends Exception> pool;
-        private boolean done;
-        public InvalidateThread(final ObjectPool<String, ? extends Exception> pool, final String obj) {
-            this.obj = obj;
-            this.pool = pool;
-        }
-        public boolean complete() {
-            return done;
-        }
-        @Override
-        public void run() {
-            try {
-                pool.invalidateObject(obj);
-            } catch (final IllegalStateException ex) {
-                // Ignore
-            } catch (final Exception ex) {
-                fail("Unexpected exception " + ex.toString());
-            } finally {
-                done = true;
-            }
-        }
-    }
-
-    private static class InvalidFactory
-            extends BasePooledObjectFactory<Object, RuntimeException> {
-
-        @Override
-        public Object create() {
-            return new Object();
-        }
-        @Override
-        public boolean validateObject(final PooledObject<Object> obj) {
-            Waiter.sleepQuietly(1000);
-            return false;
-        }
-
-        @Override
-        public PooledObject<Object> wrap(final Object value) {
-            return new DefaultPooledObject<>(value);
-        }
-    }
-
-    public static class SimpleFactory implements PooledObjectFactory<String, TestException> {
-        int makeCounter;
-
-        int activationCounter;
-
-        int validateCounter;
-
-        int activeCount;
-
-        boolean evenValid = true;
-
-        boolean oddValid = true;
-
-        boolean exceptionOnPassivate;
-
-        boolean exceptionOnActivate;
-
-        boolean exceptionOnDestroy;
-
-        boolean exceptionOnValidate;
-
-        boolean enableValidation = true;
-
-        long destroyLatency;
-
-        long makeLatency;
-
-        long validateLatency;
-
-        int maxTotal = Integer.MAX_VALUE;
-
-        public SimpleFactory() {
-            this(true);
-        }
-
-        public SimpleFactory(final boolean valid) {
-            this(valid,valid);
-        }
-        
-        public SimpleFactory(final boolean evalid, final boolean ovalid) {
-            evenValid = evalid;
-            oddValid = ovalid;
-        }
-        
-        @Override
-        public void activateObject(final PooledObject<String> obj) throws TestException {
-            final boolean hurl;
-            final boolean evenTest;
-            final boolean oddTest;
-            final int counter;
-            synchronized(this) {
-                hurl = exceptionOnActivate;
-                evenTest = evenValid;
-                oddTest = oddValid;
-                counter = activationCounter++;
-            }
-            if (hurl && !(counter%2 == 0 ? evenTest : oddTest)) {
-                throw new TestException();
-            }
-        }
-        
-        @Override
-        public void destroyObject(final PooledObject<String> obj) throws TestException {
-            final long waitLatency;
-            final boolean hurl;
-            synchronized(this) {
-                waitLatency = destroyLatency;
-                hurl = exceptionOnDestroy;
-            }
-            if (waitLatency > 0) {
-                doWait(waitLatency);
-            }
-            synchronized(this) {
-                activeCount--;
-            }
-            if (hurl) {
-                throw new TestException();
-            }
-        }
-        
-        private void doWait(final long latency) {
-            Waiter.sleepQuietly(latency);
-        }
-        
-        public synchronized int getMakeCounter() {
-            return makeCounter;
-        }
-        
-        public synchronized boolean isThrowExceptionOnActivate() {
-            return exceptionOnActivate;
-        }
-        
-        public synchronized boolean isValidationEnabled() {
-            return enableValidation;
-        }
-        
-        @Override
-        public PooledObject<String> makeObject() {
-            final long waitLatency;
-            synchronized(this) {
-                activeCount++;
-                if (activeCount > maxTotal) {
-                    throw new IllegalStateException(
-                        "Too many active instances: " + activeCount);
-                }
-                waitLatency = makeLatency;
-            }
-            if (waitLatency > 0) {
-                doWait(waitLatency);
-            }
-            final int counter;
-            synchronized(this) {
-                counter = makeCounter++;
-            }
-            return new DefaultPooledObject<>(String.valueOf(counter));
-        }
-        
-        @Override
-        public void passivateObject(final PooledObject<String> obj) throws TestException {
-            final boolean hurl;
-            synchronized(this) {
-                hurl = exceptionOnPassivate;
-            }
-            if (hurl) {
-                throw new TestException();
-            }
-        }
-        
-        public synchronized void setDestroyLatency(final long destroyLatency) {
-            this.destroyLatency = destroyLatency;
-        }
-        
-        public synchronized void setEvenValid(final boolean valid) {
-            evenValid = valid;
-        }
-        
-        public synchronized void setMakeLatency(final long makeLatency) {
-            this.makeLatency = makeLatency;
-        }
-        
-        public synchronized void setMaxTotal(final int maxTotal) {
-            this.maxTotal = maxTotal;
-        }
-        
-        public synchronized void setOddValid(final boolean valid) {
-            oddValid = valid;
-        }
-
-        public synchronized void setThrowExceptionOnActivate(final boolean b) {
-            exceptionOnActivate = b;
-        }
-
-        public synchronized void setThrowExceptionOnDestroy(final boolean b) {
-            exceptionOnDestroy = b;
-        }
-
-        public synchronized void setThrowExceptionOnPassivate(final boolean bool) {
-            exceptionOnPassivate = bool;
-        }
-
-        public synchronized void setThrowExceptionOnValidate(final boolean bool) {
-            exceptionOnValidate = bool;
-        }
-
-        public synchronized void setValid(final boolean valid) {
-            setEvenValid(valid);
-            setOddValid(valid);
-        }
-
-        public synchronized void setValidateLatency(final long validateLatency) {
-            this.validateLatency = validateLatency;
-        }
-
-        public synchronized void setValidationEnabled(final boolean b) {
-            enableValidation = b;
-        }
-
-        @Override
-        public boolean validateObject(final PooledObject<String> obj) {
-            final boolean validate;
-            final boolean throwException;
-            final boolean evenTest;
-            final boolean oddTest;
-            final long waitLatency;
-            final int counter;
-            synchronized(this) {
-                validate = enableValidation;
-                throwException = exceptionOnValidate;
-                evenTest = evenValid;
-                oddTest = oddValid;
-                counter = validateCounter++;
-                waitLatency = validateLatency;
-            }
-            if (waitLatency > 0) {
-                doWait(waitLatency);
-            }
-            if (throwException) {
-                throw new RuntimeException("validation failed");
-            }
-            if (validate) {
-                return counter%2 == 0 ? evenTest : oddTest;
-            }
-            return true;
-        }
-    }
-
-    public static class TestEvictionPolicy<T> implements EvictionPolicy<T> {
-
-        private final AtomicInteger callCount = new AtomicInteger(0);
-
-        @Override
-        public boolean evict(final EvictionConfig config, final PooledObject<T> underTest,
-                final int idleCount) {
-            return callCount.incrementAndGet() > 1500;
-        }
-    }
-
-    static class TestThread<T, E extends Exception> implements Runnable {
-
-        /** source of random delay times */
-        private final java.util.Random random;
-
-        /** pool to borrow from */
-        private final ObjectPool<T, E> pool;
-
-        /** number of borrow attempts */
-        private final int iter;
-
-        /** delay before each borrow attempt */
-        private final int startDelay;
-
-        /** time to hold each borrowed object before returning it */
-        private final int holdTime;
-
-        /** whether or not start and hold time are randomly generated */
-        private final boolean randomDelay;
-
-        /** object expected to be borrowed (fail otherwise) */
-        private final Object expectedObject;
-
-        private volatile boolean complete;
-        private volatile boolean failed;
-        private volatile Throwable error;
-
-        public TestThread(final ObjectPool<T, E> pool) {
-            this(pool, 100, 50, true, null);
-        }
-
-        public TestThread(final ObjectPool<T, E> pool, final int iter) {
-            this(pool, iter, 50, true, null);
-        }
-
-        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay) {
-            this(pool, iter, delay, true, null);
-        }
-
-        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay,
-                final boolean randomDelay) {
-            this(pool, iter, delay, randomDelay, null);
-        }
-
-        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay,
-                final boolean randomDelay, final Object obj) {
-            this(pool, iter, delay, delay, randomDelay, obj);
-        }
-
-        public TestThread(final ObjectPool<T, E> pool, final int iter, final int startDelay,
-            final int holdTime, final boolean randomDelay, final Object obj) {
-       this.pool = pool;
-       this.iter = iter;
-       this.startDelay = startDelay;
-       this.holdTime = holdTime;
-       this.randomDelay = randomDelay;
-       this.random = this.randomDelay ? new Random() : null;
-       this.expectedObject = obj;
-    }
-
-        public boolean complete() {
-            return complete;
-        }
-
-        public boolean failed() {
-            return failed;
-        }
-
-        @Override
-        public void run() {
-            for (int i = 0; i < iter; i++) {
-                final long actualStartDelay = randomDelay ? (long) random.nextInt(startDelay) : startDelay;
-                final long actualHoldTime = randomDelay ? (long) random.nextInt(holdTime) : holdTime;
-                Waiter.sleepQuietly(actualStartDelay);
-                T obj = null;
-                try {
-                    obj = pool.borrowObject();
-                } catch (final Exception e) {
-                    error = e;
-                    failed = true;
-                    complete = true;
-                    break;
-                }
-
-                if (expectedObject != null && !expectedObject.equals(obj)) {
-                    error = new Throwable("Expected: " + expectedObject + " found: " + obj);
-                    failed = true;
-                    complete = true;
-                    break;
-                }
-
-                Waiter.sleepQuietly(actualHoldTime);
-                try {
-                    pool.returnObject(obj);
-                } catch (final Exception e) {
-                    error = e;
-                    failed = true;
-                    complete = true;
-                    break;
-                }
-            }
-            complete = true;
-        }
-    }
-
-    /*
-     * Very simple test thread that just tries to borrow an object from
-     * the provided pool returns it after a wait
-     */
-    static class WaitingTestThread<E extends Exception> extends Thread {
-        private final GenericObjectPool<String, E> pool;
-        private final long pause;
-        private Throwable thrown;
-
-        private long preBorrowMillis; // just before borrow
-        private long postBorrowMillis; //  borrow returned
-        private long postReturnMillis; // after object was returned
-        private long endedMillis;
-        private String objectId;
-
-        public WaitingTestThread(final GenericObjectPool<String, E> pool, final long pause) {
-            this.pool = pool;
-            this.pause = pause;
-            this.thrown = null;
-        }
-
-        @Override
-        public void run() {
-            try {
-                preBorrowMillis = System.currentTimeMillis();
-                final String obj = pool.borrowObject();
-                objectId = obj;
-                postBorrowMillis = System.currentTimeMillis();
-                Thread.sleep(pause);
-                pool.returnObject(obj);
-                postReturnMillis = System.currentTimeMillis();
-            } catch (final Throwable e) {
-                thrown = e;
-            } finally{
-                endedMillis = System.currentTimeMillis();
-            }
-        }
-    }
-
-    private static final boolean DISPLAY_THREAD_DETAILS=
-        Boolean.parseBoolean(System.getProperty("TestGenericObjectPool.display.thread.details", "false"));
-    // To pass this to a Maven test, use:
-    // mvn test -DargLine="-DTestGenericObjectPool.display.thread.details=true"
-    // @see https://issues.apache.org/jira/browse/SUREFIRE-121
-
-    protected GenericObjectPool<String, TestException> genericObjectPool;
-
-    private SimpleFactory simpleFactory;
-
-    @SuppressWarnings("deprecation")
-    private void assertConfiguration(final GenericObjectPoolConfig<?> expected, final GenericObjectPool<?, ?> actual) {
-        assertEquals(Boolean.valueOf(expected.getTestOnCreate()), Boolean.valueOf(actual.getTestOnCreate()),
-                "testOnCreate");
-        assertEquals(Boolean.valueOf(expected.getTestOnBorrow()), Boolean.valueOf(actual.getTestOnBorrow()),
-                "testOnBorrow");
-        assertEquals(Boolean.valueOf(expected.getTestOnReturn()), Boolean.valueOf(actual.getTestOnReturn()),
-                "testOnReturn");
-        assertEquals(Boolean.valueOf(expected.getTestWhileIdle()), Boolean.valueOf(actual.getTestWhileIdle()),
-                "testWhileIdle");
-        assertEquals(Boolean.valueOf(expected.getBlockWhenExhausted()), Boolean.valueOf(actual.getBlockWhenExhausted()),
-                "whenExhaustedAction");
-        assertEquals(expected.getMaxTotal(), actual.getMaxTotal(), "maxTotal");
-        assertEquals(expected.getMaxIdle(), actual.getMaxIdle(), "maxIdle");
-        assertEquals(expected.getMaxWaitMillis(), actual.getMaxWaitMillis(), "maxWaitDuration");
-        assertEquals(expected.getMaxWaitDuration(), actual.getMaxWaitDuration(), "maxWaitDuration");
-        assertEquals(expected.getMinEvictableIdleTimeMillis(), actual.getMinEvictableIdleTimeMillis(),
-                "minEvictableIdleTimeMillis");
-        assertEquals(expected.getMinEvictableIdleTime(), actual.getMinEvictableIdleTime(),
-                "minEvictableIdleTime");
-        assertEquals(expected.getMinEvictableIdleDuration(), actual.getMinEvictableIdleDuration(),
-                "minEvictableIdleDuration");
-        assertEquals(expected.getNumTestsPerEvictionRun(), actual.getNumTestsPerEvictionRun(),
-                "numTestsPerEvictionRun");
-        assertEquals(expected.getEvictorShutdownTimeoutDuration(), actual.getEvictorShutdownTimeoutDuration(),
-                "evictorShutdownTimeoutDuration");
-        assertEquals(expected.getEvictorShutdownTimeoutMillis(), actual.getEvictorShutdownTimeoutMillis(),
-                "evictorShutdownTimeoutMillis");
-        assertEquals(expected.getEvictorShutdownTimeout(), actual.getEvictorShutdownTimeout(),
-                "evictorShutdownTimeout");
-        assertEquals(expected.getTimeBetweenEvictionRunsMillis(), actual.getTimeBetweenEvictionRunsMillis(),
-                "timeBetweenEvictionRunsMillis");
-        assertEquals(expected.getDurationBetweenEvictionRuns(), actual.getTimeBetweenEvictionRuns(),
-                "timeBetweenEvictionRuns");
-        assertEquals(expected.getTimeBetweenEvictionRuns(), actual.getTimeBetweenEvictionRuns(),
-                "timeBetweenEvictionRuns");
-    }
-
-    private void checkEvict(final boolean lifo) throws Exception {
-        // yea this is hairy but it tests all the code paths in GOP.evict()
-        genericObjectPool.setSoftMinEvictableIdle(Duration.ofMillis(10));
-        genericObjectPool.setSoftMinEvictableIdleTime(Duration.ofMillis(10));
-        genericObjectPool.setMinIdle(2);
-        genericObjectPool.setTestWhileIdle(true);
-        genericObjectPool.setLifo(lifo);
-        genericObjectPool.addObjects(5);
-        genericObjectPool.evict();
-        simpleFactory.setEvenValid(false);
-        simpleFactory.setOddValid(false);
-        simpleFactory.setThrowExceptionOnActivate(true);
-        genericObjectPool.evict();
-        genericObjectPool.addObjects(5);
-        simpleFactory.setThrowExceptionOnActivate(false);
-        simpleFactory.setThrowExceptionOnPassivate(true);
-        genericObjectPool.evict();
-        simpleFactory.setThrowExceptionOnPassivate(false);
-        simpleFactory.setEvenValid(true);
-        simpleFactory.setOddValid(true);
-        Thread.sleep(125);
-        genericObjectPool.evict();
-        assertEquals(2, genericObjectPool.getNumIdle());
-    }
-
-    private void checkEvictionOrder(final boolean lifo) throws Exception {
-        checkEvictionOrderPart1(lifo);
-        tearDown();
-        setUp();
-        checkEvictionOrderPart2(lifo);
-    }
-
-    private void checkEvictionOrderPart1(final boolean lifo) throws Exception {
-        genericObjectPool.setNumTestsPerEvictionRun(2);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(100));
-        genericObjectPool.setLifo(lifo);
-        for (int i = 0; i < 5; i++) {
-            genericObjectPool.addObject();
-            Thread.sleep(100);
-        }
-        // Order, oldest to youngest, is "0", "1", ...,"4"
-        genericObjectPool.evict(); // Should evict "0" and "1"
-        final Object obj = genericObjectPool.borrowObject();
-        assertNotEquals("0", obj, "oldest not evicted");
-        assertNotEquals("1", obj, "second oldest not evicted");
-        // 2 should be next out for FIFO, 4 for LIFO
-        assertEquals(lifo ? "4" : "2" , obj,"Wrong instance returned");
-    }
-
-    private void checkEvictionOrderPart2(final boolean lifo) throws Exception {
-        // Two eviction runs in sequence
-        genericObjectPool.setNumTestsPerEvictionRun(2);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(100));
-        genericObjectPool.setLifo(lifo);
-        for (int i = 0; i < 5; i++) {
-            genericObjectPool.addObject();
-            Thread.sleep(100);
-        }
-        genericObjectPool.evict(); // Should evict "0" and "1"
-        genericObjectPool.evict(); // Should evict "2" and "3"
-        final Object obj = genericObjectPool.borrowObject();
-        assertEquals("4", obj,"Wrong instance remaining in pool");
-    }
-
-    private void checkEvictorVisiting(final boolean lifo) throws Exception {
-        VisitTracker<Object> obj;
-        VisitTrackerFactory<Object> trackerFactory = new VisitTrackerFactory<>();
-        try (GenericObjectPool<VisitTracker<Object>,RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
-            trackerPool.setNumTestsPerEvictionRun(2);
-            trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
-            trackerPool.setTestWhileIdle(true);
-            trackerPool.setLifo(lifo);
-            trackerPool.setTestOnReturn(false);
-            trackerPool.setTestOnBorrow(false);
-            for (int i = 0; i < 8; i++) {
-                trackerPool.addObject();
-            }
-            trackerPool.evict(); // Visit oldest 2 - 0 and 1
-            obj = trackerPool.borrowObject();
-            trackerPool.returnObject(obj);
-            obj = trackerPool.borrowObject();
-            trackerPool.returnObject(obj);
-            // borrow, return, borrow, return
-            // FIFO will move 0 and 1 to end
-            // LIFO, 7 out, then in, then out, then in
-            trackerPool.evict(); // Should visit 2 and 3 in either case
-            for (int i = 0; i < 8; i++) {
-                final VisitTracker<Object> tracker = trackerPool.borrowObject();
-                if (tracker.getId() >= 4) {
-                    assertEquals( 0, tracker.getValidateCount(),"Unexpected instance visited " + tracker.getId());
-                } else {
-                    assertEquals( 1, tracker.getValidateCount(),
-                            "Instance " + tracker.getId() + " visited wrong number of times.");
-                }
-            }
-        }
-
-        trackerFactory = new VisitTrackerFactory<>();
-        try (GenericObjectPool<VisitTracker<Object>, RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
-            trackerPool.setNumTestsPerEvictionRun(3);
-            trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
-            trackerPool.setTestWhileIdle(true);
-            trackerPool.setLifo(lifo);
-            trackerPool.setTestOnReturn(false);
-            trackerPool.setTestOnBorrow(false);
-            for (int i = 0; i < 8; i++) {
-                trackerPool.addObject();
-            }
-            trackerPool.evict(); // 0, 1, 2
-            trackerPool.evict(); // 3, 4, 5
-            obj = trackerPool.borrowObject();
-            trackerPool.returnObject(obj);
-            obj = trackerPool.borrowObject();
-            trackerPool.returnObject(obj);
-            obj = trackerPool.borrowObject();
-            trackerPool.returnObject(obj);
-            // borrow, return, borrow, return
-            // FIFO 3,4,5,6,7,0,1,2
-            // LIFO 7,6,5,4,3,2,1,0
-            // In either case, pointer should be at 6
-            trackerPool.evict();
-            // Should hit 6,7,0 - 0 for second time
-            for (int i = 0; i < 8; i++) {
-                final VisitTracker<Object> tracker = trackerPool.borrowObject();
-                if (tracker.getId() != 0) {
-                    assertEquals( 1, tracker.getValidateCount(),
-                            "Instance " + tracker.getId() + " visited wrong number of times.");
-                } else {
-                    assertEquals( 2, tracker.getValidateCount(),
-                            "Instance " + tracker.getId() + " visited wrong number of times.");
-                }
-            }
-        }
-
-        // Randomly generate a pools with random numTests
-        // and make sure evictor cycles through elements appropriately
-        final int[] smallPrimes = { 2, 3, 5, 7 };
-        final Random random = new Random();
-        random.setSeed(System.currentTimeMillis());
-        for (int i = 0; i < 4; i++) {
-            for (int j = 0; j < 5; j++) {
-                try (GenericObjectPool<VisitTracker<Object>, RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
-                    trackerPool.setNumTestsPerEvictionRun(smallPrimes[i]);
-                    trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
-                    trackerPool.setTestWhileIdle(true);
-                    trackerPool.setLifo(lifo);
-                    trackerPool.setTestOnReturn(false);
-                    trackerPool.setTestOnBorrow(false);
-                    trackerPool.setMaxIdle(-1);
-                    final int instanceCount = 10 + random.nextInt(20);
-                    trackerPool.setMaxTotal(instanceCount);
-                    for (int k = 0; k < instanceCount; k++) {
-                        trackerPool.addObject();
-                    }
-
-                    // Execute a random number of evictor runs
-                    final int runs = 10 + random.nextInt(50);
-                    for (int k = 0; k < runs; k++) {
-                        trackerPool.evict();
-                    }
-
-                    // Number of times evictor should have cycled through the pool
-                    final int cycleCount = (runs * trackerPool.getNumTestsPerEvictionRun()) / instanceCount;
-
-                    // Look at elements and make sure they are visited cycleCount
-                    // or cycleCount + 1 times
-                    VisitTracker<Object> tracker = null;
-                    int visitCount = 0;
-                    for (int k = 0; k < instanceCount; k++) {
-                        tracker = trackerPool.borrowObject();
-                        assertTrue(trackerPool.getNumActive() <= trackerPool.getMaxTotal());
-                        visitCount = tracker.getValidateCount();
-                        assertTrue(visitCount >= cycleCount && visitCount <= cycleCount + 1);
-                    }
-                }
-            }
-        }
-    }
-
-    private BasePooledObjectFactory<String, RuntimeException> createDefaultPooledObjectFactory() {
-        return new BasePooledObjectFactory<String, RuntimeException>() {
-            @Override
-            public String create() {
-                // fake
-                return null;
-            }
-
-            @Override
-            public PooledObject<String> wrap(final String obj) {
-                // fake
-                return new DefaultPooledObject<>(obj);
-            }
-        };
-    }
-
-    private BasePooledObjectFactory<String, RuntimeException> createNullPooledObjectFactory() {
-        return new BasePooledObjectFactory<String, RuntimeException>() {
-            @Override
-            public String create() {
-                // fake
-                return null;
-            }
-
-            @Override
-            public PooledObject<String> wrap(final String obj) {
-                // fake
-                return null;
-            }
-        };
-    }
-
-    private BasePooledObjectFactory<String, InterruptedException> createSlowObjectFactory(final long elapsedTimeMillis) {
-        return new BasePooledObjectFactory<String, InterruptedException>() {
-            @Override
-            public String create() throws InterruptedException {
-                Thread.sleep(elapsedTimeMillis);
-                return "created";
-            }
-
-            @Override
-            public PooledObject<String> wrap(final String obj) {
-                // fake
-                return new DefaultPooledObject<>(obj);
-            }
-        };
-    }
-
-    @Override
-    protected Object getNthObject(final int n) {
-        return String.valueOf(n);
-    }
-
-    @Override
-    protected boolean isFifo() {
-        return false;
-    }
-
-    @Override
-    protected boolean isLifo() {
-        return true;
-    }
-
-    @Override
-    protected ObjectPool<String, TestException> makeEmptyPool(final int minCap) {
-        final GenericObjectPool<String, TestException> mtPool = new GenericObjectPool<>(new SimpleFactory());
-        mtPool.setMaxTotal(minCap);
-       mtPool.setMaxIdle(minCap);
-       return mtPool;
-    }
-
-    @Override
-    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> fac) {
-        return new GenericObjectPool<>(fac);
-    }
-
-    /**
-     * Kicks off <numThreads> test threads, each of which will go through
-     * <iterations> borrow-return cycles with random delay times <= delay
-     * in between.
-     */
-    private <T, E extends Exception> void runTestThreads(final int numThreads, final int iterations, final int delay, final GenericObjectPool<T, E> testPool) {
-        final TestThread<T, E>[] threads = new TestThread[numThreads];
-        for (int i = 0; i < numThreads; i++) {
-            threads[i] = new TestThread<>(testPool, iterations, delay);
-            final Thread t = new Thread(threads[i]);
-            t.start();
-        }
-        for (int i = 0; i < numThreads; i++) {
-            while (!(threads[i]).complete()) {
-                Waiter.sleepQuietly(500L);
-            }
-            if (threads[i].failed()) {
-                fail("Thread " + i + " failed: " + threads[i].error.toString());
-            }
-        }
-    }
-
-    @BeforeEach
-    public void setUp() {
-        simpleFactory = new SimpleFactory();
-        genericObjectPool = new GenericObjectPool<>(simpleFactory);
-    }
-
-    @AfterEach
-    public void tearDown() throws Exception {
-        final ObjectName jmxName = genericObjectPool.getJmxName();
-        final String poolName = Objects.toString(jmxName, null);
-
-        genericObjectPool.clear();
-        genericObjectPool.close();
-        genericObjectPool = null;
-        simpleFactory = null;
-
-        final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
-        final Set<ObjectName> result = mbs.queryNames(new ObjectName("org.apache.commoms.pool2:type=GenericObjectPool,*"), null);
-        // There should be no registered pools at this point
-        final int registeredPoolCount = result.size();
-        final StringBuilder msg = new StringBuilder("Current pool is: ");
-        msg.append(poolName);
-        msg.append("  Still open pools are: ");
-        for (final ObjectName name : result) {
-            // Clean these up ready for the next test
-            msg.append(name.toString());
-            msg.append(" created via\n");
-            msg.append(mbs.getAttribute(name, "CreationStackTrace"));
-            msg.append('\n');
-            mbs.unregisterMBean(name);
-        }
-        assertEquals(0, registeredPoolCount, msg.toString());
-
-        // Make sure that EvictionTimer executor is shut down.
-        Thread.yield();
-        if (EvictionTimer.getExecutor() != null) {
-            Thread.sleep(1000);
-        }
-        assertNull(EvictionTimer.getExecutor(), "EvictionTimer.getExecutor()");
-    }
-
-    /**
-     * Check that a pool that starts an evictor, but is never closed does not leave EvictionTimer executor running. Confirmation check is in
-     * {@link #tearDown()}.
-     * 
-     * @throws TestException Custom exception
-     * @throws InterruptedException if any thread has interrupted the current thread. The <em>interrupted status</em> of the current thread is cleared when this
-     *         exception is thrown.
-     */
-    @SuppressWarnings("deprecation")
-    @Test
-    public void testAbandonedPool() throws TestException, InterruptedException {
-        final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
-        config.setJmxEnabled(false);
-        GenericObjectPool<String, TestException> abandoned = new GenericObjectPool<>(simpleFactory, config);
-        abandoned.setTimeBetweenEvictionRuns(Duration.ofMillis(100)); // Starts evictor
-        assertEquals(abandoned.getRemoveAbandonedTimeout(), abandoned.getRemoveAbandonedTimeoutDuration().getSeconds());
-
-        // This is ugly, but forces GC to hit the pool
-        final WeakReference<GenericObjectPool<String, TestException>> ref = new WeakReference<>(abandoned);
-        abandoned = null;
-        while (ref.get() != null) {
-            System.gc();
-            Thread.sleep(100);
-        }
-    }
-
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testAddObject() throws Exception {
-        assertEquals( 0, genericObjectPool.getNumIdle(),"should be zero idle");
-        genericObjectPool.addObject();
-        assertEquals( 1, genericObjectPool.getNumIdle(),"should be one idle");
-        assertEquals( 0, genericObjectPool.getNumActive(),"should be zero active");
-        final String obj = genericObjectPool.borrowObject();
-        assertEquals( 0, genericObjectPool.getNumIdle(),"should be zero idle");
-        assertEquals( 1, genericObjectPool.getNumActive(),"should be one active");
-        genericObjectPool.returnObject(obj);
-        assertEquals( 1, genericObjectPool.getNumIdle(),"should be one idle");
-        assertEquals( 0, genericObjectPool.getNumActive(),"should be zero active");
-    }
-
-    @Test
-    public void testAppendStats() {
-        assertFalse(genericObjectPool.getMessageStatistics());
-        assertEquals("foo", (genericObjectPool.appendStats("foo")));
-        try (final GenericObjectPool<?, TestException> pool = new GenericObjectPool<>(new SimpleFactory())) {
-            pool.setMessagesStatistics(true);
-            assertNotEquals("foo", (pool.appendStats("foo")));
-            pool.setMessagesStatistics(false);
-            assertEquals("foo", (pool.appendStats("foo")));
-        }
-    }
-
-    /*
-     * Note: This test relies on timing for correct execution. There *should* be
-     * enough margin for this to work correctly on most (all?) systems but be
-     * aware of this if you see a failure of this test.
-     */
-    @SuppressWarnings({
-        "rawtypes", "unchecked"
-    })
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testBorrowObjectFairness() throws Exception {
-
-        final int numThreads = 40;
-        final int maxTotal = 40;
-
-        final GenericObjectPoolConfig config = new GenericObjectPoolConfig();
-        config.setMaxTotal(maxTotal);
-        config.setMaxIdle(maxTotal);
-        config.setFairness(true);
-        config.setLifo(false);
-
-        genericObjectPool = new GenericObjectPool(simpleFactory, config);
-
-        // Exhaust the pool
-        final String[] objects = new String[maxTotal];
-        for (int i = 0; i < maxTotal; i++) {
-            objects[i] = genericObjectPool.borrowObject();
-        }
-
-        // Start and park threads waiting to borrow objects
-        final TestThread[] threads = new TestThread[numThreads];
-        for(int i=0;i<numThreads;i++) {
-            threads[i] = new TestThread(genericObjectPool, 1, 0, 2000, false, String.valueOf(i % maxTotal));
-            final Thread t = new Thread(threads[i]);
-            t.start();
-            // Short delay to ensure threads start in correct order
-            try {
-                Thread.sleep(10);
-            } catch (final InterruptedException e) {
-                fail(e.toString());
-            }
-        }
-
-        // Return objects, other threads should get served in order
-        for (int i = 0; i < maxTotal; i++) {
-            genericObjectPool.returnObject(objects[i]);
-        }
-
-        // Wait for threads to finish
-        for (int i = 0; i < numThreads; i++) {
-            while (!(threads[i]).complete()) {
-                Waiter.sleepQuietly(500L);
-            }
-            if (threads[i].failed()) {
-                fail("Thread " + i + " failed: " + threads[i].error.toString());
-            }
-        }
-    }
-
-    @Test
-    public void testBorrowTimings() throws Exception {
-        // Borrow
-        final String object = genericObjectPool.borrowObject();
-        final PooledObject<String> po = genericObjectPool.getPooledObject(object);
-        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
-        // In the initial state, the active duration is the time between "now" and the creation time.
-        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
-        // But... this PO might have already been used in other tests in this class.
-
-        final Instant lastBorrowInstant1 = po.getLastBorrowInstant();
-        final Instant lastReturnInstant1 = po.getLastReturnInstant();
-        final Instant lastUsedInstant1 = po.getLastUsedInstant();
-
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastBorrowInstant1));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastReturnInstant1));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastUsedInstant1));
-        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastBorrowInstant1.toEpochMilli()));
-        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastReturnInstant1.toEpochMilli()));
-        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastUsedInstant1.toEpochMilli()));
-
-        // Sleep MUST be "long enough" to detect that more than 0 milliseconds have elapsed.
-        // Need an API in Java 8 to get the clock granularity.
-        Thread.sleep(200);
-
-        assertFalse(po.getActiveDuration().isNegative());
-        assertFalse(po.getActiveDuration().isZero());
-        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
-        assertThat(1L, lessThanOrEqualTo(2L)); // sanity check
-        assertThat(Duration.ZERO, lessThanOrEqualTo(Duration.ZERO.plusNanos(1))); // sanity check
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleDuration()));
-        // Deprecated
-        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
-        //
-        // TODO How to compare ID with AD since other tests may have touched the PO?
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleTime()));
-        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getIdleTimeMillis()));
-        //
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastBorrowInstant()));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastReturnInstant()));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastUsedInstant()));
-
-        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
-        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
-        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
-
-        genericObjectPool.returnObject(object);
-
-        assertFalse(po.getActiveDuration().isNegative());
-        assertFalse(po.getActiveDuration().isZero());
-        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
-
-        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
-        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
-        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
-    }
-
-    /**
-     * On first borrow, first object fails validation, second object is OK.
-     * Subsequent borrows are OK. This was POOL-152.
-     * @throws Exception
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testBrokenFactoryShouldNotBlockPool() throws Exception {
-        final int maxTotal = 1;
-
-        simpleFactory.setMaxTotal(maxTotal);
-        genericObjectPool.setMaxTotal(maxTotal);
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setTestOnBorrow(true);
-
-        // First borrow object will need to create a new object which will fail
-        // validation.
-        String obj = null;
-        Exception ex = null;
-        simpleFactory.setValid(false);
-        try {
-            obj = genericObjectPool.borrowObject();
-        } catch (final Exception e) {
-            ex = e;
-        }
-        // Failure expected
-        assertNotNull(ex);
-        assertTrue(ex instanceof NoSuchElementException);
-        assertNull(obj);
-
-        // Configure factory to create valid objects so subsequent borrows work
-        simpleFactory.setValid(true);
-
-        // Subsequent borrows should be OK
-        obj = genericObjectPool.borrowObject();
-        assertNotNull(obj);
-        genericObjectPool.returnObject(obj);
-    }
-
-    // POOL-259
-    @Test
-    public void testClientWaitStats() throws TestException {
-        final SimpleFactory factory = new SimpleFactory();
-        // Give makeObject a little latency
-        factory.setMakeLatency(200);
-        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(factory, new GenericObjectPoolConfig<>())) {
-            final String s = pool.borrowObject();
-            // First borrow waits on create, so wait time should be at least 200 ms
-            // Allow 100ms error in clock times
-            assertTrue(pool.getMaxBorrowWaitTimeMillis() >= 100);
-            assertTrue(pool.getMeanBorrowWaitTimeMillis() >= 100);
-            pool.returnObject(s);
-            pool.borrowObject();
-            // Second borrow does not have to wait on create, average should be about 100
-            assertTrue(pool.getMaxBorrowWaitTimeMillis() > 100);
-            assertTrue(pool.getMeanBorrowWaitTimeMillis() < 200);
-            assertTrue(pool.getMeanBorrowWaitTimeMillis() > 20);
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testCloseMultiplePools1() {
-        try (final GenericObjectPool<String, TestException> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
-            genericObjectPool.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
-            genericObjectPool2.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
-        }
-        genericObjectPool.close();
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testCloseMultiplePools2() throws Exception {
-        try (final GenericObjectPool<String, TestException> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
-            // Ensure eviction takes a long time, during which time EvictionTimer.executor's queue is empty
-            simpleFactory.setDestroyLatency(1000L);
-            // Ensure there is an object to evict, so that above latency takes effect
-            genericObjectPool.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
-            genericObjectPool2.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
-            genericObjectPool.setMinEvictableIdleTime(TestConstants.ONE_MILLISECOND_DURATION);
-            genericObjectPool2.setMinEvictableIdleTime(TestConstants.ONE_MILLISECOND_DURATION);
-            genericObjectPool.addObject();
-            genericObjectPool2.addObject();
-            // Close both pools
-        }
-        genericObjectPool.close();
-    }
-
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testConcurrentBorrowAndEvict() throws Exception {
-
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.addObject();
-
-        for (int i = 0; i < 5000; i++) {
-            final ConcurrentBorrowAndEvictThread one =
-                    new ConcurrentBorrowAndEvictThread(true);
-            final ConcurrentBorrowAndEvictThread two =
-                    new ConcurrentBorrowAndEvictThread(false);
-
-            one.start();
-            two.start();
-            one.join();
-            two.join();
-
-            genericObjectPool.returnObject(one.obj);
-
-            /* Uncomment this for a progress indication
-            if (i % 10 == 0) {
-                System.out.println(i/10);
-            }
-            */
-        }
-    }
-
-    /**
-     * POOL-231 - verify that concurrent invalidates of the same object do not
-     * corrupt pool destroyCount.
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    public void testConcurrentInvalidate() throws Exception {
-        // Get allObjects and idleObjects loaded with some instances
-        final int nObjects = 1000;
-        genericObjectPool.setMaxTotal(nObjects);
-        genericObjectPool.setMaxIdle(nObjects);
-        final String[] obj = new String[nObjects];
-        for (int i = 0; i < nObjects; i++) {
-            obj[i] = genericObjectPool.borrowObject();
-        }
-        for (int i = 0; i < nObjects; i++) {
-            if (i % 2 == 0) {
-                genericObjectPool.returnObject(obj[i]);
-            }
-        }
-        final int nThreads = 20;
-        final int nIterations = 60;
-        final InvalidateThread[] threads = new InvalidateThread[nThreads];
-        // Randomly generated list of distinct invalidation targets
-        final ArrayList<Integer> targets = new ArrayList<>();
-        final Random random = new Random();
-        for (int j = 0; j < nIterations; j++) {
-            // Get a random invalidation target
-            Integer targ = Integer.valueOf(random.nextInt(nObjects));
-            while (targets.contains(targ)) {
-                targ = Integer.valueOf(random.nextInt(nObjects));
-            }
-            targets.add(targ);
-            // Launch nThreads threads all trying to invalidate the target
-            for (int i = 0; i < nThreads; i++) {
-                threads[i] = new InvalidateThread(genericObjectPool, obj[targ.intValue()]);
-            }
-            for (int i = 0; i < nThreads; i++) {
-                new Thread(threads[i]).start();
-            }
-            boolean done = false;
-            while (!done) {
-                done = true;
-                for (int i = 0; i < nThreads; i++) {
-                    done = done && threads[i].complete();
-                }
-                Thread.sleep(100);
-            }
-        }
-        assertEquals(nIterations, genericObjectPool.getDestroyedCount());
-    }
-
-    @Test
-    public void testConstructorNullFactory() {
-        // add dummy assert (won't be invoked because of IAE) to avoid "unused" warning
-        assertThrows(IllegalArgumentException.class,
-                () -> new GenericObjectPool<>(null));
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testConstructors() {
-
-        // Make constructor arguments all different from defaults
-        final int minIdle = 2;
-        final Duration maxWaitDuration = Duration.ofMillis(3);
-        final long maxWaitMillis = maxWaitDuration.toMillis();
-        final int maxIdle = 4;
-        final int maxTotal = 5;
-        final Duration minEvictableIdleDuration = Duration.ofMillis(6);
-        final long minEvictableIdleMillis = minEvictableIdleDuration.toMillis();
-        final int numTestsPerEvictionRun = 7;
-        final boolean testOnBorrow = true;
-        final boolean testOnReturn = true;
-        final boolean testWhileIdle = true;
-        final long timeBetweenEvictionRunsMillis = 8;
-        final boolean blockWhenExhausted = false;
-        final boolean lifo = false;
-        final PooledObjectFactory<Object, RuntimeException> dummyFactory = new DummyFactory();
-        try (GenericObjectPool<Object, RuntimeException> dummyPool = new GenericObjectPool<>(dummyFactory)) {
-            assertEquals(GenericObjectPoolConfig.DEFAULT_MAX_IDLE, dummyPool.getMaxIdle());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS, dummyPool.getMaxWaitMillis());
-            assertEquals(GenericObjectPoolConfig.DEFAULT_MIN_IDLE, dummyPool.getMinIdle());
-            assertEquals(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL, dummyPool.getMaxTotal());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,
-                    dummyPool.getMinEvictableIdleTimeMillis());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME,
-                    dummyPool.getMinEvictableIdleTime());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME,
-                    dummyPool.getMinEvictableIdleDuration());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN,
-                    dummyPool.getNumTestsPerEvictionRun());
-            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW),
-                    Boolean.valueOf(dummyPool.getTestOnBorrow()));
-            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN),
-                    Boolean.valueOf(dummyPool.getTestOnReturn()));
-            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE),
-                    Boolean.valueOf(dummyPool.getTestWhileIdle()));
-            assertEquals(BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS,
-                    dummyPool.getDurationBetweenEvictionRuns());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,
-                    dummyPool.getTimeBetweenEvictionRunsMillis());
-            assertEquals(BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS,
-                    dummyPool.getTimeBetweenEvictionRuns());
-            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_BLOCK_WHEN_EXHAUSTED),
-                    Boolean.valueOf(dummyPool.getBlockWhenExhausted()));
-            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_LIFO), Boolean.valueOf(dummyPool.getLifo()));
-        }
-
-        final GenericObjectPoolConfig<Object> config = new GenericObjectPoolConfig<>();
-        config.setLifo(lifo);
-        config.setMaxIdle(maxIdle);
-        config.setMinIdle(minIdle);
-        config.setMaxTotal(maxTotal);
-        config.setMaxWait(maxWaitDuration);
-        config.setMinEvictableIdleTimeMillis(minEvictableIdleMillis);
-        assertEquals(minEvictableIdleMillis, config.getMinEvictableIdleTime().toMillis());
-        config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
-        config.setTestOnBorrow(testOnBorrow);
-        config.setTestOnReturn(testOnReturn);
-        config.setTestWhileIdle(testWhileIdle);
-        config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
-        assertEquals(timeBetweenEvictionRunsMillis, config.getTimeBetweenEvictionRuns().toMillis());
-        config.setBlockWhenExhausted(blockWhenExhausted);
-        try (GenericObjectPool<Object, RuntimeException> dummyPool = new GenericObjectPool<>(dummyFactory, config)) {
-            assertEquals(maxIdle, dummyPool.getMaxIdle());
-            assertEquals(maxWaitDuration, dummyPool.getMaxWaitDuration());
-            assertEquals(maxWaitMillis, dummyPool.getMaxWaitMillis());
-            assertEquals(minIdle, dummyPool.getMinIdle());
-            assertEquals(maxTotal, dummyPool.getMaxTotal());
-            assertEquals(minEvictableIdleMillis, dummyPool.getMinEvictableIdleTimeMillis());
-            assertEquals(numTestsPerEvictionRun, dummyPool.getNumTestsPerEvictionRun());
-            assertEquals(Boolean.valueOf(testOnBorrow), Boolean.valueOf(dummyPool.getTestOnBorrow()));
-            assertEquals(Boolean.valueOf(testOnReturn), Boolean.valueOf(dummyPool.getTestOnReturn()));
-            assertEquals(Boolean.valueOf(testWhileIdle), Boolean.valueOf(dummyPool.getTestWhileIdle()));
-            assertEquals(timeBetweenEvictionRunsMillis, dummyPool.getTimeBetweenEvictionRunsMillis());
-            assertEquals(Boolean.valueOf(blockWhenExhausted), Boolean.valueOf(dummyPool.getBlockWhenExhausted()));
-            assertEquals(Boolean.valueOf(lifo), Boolean.valueOf(dummyPool.getLifo()));
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testDefaultConfiguration() {
-        assertConfiguration(new GenericObjectPoolConfig<>(),genericObjectPool);
-    }
-
-    /**
-     * Verifies that when a factory's makeObject produces instances that are not
-     * discernible by equals, the pool can handle them.
-     *
-     * JIRA: POOL-283
-     */
-    @Test
-    public void testEqualsIndiscernible() throws Exception {
-        final HashSetFactory factory = new HashSetFactory();
-        try (final GenericObjectPool<HashSet<String>, RuntimeException> pool = new GenericObjectPool<>(factory,
-                new GenericObjectPoolConfig<>())) {
-            final HashSet<String> s1 = pool.borrowObject();
-            final HashSet<String> s2 = pool.borrowObject();
-            pool.returnObject(s1);
-            pool.returnObject(s2);
-        }
-    }
-
-    @Test
-    public void testErrorFactoryDoesNotBlockThreads() throws Exception {
-
-        final CreateErrorFactory factory = new CreateErrorFactory();
-        try (final GenericObjectPool<String, InterruptedException> createFailFactoryPool = new GenericObjectPool<>(factory)) {
-
-            createFailFactoryPool.setMaxTotal(1);
-
-            // Try and borrow the first object from the pool
-            final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createFailFactoryPool, 0);
-            thread1.start();
-
-            // Wait for thread to reach semaphore
-            while (!factory.hasQueuedThreads()) {
-                Thread.sleep(200);
-            }
-
-            // Try and borrow the second object from the pool
-            final WaitingTestThread<InterruptedException> thread2 = new WaitingTestThread<>(createFailFactoryPool, 0);
-            thread2.start();
-            // Pool will not call factory since maximum number of object creations
-            // are already queued.
-
-            // Thread 2 will wait on an object being returned to the pool
-            // Give thread 2 a chance to reach this state
-            Thread.sleep(1000);
-
-            // Release thread1
-            factory.release();
-            // Pre-release thread2
-            factory.release();
-
-            // Both threads should now complete.
-            boolean threadRunning = true;
-            int count = 0;
-            while (threadRunning && count < 15) {
-                threadRunning = thread1.isAlive();
-                threadRunning = thread2.isAlive();
-                Thread.sleep(200);
-                count++;
-            }
-            assertFalse(thread1.isAlive());
-            assertFalse(thread2.isAlive());
-
-            assertTrue(thread1.thrown instanceof UnknownError);
-            assertTrue(thread2.thrown instanceof UnknownError);
-        }
-    }
-
-    /**
-     * Tests addObject contention between ensureMinIdle triggered by
-     * the Evictor with minIdle &gt; 0 and borrowObject.
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictAddObjects() throws Exception {
-        simpleFactory.setMakeLatency(300);
-        simpleFactory.setMaxTotal(2);
-        genericObjectPool.setMaxTotal(2);
-        genericObjectPool.setMinIdle(1);
-        genericObjectPool.borrowObject(); // numActive = 1, numIdle = 0
-        // Create a test thread that will run once and try a borrow after
-        // 150ms fixed delay
-        final TestThread<String, TestException> borrower = new TestThread<>(genericObjectPool, 1, 150, false);
-        final Thread borrowerThread = new Thread(borrower);
-        // Set evictor to run in 100 ms - will create idle instance
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
-        borrowerThread.start();  // Off to the races
-        borrowerThread.join();
-        assertFalse(borrower.failed());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictFIFO() throws Exception {
-        checkEvict(false);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEviction() throws Exception {
-        genericObjectPool.setMaxIdle(500);
-        genericObjectPool.setMaxTotal(500);
-        genericObjectPool.setNumTestsPerEvictionRun(100);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(250));
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
-        genericObjectPool.setTestWhileIdle(true);
-
-        final String[] active = new String[500];
-        for (int i = 0; i < 500; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-        for (int i = 0; i < 500; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        Waiter.sleepQuietly(1000L);
-        assertTrue(genericObjectPool.getNumIdle() < 500,"Should be less than 500 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 400,"Should be less than 400 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 300,"Should be less than 300 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 200,"Should be less than 200 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 100,"Should be less than 100 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertEquals(0,genericObjectPool.getNumIdle(),"Should be zero idle, found " + genericObjectPool.getNumIdle());
-
-        for (int i = 0; i < 500; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-        for (int i = 0; i < 500; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        Waiter.sleepQuietly(1000L);
-        assertTrue(genericObjectPool.getNumIdle() < 500,"Should be less than 500 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 400,"Should be less than 400 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 300,"Should be less than 300 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 200,"Should be less than 200 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertTrue(genericObjectPool.getNumIdle() < 100,"Should be less than 100 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(600L);
-        assertEquals(0,genericObjectPool.getNumIdle(),"Should be zero idle, found " + genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictionInvalid() throws Exception {
-
-        try (final GenericObjectPool<Object, RuntimeException> invalidFactoryPool = new GenericObjectPool<>(new InvalidFactory())) {
-
-            invalidFactoryPool.setMaxIdle(1);
-            invalidFactoryPool.setMaxTotal(1);
-            invalidFactoryPool.setTestOnBorrow(false);
-            invalidFactoryPool.setTestOnReturn(false);
-            invalidFactoryPool.setTestWhileIdle(true);
-            invalidFactoryPool.setMinEvictableIdleTime(Duration.ofSeconds(100));
-            invalidFactoryPool.setNumTestsPerEvictionRun(1);
-
-            final Object p = invalidFactoryPool.borrowObject();
-            invalidFactoryPool.returnObject(p);
-
-            // Run eviction in a separate thread
-            final Thread t = new EvictionThread<>(invalidFactoryPool);
-            t.start();
-
-            // Sleep to make sure evictor has started
-            Thread.sleep(300);
-
-            try {
-                invalidFactoryPool.borrowObject(1);
-            } catch (final NoSuchElementException nsee) {
-                // Ignore
-            }
-
-            // Make sure evictor has finished
-            Thread.sleep(1000);
-
-            // Should have an empty pool
-            assertEquals( 0, invalidFactoryPool.getNumIdle(),"Idle count different than expected.");
-            assertEquals( 0, invalidFactoryPool.getNumActive(),"Total count different than expected.");
-        }
-    }
-
-    /**
-     * Test to make sure evictor visits least recently used objects first,
-     * regardless of FIFO/LIFO.
-     *
-     * JIRA: POOL-86
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictionOrder() throws Exception {
-        checkEvictionOrder(false);
-        tearDown();
-        setUp();
-        checkEvictionOrder(true);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictionPolicy() throws Exception {
-        genericObjectPool.setMaxIdle(500);
-        genericObjectPool.setMaxTotal(500);
-        genericObjectPool.setNumTestsPerEvictionRun(500);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(250));
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
-        genericObjectPool.setTestWhileIdle(true);
-
-        // ClassNotFoundException
-        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(Long.toString(System.currentTimeMillis())),
-                "setEvictionPolicyClassName must throw an error if the class name is invalid.");
-
-        // InstantiationException
-        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(java.io.Serializable.class.getName()),
-                "setEvictionPolicyClassName must throw an error if the class name is invalid.");
-
-        // IllegalAccessException
-        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(java.util.Collections.class.getName()),
-                "setEvictionPolicyClassName must throw an error if the class name is invalid.");
-
-        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(java.lang.String.class.getName()),
-                () -> "setEvictionPolicyClassName must throw an error if a class that does not implement EvictionPolicy is specified.");
-
-        genericObjectPool.setEvictionPolicy(new TestEvictionPolicy<>());
-        assertEquals(TestEvictionPolicy.class.getName(), genericObjectPool.getEvictionPolicyClassName());
-
-        genericObjectPool.setEvictionPolicyClassName(TestEvictionPolicy.class.getName());
-        assertEquals(TestEvictionPolicy.class.getName(), genericObjectPool.getEvictionPolicyClassName());
-
-        final String[] active = new String[500];
-        for (int i = 0; i < 500; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-        for (int i = 0; i < 500; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        // Eviction policy ignores first 1500 attempts to evict and then always
-        // evicts. After 1s, there should have been two runs of 500 tests so no
-        // evictions
-        Waiter.sleepQuietly(1000L);
-        assertEquals(500, genericObjectPool.getNumIdle(), "Should be 500 idle");
-        // A further 1s wasn't enough so allow 2s for the evictor to clear out
-        // all of the idle objects.
-        Waiter.sleepQuietly(2000L);
-        assertEquals(0, genericObjectPool.getNumIdle(), "Should be 0 idle");
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictionSoftMinIdle() throws Exception {
-        class TimeTest extends BasePooledObjectFactory<TimeTest, RuntimeException> {
-            private final long createTimeMillis;
-
-            public TimeTest() {
-                createTimeMillis = System.currentTimeMillis();
-            }
-
-            @Override
-            public TimeTest create() {
-                return new TimeTest();
-            }
-
-            public long getCreateTimeMillis() {
-                return createTimeMillis;
-            }
-
-            @Override
-            public PooledObject<TimeTest> wrap(final TimeTest value) {
-                return new DefaultPooledObject<>(value);
-            }
-        }
-
-        try (final GenericObjectPool<TimeTest, RuntimeException> timePool = new GenericObjectPool<>(new TimeTest())) {
-
-            timePool.setMaxIdle(5);
-            timePool.setMaxTotal(5);
-            timePool.setNumTestsPerEvictionRun(5);
-            timePool.setMinEvictableIdle(Duration.ofSeconds(3));
-            timePool.setMinEvictableIdleTime(Duration.ofSeconds(3));
-            timePool.setSoftMinEvictableIdleTime(TestConstants.ONE_SECOND_DURATION);
-            timePool.setMinIdle(2);
-
-            final TimeTest[] active = new TimeTest[5];
-            final Long[] creationTime = new Long[5];
-            for (int i = 0; i < 5; i++) {
-                active[i] = timePool.borrowObject();
-                creationTime[i] = Long.valueOf((active[i]).getCreateTimeMillis());
-            }
-
-            for (int i = 0; i < 5; i++) {
-                timePool.returnObject(active[i]);
-            }
-
-            // Soft evict all but minIdle(2)
-            Thread.sleep(1500L);
-            timePool.evict();
-            assertEquals( 2, timePool.getNumIdle(),"Idle count different than expected.");
-
-            // Hard evict the rest.
-            Thread.sleep(2000L);
-            timePool.evict();
-            assertEquals( 0, timePool.getNumIdle(),"Idle count different than expected.");
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictionWithNegativeNumTests() throws Exception {
-        // when numTestsPerEvictionRun is negative, it represents a fraction of the idle objects to test
-        genericObjectPool.setMaxIdle(6);
-        genericObjectPool.setMaxTotal(6);
-        genericObjectPool.setNumTestsPerEvictionRun(-2);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(50));
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
-
-        final String[] active = new String[6];
-        for (int i = 0; i < 6; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-        for (int i = 0; i < 6; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        Waiter.sleepQuietly(100L);
-        assertTrue(genericObjectPool.getNumIdle() <= 6,"Should at most 6 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(100L);
-        assertTrue(genericObjectPool.getNumIdle() <= 3,"Should at most 3 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(100L);
-        assertTrue(genericObjectPool.getNumIdle() <= 2,"Should be at most 2 idle, found " + genericObjectPool.getNumIdle());
-        Waiter.sleepQuietly(100L);
-        assertEquals(0,genericObjectPool.getNumIdle(),"Should be zero idle, found " + genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictLIFO() throws Exception {
-        checkEvict(true);
-    }
-
-    /**
-     * Verifies that the evictor visits objects in expected order
-     * and frequency.
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    public void testEvictorVisiting() throws Exception {
-        checkEvictorVisiting(true);
-        checkEvictorVisiting(false);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testEvictWhileEmpty() throws Exception {
-        genericObjectPool.evict();
-        genericObjectPool.evict();
-        genericObjectPool.close();
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testExceptionInValidationDuringEviction() throws Exception {
-        genericObjectPool.setMaxIdle(1);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ZERO);
-        genericObjectPool.setTestWhileIdle(true);
-
-        final String active = genericObjectPool.borrowObject();
-        genericObjectPool.returnObject(active);
-
-        simpleFactory.setThrowExceptionOnValidate(true);
-
-        assertThrows(RuntimeException.class, () -> genericObjectPool.evict());
-        assertEquals(0, genericObjectPool.getNumActive());
-        assertEquals(0, genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testExceptionOnActivateDuringBorrow() throws Exception {
-        final String obj1 = genericObjectPool.borrowObject();
-        final String obj2 = genericObjectPool.borrowObject();
-        genericObjectPool.returnObject(obj1);
-        genericObjectPool.returnObject(obj2);
-        simpleFactory.setThrowExceptionOnActivate(true);
-        simpleFactory.setEvenValid(false);
-        // Activation will now throw every other time
-        // First attempt throws, but loop continues and second succeeds
-        final String obj = genericObjectPool.borrowObject();
-        assertEquals(1, genericObjectPool.getNumActive());
-        assertEquals(0, genericObjectPool.getNumIdle());
-
-        genericObjectPool.returnObject(obj);
-        simpleFactory.setValid(false);
-        // Validation will now fail on activation when borrowObject returns
-        // an idle instance, and then when attempting to create a new instance
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-        assertEquals(0, genericObjectPool.getNumActive());
-        assertEquals(0, genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testExceptionOnDestroyDuringBorrow() throws Exception {
-        simpleFactory.setThrowExceptionOnDestroy(true);
-        genericObjectPool.setTestOnBorrow(true);
-        genericObjectPool.borrowObject();
-        simpleFactory.setValid(false); // Make validation fail on next borrow attempt
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-        assertEquals(1, genericObjectPool.getNumActive());
-        assertEquals(0, genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testExceptionOnDestroyDuringReturn() throws Exception {
-        simpleFactory.setThrowExceptionOnDestroy(true);
-        genericObjectPool.setTestOnReturn(true);
-        final String obj1 = genericObjectPool.borrowObject();
-        genericObjectPool.borrowObject();
-        simpleFactory.setValid(false); // Make validation fail
-        genericObjectPool.returnObject(obj1);
-        assertEquals(1, genericObjectPool.getNumActive());
-        assertEquals(0, genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testExceptionOnPassivateDuringReturn() throws Exception {
-        final String obj = genericObjectPool.borrowObject();
-        simpleFactory.setThrowExceptionOnPassivate(true);
-        genericObjectPool.returnObject(obj);
-        assertEquals(0,genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    public void testFailingFactoryDoesNotBlockThreads() throws Exception {
-
-        final CreateFailFactory factory = new CreateFailFactory();
-        try (final GenericObjectPool<String, InterruptedException> createFailFactoryPool = new GenericObjectPool<>(factory)) {
-
-            createFailFactoryPool.setMaxTotal(1);
-
-            // Try and borrow the first object from the pool
-            final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createFailFactoryPool, 0);
-            thread1.start();
-
-            // Wait for thread to reach semaphore
-            while (!factory.hasQueuedThreads()) {
-                Thread.sleep(200);
-            }
-
-            // Try and borrow the second object from the pool
-            final WaitingTestThread<InterruptedException> thread2 = new WaitingTestThread<>(createFailFactoryPool, 0);
-            thread2.start();
-            // Pool will not call factory since maximum number of object creations
-            // are already queued.
-
-            // Thread 2 will wait on an object being returned to the pool
-            // Give thread 2 a chance to reach this state
-            Thread.sleep(1000);
-
-            // Release thread1
-            factory.release();
-            // Pre-release thread2
-            factory.release();
-
-            // Both threads should now complete.
-            boolean threadRunning = true;
-            int count = 0;
-            while (threadRunning && count < 15) {
-                threadRunning = thread1.isAlive();
-                threadRunning = thread2.isAlive();
-                Thread.sleep(200);
-                count++;
-            }
-            assertFalse(thread1.isAlive());
-            assertFalse(thread2.isAlive());
-
-            assertTrue(thread1.thrown instanceof UnsupportedCharsetException);
-            assertTrue(thread2.thrown instanceof UnsupportedCharsetException);
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testFIFO() throws Exception {
-        genericObjectPool.setLifo(false);
-        genericObjectPool.addObject(); // "0"
-        genericObjectPool.addObject(); // "1"
-        genericObjectPool.addObject(); // "2"
-        assertEquals( "0", genericObjectPool.borrowObject(),"Oldest");
-        assertEquals( "1", genericObjectPool.borrowObject(),"Middle");
-        assertEquals( "2", genericObjectPool.borrowObject(),"Youngest");
-        final String o = genericObjectPool.borrowObject();
-        assertEquals( "3", o,"new-3");
-        genericObjectPool.returnObject(o);
-        assertEquals( o, genericObjectPool.borrowObject(),"returned-3");
-        assertEquals( "4", genericObjectPool.borrowObject(),"new-4");
-    }
-
-    @Test
-    public void testGetFactoryType_DefaultPooledObjectFactory() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(createDefaultPooledObjectFactory())) {
-            assertNotNull((pool.getFactoryType()));
-        }
-    }
-
-    @Test
-    public void testGetFactoryType_NullPooledObjectFactory() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(createNullPooledObjectFactory())) {
-            assertNotNull((pool.getFactoryType()));
-        }
-    }
-
-    @Test
-    public void testGetFactoryType_PoolUtilsSynchronizedDefaultPooledFactory() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
-                PoolUtils.synchronizedPooledFactory(createDefaultPooledObjectFactory()))) {
-            assertNotNull((pool.getFactoryType()));
-        }
-    }
-
-    @Test
-    public void testGetFactoryType_PoolUtilsSynchronizedNullPooledFactory() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
-                PoolUtils.synchronizedPooledFactory(createNullPooledObjectFactory()))) {
-            assertNotNull((pool.getFactoryType()));
-        }
-    }
-
-    @Test
-    public void testGetFactoryType_SynchronizedDefaultPooledObjectFactory() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
-                new TestSynchronizedPooledObjectFactory<>(createDefaultPooledObjectFactory()))) {
-            assertNotNull((pool.getFactoryType()));
-        }
-    }
-
-    @Test
-    public void testGetFactoryType_SynchronizedNullPooledObjectFactory() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
-                new TestSynchronizedPooledObjectFactory<>(createNullPooledObjectFactory()))) {
-            assertNotNull((pool.getFactoryType()));
-        }
-    }
-
-    @Test
-    public void testGetStatsString() {
-        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
-                new TestSynchronizedPooledObjectFactory<>(createNullPooledObjectFactory()))) {
-            assertNotNull(pool.getStatsString());
-        }
-    }
-
-    /**
-     * Verify that threads waiting on a depleted pool get served when a checked out object is
-     * invalidated.
-     *
-     * JIRA: POOL-240
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    public void testInvalidateFreesCapacity() throws Exception {
-        final SimpleFactory factory = new SimpleFactory();
-        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(factory)) {
-            pool.setMaxTotal(2);
-            pool.setMaxWaitMillis(500);
-            // Borrow an instance and hold if for 5 seconds
-            final WaitingTestThread<TestException> thread1 = new WaitingTestThread<>(pool, 5000);
-            thread1.start();
-            // Borrow another instance
-            final String obj = pool.borrowObject();
-            // Launch another thread - will block, but fail in 500 ms
-            final WaitingTestThread<TestException> thread2 = new WaitingTestThread<>(pool, 100);
-            thread2.start();
-            // Invalidate the object borrowed by this thread - should allow thread2 to create
-            Thread.sleep(20);
-            pool.invalidateObject(obj);
-            Thread.sleep(600); // Wait for thread2 to timeout
-            if (thread2.thrown != null) {
-                fail(thread2.thrown.toString());
-            }
-        }
-    }
-
-    /**
-     * Ensure the pool is registered.
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testJmxRegistration() {
-        final ObjectName oname = genericObjectPool.getJmxName();
-        final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
-        final Set<ObjectName> result = mbs.queryNames(oname, null);
-        assertEquals(1, result.size());
-        genericObjectPool.jmxUnregister();
-
-        final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
-        config.setJmxEnabled(false);
-        try (final GenericObjectPool<String, TestException> poolWithoutJmx = new GenericObjectPool<>(simpleFactory, config)) {
-            assertNull(poolWithoutJmx.getJmxName());
-            config.setJmxEnabled(true);
-            poolWithoutJmx.jmxUnregister();
-        }
-
-        config.setJmxNameBase(null);
-        try (final GenericObjectPool<String, TestException> poolWithDefaultJmxNameBase = new GenericObjectPool<>(simpleFactory, config)) {
-            assertNotNull(poolWithDefaultJmxNameBase.getJmxName());
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testLIFO() throws Exception {
-        final String o;
-        genericObjectPool.setLifo(true);
-        genericObjectPool.addObject(); // "0"
-        genericObjectPool.addObject(); // "1"
-        genericObjectPool.addObject(); // "2"
-        assertEquals( "2", genericObjectPool.borrowObject(),"Youngest");
-        assertEquals( "1", genericObjectPool.borrowObject(),"Middle");
-        assertEquals( "0", genericObjectPool.borrowObject(),"Oldest");
-        o = genericObjectPool.borrowObject();
-        assertEquals( "3", o,"new-3");
-        genericObjectPool.returnObject(o);
-        assertEquals( o, genericObjectPool.borrowObject(),"returned-3");
-        assertEquals( "4", genericObjectPool.borrowObject(),"new-4");
-    }
-
-    /**
-     * Test the following scenario:
-     *   Thread 1 borrows an instance
-     *   Thread 2 starts to borrow another instance before thread 1 returns its instance
-     *   Thread 1 returns its instance while thread 2 is validating its newly created instance
-     * The test verifies that the instance created by Thread 2 is not leaked.
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMakeConcurrentWithReturn() throws Exception {
-        genericObjectPool.setTestOnBorrow(true);
-        simpleFactory.setValid(true);
-        // Borrow and return an instance, with a short wait
-        final WaitingTestThread<TestException> thread1 = new WaitingTestThread<>(genericObjectPool, 200);
-        thread1.start();
-        Thread.sleep(50); // wait for validation to succeed
-        // Slow down validation and borrow an instance
-        simpleFactory.setValidateLatency(400);
-        final String instance = genericObjectPool.borrowObject();
-        // Now make sure that we have not leaked an instance
-        assertEquals(simpleFactory.getMakeCounter(), genericObjectPool.getNumIdle() + 1);
-        genericObjectPool.returnObject(instance);
-        assertEquals(simpleFactory.getMakeCounter(), genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxIdle() throws Exception {
-        genericObjectPool.setMaxTotal(100);
-        genericObjectPool.setMaxIdle(8);
-        final String[] active = new String[100];
-        for(int i=0;i<100;i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-        assertEquals(100,genericObjectPool.getNumActive());
-        assertEquals(0,genericObjectPool.getNumIdle());
-        for(int i=0;i<100;i++) {
-            genericObjectPool.returnObject(active[i]);
-            assertEquals(99 - i,genericObjectPool.getNumActive());
-            assertEquals((i < 8 ? i+1 : 8),genericObjectPool.getNumIdle());
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxIdleZero() throws Exception {
-        genericObjectPool.setMaxTotal(100);
-        genericObjectPool.setMaxIdle(0);
-        final String[] active = new String[100];
-        for(int i=0;i<100;i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-        assertEquals(100,genericObjectPool.getNumActive());
-        assertEquals(0,genericObjectPool.getNumIdle());
-        for(int i=0;i<100;i++) {
-            genericObjectPool.returnObject(active[i]);
-            assertEquals(99 - i,genericObjectPool.getNumActive());
-            assertEquals(0, genericObjectPool.getNumIdle());
-        }
-    }
-
-    /**
-     * Showcasing a possible deadlock situation as reported in POOL-356
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    @SuppressWarnings("rawtypes")
-    public void testMaxIdleZeroUnderLoad() {
-        // Config
-        final int numThreads = 199; // And main thread makes a round 200.
-        final int numIter = 20;
-        final int delay = 25;
-        final int maxTotal = 10;
-
-        simpleFactory.setMaxTotal(maxTotal);
-        genericObjectPool.setMaxTotal(maxTotal);
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(-1));
-
-        // this is important to trigger POOL-356
-        genericObjectPool.setMaxIdle(0);
-
-        // Start threads to borrow objects
-        final TestThread[] threads = new TestThread[numThreads];
-        for(int i=0;i<numThreads;i++) {
-            // Factor of 2 on iterations so main thread does work whilst other
-            // threads are running. Factor of 2 on delay so average delay for
-            // other threads == actual delay for main thread
-            threads[i] = new TestThread<>(genericObjectPool, numIter * 2, delay * 2);
-            final Thread t = new Thread(threads[i]);
-            t.start();
-        }
-        // Give the threads a chance to start doing some work
-        Waiter.sleepQuietly(100L);
-
-        for (int i = 0; i < numIter; i++) {
-            String obj = null;
-            try {
-                Waiter.sleepQuietly(delay);
-                obj = genericObjectPool.borrowObject();
-                // Under load, observed numActive > maxTotal
-                if (genericObjectPool.getNumActive() > genericObjectPool.getMaxTotal()) {
-                    throw new IllegalStateException("Too many active objects");
-                }
-                Waiter.sleepQuietly(delay);
-            } catch (final Exception e) {
-                // Shouldn't happen
-                e.printStackTrace();
-                fail("Exception on borrow");
-            } finally {
-                if (obj != null) {
-                    try {
-                        genericObjectPool.returnObject(obj);
-                    } catch (final Exception e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        for (int i = 0; i < numThreads; i++) {
-            while (!(threads[i]).complete()) {
-                Waiter.sleepQuietly(500L);
-            }
-            if (threads[i].failed()) {
-                threads[i].error.printStackTrace();
-                fail("Thread " + i + " failed: " + threads[i].error.toString());
-            }
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxTotal() throws Exception {
-        genericObjectPool.setMaxTotal(3);
-        genericObjectPool.setBlockWhenExhausted(false);
-
-        genericObjectPool.borrowObject();
-        genericObjectPool.borrowObject();
-        genericObjectPool.borrowObject();
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-    }
-
-    /**
-     * Verifies that maxTotal is not exceeded when factory destroyObject
-     * has high latency, testOnReturn is set and there is high incidence of
-     * validation failures.
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxTotalInvariant() {
-        final int maxTotal = 15;
-        simpleFactory.setEvenValid(false);     // Every other validation fails
-        simpleFactory.setDestroyLatency(100);  // Destroy takes 100 ms
-        simpleFactory.setMaxTotal(maxTotal); // (makes - destroys) bound
-        simpleFactory.setValidationEnabled(true);
-        genericObjectPool.setMaxTotal(maxTotal);
-        genericObjectPool.setMaxIdle(-1);
-        genericObjectPool.setTestOnReturn(true);
-        genericObjectPool.setMaxWaitMillis(1000L);
-        runTestThreads(5, 10, 50, genericObjectPool);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    @SuppressWarnings("rawtypes")
-    public void testMaxTotalUnderLoad() {
-        // Config
-        final int numThreads = 199; // And main thread makes a round 200.
-        final int numIter = 20;
-        final int delay = 25;
-        final int maxTotal = 10;
-
-        simpleFactory.setMaxTotal(maxTotal);
-        genericObjectPool.setMaxTotal(maxTotal);
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(-1));
-
-        // Start threads to borrow objects
-        final TestThread[] threads = new TestThread[numThreads];
-        for(int i=0;i<numThreads;i++) {
-            // Factor of 2 on iterations so main thread does work whilst other
-            // threads are running. Factor of 2 on delay so average delay for
-            // other threads == actual delay for main thread
-            threads[i] = new TestThread<>(genericObjectPool, numIter * 2, delay * 2);
-            final Thread t = new Thread(threads[i]);
-            t.start();
-        }
-        // Give the threads a chance to start doing some work
-        Waiter.sleepQuietly(5000);
-
-        for (int i = 0; i < numIter; i++) {
-            String obj = null;
-            try {
-                Waiter.sleepQuietly(delay);
-                obj = genericObjectPool.borrowObject();
-                // Under load, observed numActive > maxTotal
-                if (genericObjectPool.getNumActive() > genericObjectPool.getMaxTotal()) {
-                    throw new IllegalStateException("Too many active objects");
-                }
-                Waiter.sleepQuietly(delay);
-            } catch (final Exception e) {
-                // Shouldn't happen
-                e.printStackTrace();
-                fail("Exception on borrow");
-            } finally {
-                if (obj != null) {
-                    try {
-                        genericObjectPool.returnObject(obj);
-                    } catch (final Exception e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        for (int i = 0; i < numThreads; i++) {
-            while(!(threads[i]).complete()) {
-                Waiter.sleepQuietly(500L);
-            }
-            if(threads[i].failed()) {
-                fail("Thread " + i + " failed: " + threads[i].error.toString());
-            }
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxTotalZero() throws Exception {
-        genericObjectPool.setMaxTotal(0);
-        genericObjectPool.setBlockWhenExhausted(false);
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-    }
-
-    /*
-     * Test multi-threaded pool access.
-     * Multiple threads, but maxTotal only allows half the threads to succeed.
-     *
-     * This test was prompted by Continuum build failures in the Commons DBCP test case:
-     * TestPerUserPoolDataSource.testMultipleThreads2()
-     * Let's see if the this fails on Continuum too!
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxWaitMultiThreaded() throws Exception {
-        final long maxWait = 500; // wait for connection
-        final long holdTime = 2 * maxWait; // how long to hold connection
-        final int threads = 10; // number of threads to grab the object initially
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setMaxWaitMillis(maxWait);
-        genericObjectPool.setMaxTotal(threads);
-        // Create enough threads so half the threads will have to wait
-        final WaitingTestThread<TestException>[] wtt = new WaitingTestThread[threads * 2];
-        for (int i = 0; i < wtt.length; i++) {
-            wtt[i] = new WaitingTestThread<>(genericObjectPool, holdTime);
-        }
-        final long originMillis = System.currentTimeMillis() - 1000;
-        for (final WaitingTestThread<TestException> element : wtt) {
-            element.start();
-        }
-        int failed = 0;
-        for (final WaitingTestThread<TestException> element : wtt) {
-            element.join();
-            if (element.thrown != null){
-                failed++;
-            }
-        }
-        if (DISPLAY_THREAD_DETAILS || wtt.length/2 != failed){
-            System.out.println(
-                    "MaxWait: " + maxWait +
-                    " HoldTime: " + holdTime +
-                     " MaxTotal: " + threads +
-                    " Threads: " + wtt.length +
-                    " Failed: " + failed
-                    );
-            for (final WaitingTestThread<TestException> wt : wtt) {
-                System.out.println(
-                        "PreBorrow: " + (wt.preBorrowMillis - originMillis) +
-                        " PostBorrow: " + (wt.postBorrowMillis != 0 ? wt.postBorrowMillis - originMillis : -1) +
-                        " BorrowTime: " + (wt.postBorrowMillis != 0 ? wt.postBorrowMillis - wt.preBorrowMillis : -1) +
-                        " PostReturn: " + (wt.postReturnMillis != 0 ? wt.postReturnMillis - originMillis : -1) +
-                        " Ended: " + (wt.endedMillis - originMillis) +
-                        " ObjId: " + wt.objectId
-                        );
-            }
-        }
-        assertEquals(wtt.length / 2, failed,"Expected half the threads to fail");
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMinIdle() throws Exception {
-        genericObjectPool.setMaxIdle(500);
-        genericObjectPool.setMinIdle(5);
-        genericObjectPool.setMaxTotal(10);
-        genericObjectPool.setNumTestsPerEvictionRun(0);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(50));
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
-        genericObjectPool.setTestWhileIdle(true);
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
-
-        final String[] active = new String[5];
-        active[0] = genericObjectPool.borrowObject();
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
-
-        for (int i = 1; i < 5; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
-
-        for (int i = 0; i < 5; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(10, genericObjectPool.getNumIdle(), "Should be 10 idle, found " + genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMinIdleMaxTotal() throws Exception {
-        genericObjectPool.setMaxIdle(500);
-        genericObjectPool.setMinIdle(5);
-        genericObjectPool.setMaxTotal(10);
-        genericObjectPool.setNumTestsPerEvictionRun(0);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(50));
-        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
-        genericObjectPool.setTestWhileIdle(true);
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
-
-        final String[] active = new String[10];
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
-
-        for (int i = 0; i < 5; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
-
-        for(int i = 0 ; i < 5 ; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(10, genericObjectPool.getNumIdle(), "Should be 10 idle, found " + genericObjectPool.getNumIdle());
-
-        for (int i = 0; i < 10; i++) {
-            active[i] = genericObjectPool.borrowObject();
-        }
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(0, genericObjectPool.getNumIdle(), "Should be 0 idle, found " + genericObjectPool.getNumIdle());
-
-        for (int i = 0; i < 10; i++) {
-            genericObjectPool.returnObject(active[i]);
-        }
-
-        Waiter.sleepQuietly(150L);
-        assertEquals(10, genericObjectPool.getNumIdle(), "Should be 10 idle, found " + genericObjectPool.getNumIdle());
-    }
-
-    /**
-     * Verifies that returning an object twice (without borrow in between) causes ISE
-     * but does not re-validate or re-passivate the instance.
-     *
-     * JIRA: POOL-285
-     */
-    @Test
-    public void testMultipleReturn() throws Exception {
-        final WaiterFactory<String> factory = new WaiterFactory<>(0, 0, 0, 0, 0, 0);
-        try (final GenericObjectPool<Waiter, IllegalStateException> pool = new GenericObjectPool<>(factory)) {
-            pool.setTestOnReturn(true);
-            final Waiter waiter = pool.borrowObject();
-            pool.returnObject(waiter);
-            assertEquals(1, waiter.getValidationCount());
-            assertEquals(1, waiter.getPassivationCount());
-            try {
-                pool.returnObject(waiter);
-                fail("Expecting IllegalStateException from multiple return");
-            } catch (final IllegalStateException ex) {
-                // Exception is expected, now check no repeat validation/passivation
-                assertEquals(1, waiter.getValidationCount());
-                assertEquals(1, waiter.getPassivationCount());
-            }
-        }
-    }
-
-    // POOL-248
-    @Test
-    public void testMultipleReturnOfSameObject() throws Exception {
-        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(simpleFactory, new GenericObjectPoolConfig<>())) {
-
-            assertEquals(0, pool.getNumActive());
-            assertEquals(0, pool.getNumIdle());
-
-            final String obj = pool.borrowObject();
-
-            assertEquals(1, pool.getNumActive());
-            assertEquals(0, pool.getNumIdle());
-
-            pool.returnObject(obj);
-
-            assertEquals(0, pool.getNumActive());
-            assertEquals(1, pool.getNumIdle());
-
-            assertThrows(IllegalStateException.class,
-                    () -> pool.returnObject(obj));
-
-            assertEquals(0, pool.getNumActive());
-            assertEquals(1, pool.getNumIdle());
-        }
-    }
-
-    /**
-     * Verifies that when a borrowed object is mutated in a way that does not
-     * preserve equality and hashcode, the pool can recognized it on return.
-     *
-     * JIRA: POOL-284
-     */
-    @Test
-    public void testMutable() throws Exception {
-        final HashSetFactory factory = new HashSetFactory();
-        try (final GenericObjectPool<HashSet<String>, RuntimeException> pool = new GenericObjectPool<>(factory,
-                new GenericObjectPoolConfig<>())) {
-            final HashSet<String> s1 = pool.borrowObject();
-            final HashSet<String> s2 = pool.borrowObject();
-            s1.add("One");
-            s2.add("One");
-            pool.returnObject(s1);
-            pool.returnObject(s2);
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testNegativeMaxTotal() throws Exception {
-        genericObjectPool.setMaxTotal(-1);
-        genericObjectPool.setBlockWhenExhausted(false);
-        final String obj = genericObjectPool.borrowObject();
-        assertEquals(getNthObject(0),obj);
-        genericObjectPool.returnObject(obj);
-    }
-
-    /**
-     * Verifies that concurrent threads never "share" instances
-     */
-    @Test
-    public void testNoInstanceOverlap() {
-        final int maxTotal = 5;
-        final int numThreads = 100;
-        final int delay = 1;
-        final int iterations = 1000;
-        final AtomicIntegerFactory factory = new AtomicIntegerFactory();
-        try (final GenericObjectPool<AtomicInteger, RuntimeException> pool = new GenericObjectPool<>(factory)) {
-            pool.setMaxTotal(maxTotal);
-            pool.setMaxIdle(maxTotal);
-            pool.setTestOnBorrow(true);
-            pool.setBlockWhenExhausted(true);
-            pool.setMaxWaitMillis(-1);
-            runTestThreads(numThreads, iterations, delay, pool);
-            assertEquals(0, pool.getDestroyedByBorrowValidationCount());
-        }
-    }
-
-    /**
-     * POOL-376
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testNoInvalidateNPE() throws Exception {
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setTestOnCreate(true);
-        genericObjectPool.setMaxWaitMillis(-1);
-        final String obj = genericObjectPool.borrowObject();
-        // Make validation fail - this will cause create() to return null
-        simpleFactory.setValid(false);
-        // Create a take waiter
-        final WaitingTestThread<TestException> wtt = new WaitingTestThread<>(genericObjectPool, 200);
-        wtt.start();
-        // Give wtt time to start
-        Thread.sleep(200);
-        genericObjectPool.invalidateObject(obj);
-        // Now allow create to succeed so waiter can be served
-        simpleFactory.setValid(true);
-    }
-
-    public void testPreparePool() throws Exception {
-        genericObjectPool.setMinIdle(1);
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.preparePool();
-        assertEquals(1, genericObjectPool.getNumIdle());
-        final String obj = genericObjectPool.borrowObject();
-        genericObjectPool.preparePool();
-        assertEquals(0, genericObjectPool.getNumIdle());
-        genericObjectPool.setMinIdle(0);
-        genericObjectPool.returnObject(obj);
-        genericObjectPool.preparePool();
-        assertEquals(0, genericObjectPool.getNumIdle());
-    }
-
-    @Test/* maxWaitMillis x2 + padding */
-    @Timeout(value = 1200, unit = TimeUnit.MILLISECONDS)
-    public void testReturnBorrowObjectWithingMaxWaitMillis() throws Exception {
-        final long maxWaitMillis = 500;
-
-        try (final GenericObjectPool<String, InterruptedException> createSlowObjectFactoryPool = new GenericObjectPool<>(
-                createSlowObjectFactory(60000))) {
-            createSlowObjectFactoryPool.setMaxTotal(1);
-            createSlowObjectFactoryPool.setMaxWaitMillis(maxWaitMillis);
-
-            // thread1 tries creating a slow object to make pool full.
-            final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createSlowObjectFactoryPool, 0);
-            thread1.start();
-
-            // Wait for thread1's reaching to create().
-            Thread.sleep(100);
-
-            // another one tries borrowObject. It should return within maxWaitMillis.
-            assertThrows(NoSuchElementException.class, () -> createSlowObjectFactoryPool.borrowObject(maxWaitMillis),
-                    "borrowObject must fail due to timeout by maxWaitMillis");
-
-            assertTrue(thread1.isAlive());
-        }
-    }
-
-    /**
-     * This is the test case for POOL-263. It is disabled since it will always
-     * pass without artificial delay being injected into GOP.returnObject() and
-     * a way to this hasn't currently been found that doesn't involve
-     * polluting the GOP implementation. The artificial delay needs to be
-     * inserted just before the final call to isLifo() in the returnObject()
-     * method.
-     */
-    //@Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testReturnObject() throws Exception {
-
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setMaxIdle(-1);
-        final String active = genericObjectPool.borrowObject();
-
-        assertEquals(1, genericObjectPool.getNumActive());
-        assertEquals(0, genericObjectPool.getNumIdle());
-
-        final Thread t = new Thread(() -> genericObjectPool.close());
-        t.start();
-
-        genericObjectPool.returnObject(active);
-
-        // Wait for the close() thread to complete
-        while (t.isAlive()) {
-            Thread.sleep(50);
-        }
-
-        assertEquals(0, genericObjectPool.getNumIdle());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testSetConfig() throws Exception {
-        final GenericObjectPoolConfig<String> expected = new GenericObjectPoolConfig<>();
-        assertConfiguration(expected,genericObjectPool);
-        expected.setMaxTotal(2);
-        expected.setMaxIdle(3);
-        expected.setMaxWait(Duration.ofMillis(5));
-        expected.setMinEvictableIdleTime(Duration.ofMillis(7L));
-        expected.setNumTestsPerEvictionRun(9);
-        expected.setTestOnCreate(true);
-        expected.setTestOnBorrow(true);
-        expected.setTestOnReturn(true);
-        expected.setTestWhileIdle(true);
-        expected.setTimeBetweenEvictionRuns(Duration.ofMillis(11L));
-        expected.setBlockWhenExhausted(false);
-        genericObjectPool.setConfig(expected);
-        assertConfiguration(expected,genericObjectPool);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testSettersAndGetters() throws Exception {
-        {
-            // The object receives an Exception during its creation to prevent
-            // memory leaks. See BaseGenericObjectPool constructor for more details.
-            assertNotEquals("", genericObjectPool.getCreationStackTrace());
-        }
-        {
-            assertEquals(0, genericObjectPool.getBorrowedCount());
-        }
-        {
-            assertEquals(0, genericObjectPool.getReturnedCount());
-        }
-        {
-            assertEquals(0, genericObjectPool.getCreatedCount());
-        }
-        {
-            assertEquals(0, genericObjectPool.getDestroyedCount());
-        }
-        {
-            assertEquals(0, genericObjectPool.getDestroyedByEvictorCount());
-        }
-        {
-            assertEquals(0, genericObjectPool.getDestroyedByBorrowValidationCount());
-        }
-        {
-            assertEquals(0, genericObjectPool.getMeanActiveTimeMillis());
-        }
-        {
-            assertEquals(0, genericObjectPool.getMeanIdleTimeMillis());
-        }
-        {
-            assertEquals(0, genericObjectPool.getMeanBorrowWaitTimeMillis());
-        }
-        {
-            assertEquals(0, genericObjectPool.getMaxBorrowWaitTimeMillis());
-        }
-        {
-            assertEquals(0, genericObjectPool.getNumIdle());
-        }
-        {
-            genericObjectPool.setMaxTotal(123);
-            assertEquals(123,genericObjectPool.getMaxTotal());
-        }
-        {
-            genericObjectPool.setMaxIdle(12);
-            assertEquals(12,genericObjectPool.getMaxIdle());
-        }
-        {
-            genericObjectPool.setMaxWaitMillis(1234L);
-            assertEquals(1234L,genericObjectPool.getMaxWaitMillis());
-        }
-        {
-            genericObjectPool.setMinEvictableIdleTimeMillis(12345L);
-            assertEquals(12345L,genericObjectPool.getMinEvictableIdleDuration().toMillis());
-            assertEquals(12345L,genericObjectPool.getMinEvictableIdleTimeMillis());
-            assertEquals(12345L,genericObjectPool.getMinEvictableIdleTime().toMillis());
-        }
-        {
-            genericObjectPool.setNumTestsPerEvictionRun(11);
-            assertEquals(11,genericObjectPool.getNumTestsPerEvictionRun());
-        }
-        {
-            genericObjectPool.setTestOnBorrow(true);
-            assertTrue(genericObjectPool.getTestOnBorrow());
-            genericObjectPool.setTestOnBorrow(false);
-            assertFalse(genericObjectPool.getTestOnBorrow());
-        }
-        {
-            genericObjectPool.setTestOnReturn(true);
-            assertTrue(genericObjectPool.getTestOnReturn());
-            genericObjectPool.setTestOnReturn(false);
-            assertFalse(genericObjectPool.getTestOnReturn());
-        }
-        {
-            genericObjectPool.setTestWhileIdle(true);
-            assertTrue(genericObjectPool.getTestWhileIdle());
-            genericObjectPool.setTestWhileIdle(false);
-            assertFalse(genericObjectPool.getTestWhileIdle());
-        }
-        {
-            genericObjectPool.setTimeBetweenEvictionRunsMillis(11235L);
-            assertEquals(11235L,genericObjectPool.getDurationBetweenEvictionRuns().toMillis());
-            assertEquals(11235L,genericObjectPool.getTimeBetweenEvictionRunsMillis());
-            assertEquals(11235L,genericObjectPool.getTimeBetweenEvictionRuns().toMillis());
-        }
-        {
-            genericObjectPool.setSoftMinEvictableIdleTimeMillis(12135L);
-            assertEquals(12135L,genericObjectPool.getSoftMinEvictableIdleDuration().toMillis());
-            assertEquals(12135L,genericObjectPool.getSoftMinEvictableIdleTimeMillis());
-            assertEquals(12135L,genericObjectPool.getSoftMinEvictableIdleTime().toMillis());
-        }
-        {
-            genericObjectPool.setBlockWhenExhausted(true);
-            assertTrue(genericObjectPool.getBlockWhenExhausted());
-            genericObjectPool.setBlockWhenExhausted(false);
-            assertFalse(genericObjectPool.getBlockWhenExhausted());
-        }
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testStartAndStopEvictor() throws Exception {
-        // set up pool without evictor
-        genericObjectPool.setMaxIdle(6);
-        genericObjectPool.setMaxTotal(6);
-        genericObjectPool.setNumTestsPerEvictionRun(6);
-        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(100));
-
-        for (int j = 0; j < 2; j++) {
-            // populate the pool
-            {
-                final String[] active = new String[6];
-                for (int i = 0; i < 6; i++) {
-                    active[i] = genericObjectPool.borrowObject();
-                }
-                for (int i = 0; i < 6; i++) {
-                    genericObjectPool.returnObject(active[i]);
-                }
-            }
-
-            // note that it stays populated
-            assertEquals(6,genericObjectPool.getNumIdle(),"Should have 6 idle");
-
-            // start the evictor
-            genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(50));
-
-            // wait a second (well, .2 seconds)
-            Waiter.sleepQuietly(200L);
-
-            // assert that the evictor has cleared out the pool
-            assertEquals(0,genericObjectPool.getNumIdle(),"Should have 0 idle");
-
-            // stop the evictor
-            genericObjectPool.startEvictor(Duration.ZERO);
-        }
-    }
-
-    @Test
-    public void testSwallowedExceptionListener() {
-        genericObjectPool.setSwallowedExceptionListener(null); // must simply return
-        final List<Exception> swallowedExceptions = new ArrayList<>();
-        /*
-         * A simple listener, that will throw a OOM on 3rd exception.
-         */
-        final SwallowedExceptionListener listener = e -> {
-            if (swallowedExceptions.size() == 2) {
-                throw new OutOfMemoryError();
-            }
-            swallowedExceptions.add(e);
-        };
-        genericObjectPool.setSwallowedExceptionListener(listener);
-
-        final Exception e1 = new Exception();
-        final Exception e2 = new ArrayIndexOutOfBoundsException();
-
-        genericObjectPool.swallowException(e1);
-        genericObjectPool.swallowException(e2);
-
-        assertThrows(OutOfMemoryError.class, () -> genericObjectPool.swallowException(e1));
-
-        assertEquals(2, swallowedExceptions.size());
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testThreaded1() throws Exception {
-        genericObjectPool.setMaxTotal(15);
-        genericObjectPool.setMaxIdle(15);
-        genericObjectPool.setMaxWaitMillis(1000L);
-        runTestThreads(20, 100, 50, genericObjectPool);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testTimeoutNoLeak() throws Exception {
-        genericObjectPool.setMaxTotal(2);
-        genericObjectPool.setMaxWaitMillis(10);
-        genericObjectPool.setBlockWhenExhausted(true);
-        final String obj = genericObjectPool.borrowObject();
-        final String obj2 = genericObjectPool.borrowObject();
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-        genericObjectPool.returnObject(obj2);
-        genericObjectPool.returnObject(obj);
-
-        genericObjectPool.borrowObject();
-        genericObjectPool.borrowObject();
-    }
-
-    /**
-     * Tests POOL-361
-     */
-    @Test
-    public void testValidateOnCreate() throws Exception {
-        genericObjectPool.setTestOnCreate(true);
-        genericObjectPool.addObject();
-        assertEquals(1, simpleFactory.validateCounter);
-    }
-
-    /**
-     * Tests POOL-361
-     */
-    @Test
-    public void testValidateOnCreateFailure() throws Exception {
-        genericObjectPool.setTestOnCreate(true);
-        genericObjectPool.setTestOnBorrow(false);
-        genericObjectPool.setMaxTotal(2);
-        simpleFactory.setValid(false);
-        // Make sure failed validations do not leak capacity
-        genericObjectPool.addObject();
-        genericObjectPool.addObject();
-        assertEquals(0, genericObjectPool.getNumIdle());
-        assertEquals(0, genericObjectPool.getNumActive());
-        simpleFactory.setValid(true);
-        final String obj = genericObjectPool.borrowObject();
-        assertNotNull(obj);
-        genericObjectPool.addObject();
-        // Should have one idle, one out now
-        assertEquals(1, genericObjectPool.getNumIdle());
-        assertEquals(1, genericObjectPool.getNumActive());
-	}
-
-    /**
-     * Verify that threads waiting on a depleted pool get served when a returning object fails
-     * validation.
-     *
-     * JIRA: POOL-240
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    public void testValidationFailureOnReturnFreesCapacity() throws Exception {
-        final SimpleFactory factory = new SimpleFactory();
-        factory.setValid(false); // Validate will always fail
-        factory.setValidationEnabled(true);
-        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(factory)) {
-            pool.setMaxTotal(2);
-            pool.setMaxWaitMillis(1500);
-            pool.setTestOnReturn(true);
-            pool.setTestOnBorrow(false);
-            // Borrow an instance and hold if for 5 seconds
-            final WaitingTestThread<TestException> thread1 = new WaitingTestThread<>(pool, 5000);
-            thread1.start();
-            // Borrow another instance and return it after 500 ms (validation will fail)
-            final WaitingTestThread<TestException> thread2 = new WaitingTestThread<>(pool, 500);
-            thread2.start();
-            Thread.sleep(50);
-            // Try to borrow an object
-            final String obj = pool.borrowObject();
-            pool.returnObject(obj);
-        }
-    }
-
-    // POOL-276
-    @Test
-    public void testValidationOnCreateOnly() throws Exception {
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setTestOnCreate(true);
-        genericObjectPool.setTestOnBorrow(false);
-        genericObjectPool.setTestOnReturn(false);
-        genericObjectPool.setTestWhileIdle(false);
-
-        final String o1 = genericObjectPool.borrowObject();
-        assertEquals("0", o1);
-        final Timer t = new Timer();
-        t.schedule(
-                new TimerTask() {
-                    @Override
-                    public void run() {
-                        genericObjectPool.returnObject(o1);
-                    }
-                }, 3000);
-
-        final String o2 = genericObjectPool.borrowObject();
-        assertEquals("0", o2);
-
-        assertEquals(1, simpleFactory.validateCounter);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testWhenExhaustedBlock() throws Exception {
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setMaxWaitMillis(10L);
-        final String obj1 = genericObjectPool.borrowObject();
-        assertNotNull(obj1);
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-        genericObjectPool.returnObject(obj1);
-        genericObjectPool.close();
-    }
-
-    /**
-     * POOL-189
-     *
-     * @throws Exception May occur in some failure modes
-     */
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testWhenExhaustedBlockClosePool() throws Exception {
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setMaxWaitMillis(-1);
-        final Object obj1 = genericObjectPool.borrowObject();
-
-        // Make sure an object was obtained
-        assertNotNull(obj1);
-
-        // Create a separate thread to try and borrow another object
-        final WaitingTestThread<TestException> wtt = new WaitingTestThread<TestException>(genericObjectPool, 200);
-        wtt.start();
-        // Give wtt time to start
-        Thread.sleep(200);
-
-        // close the pool (Bug POOL-189)
-        genericObjectPool.close();
-
-        // Give interrupt time to take effect
-        Thread.sleep(200);
-
-        // Check thread was interrupted
-        assertTrue(wtt.thrown instanceof InterruptedException);
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testWhenExhaustedBlockInterrupt() throws Exception {
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setBlockWhenExhausted(true);
-        genericObjectPool.setMaxWaitMillis(-1);
-        final String obj1 = genericObjectPool.borrowObject();
-
-        // Make sure on object was obtained
-        assertNotNull(obj1);
-
-        // Create a separate thread to try and borrow another object
-        final WaitingTestThread<TestException> wtt = new WaitingTestThread<>(genericObjectPool, 200000);
-        wtt.start();
-        // Give wtt time to start
-        Thread.sleep(200);
-        wtt.interrupt();
-
-        // Give interrupt time to take effect
-        Thread.sleep(200);
-
-        // Check thread was interrupted
-        assertTrue(wtt.thrown instanceof InterruptedException);
-
-        // Return object to the pool
-        genericObjectPool.returnObject(obj1);
-
-        // Bug POOL-162 - check there is now an object in the pool
-        genericObjectPool.setMaxWaitMillis(10L);
-        String obj2 = null;
-        try {
-            obj2 = genericObjectPool.borrowObject();
-            assertNotNull(obj2);
-        } catch (final NoSuchElementException e) {
-            // Not expected
-            fail("NoSuchElementException not expected");
-        }
-        genericObjectPool.returnObject(obj2);
-        genericObjectPool.close();
-
-    }
-
-    @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testWhenExhaustedFail() throws Exception {
-        genericObjectPool.setMaxTotal(1);
-        genericObjectPool.setBlockWhenExhausted(false);
-        final String obj1 = genericObjectPool.borrowObject();
-        assertNotNull(obj1);
-        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
-        genericObjectPool.returnObject(obj1);
-        assertEquals(1, genericObjectPool.getNumIdle());
-        genericObjectPool.close();
-    }
-
-}
+/*
+ * 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.commons.pool2.impl;
+
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.management.ManagementFactory;
+import java.lang.ref.WeakReference;
+import java.nio.charset.UnsupportedCharsetException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.ObjectPool;
+import org.apache.commons.pool2.PoolUtils;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.PooledObjectFactory;
+import org.apache.commons.pool2.SwallowedExceptionListener;
+import org.apache.commons.pool2.TestBaseObjectPool;
+import org.apache.commons.pool2.TestException;
+import org.apache.commons.pool2.VisitTracker;
+import org.apache.commons.pool2.VisitTrackerFactory;
+import org.apache.commons.pool2.Waiter;
+import org.apache.commons.pool2.WaiterFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ */
+public class TestGenericObjectPool extends TestBaseObjectPool {
+
+    private class ConcurrentBorrowAndEvictThread extends Thread {
+        private final boolean borrow;
+        public String obj;
+
+        public ConcurrentBorrowAndEvictThread(final boolean borrow) {
+            this.borrow = borrow;
+        }
+
+        @Override
+        public void run() {
+            try {
+                if (borrow) {
+                    obj = genericObjectPool.borrowObject();
+                } else {
+                    genericObjectPool.evict();
+                }
+            } catch (final Exception e) {
+                // Ignore.
+            }
+        }
+    }
+
+    private static class CreateErrorFactory extends BasePooledObjectFactory<String, InterruptedException> {
+
+        private final Semaphore semaphore = new Semaphore(0);
+
+        @Override
+        public String create() throws InterruptedException {
+            semaphore.acquire();
+            throw new UnknownError("wiggle");
+        }
+
+        public boolean hasQueuedThreads() {
+            return semaphore.hasQueuedThreads();
+        }
+
+        public void release() {
+            semaphore.release();
+        }
+
+        @Override
+        public PooledObject<String> wrap(final String obj) {
+            return new DefaultPooledObject<>(obj);
+        }
+    }
+
+    private static class CreateFailFactory extends BasePooledObjectFactory<String, InterruptedException> {
+
+        private final Semaphore semaphore = new Semaphore(0);
+
+        @Override
+        public String create() throws InterruptedException {
+            semaphore.acquire();
+            throw new UnsupportedCharsetException("wibble");
+        }
+
+        public boolean hasQueuedThreads() {
+            return semaphore.hasQueuedThreads();
+        }
+
+        public void release() {
+            semaphore.release();
+        }
+
+        @Override
+        public PooledObject<String> wrap(final String obj) {
+            return new DefaultPooledObject<>(obj);
+        }
+    }
+
+    private static final class DummyFactory
+            extends BasePooledObjectFactory<Object, RuntimeException> {
+        @Override
+        public Object create() {
+            return null;
+        }
+        @Override
+        public PooledObject<Object> wrap(final Object value) {
+            return new DefaultPooledObject<>(value);
+        }
+    }
+
+    private static class EvictionThread<T, E extends Exception> extends Thread {
+
+        private final GenericObjectPool<T, E> pool;
+
+        public EvictionThread(final GenericObjectPool<T, E> pool) {
+            this.pool = pool;
+        }
+
+        @Override
+        public void run() {
+            try {
+                pool.evict();
+            } catch (final Exception e) {
+                // Ignore
+            }
+        }
+    }
+
+    /**
+     * Factory that creates HashSets.  Note that this means
+     *  0) All instances are initially equal (not discernible by equals)
+     *  1) Instances are mutable and mutation can cause change in identity / hashcode.
+     */
+    private static final class HashSetFactory
+            extends BasePooledObjectFactory<HashSet<String>, RuntimeException> {
+        @Override
+        public HashSet<String> create() {
+            return new HashSet<>();
+        }
+        @Override
+        public PooledObject<HashSet<String>> wrap(final HashSet<String> value) {
+            return new DefaultPooledObject<>(value);
+        }
+    }
+
+    /**
+     * Attempts to invalidate an object, swallowing IllegalStateException.
+     */
+    static class InvalidateThread implements Runnable {
+        private final String obj;
+        private final ObjectPool<String, ? extends Exception> pool;
+        private boolean done;
+        public InvalidateThread(final ObjectPool<String, ? extends Exception> pool, final String obj) {
+            this.obj = obj;
+            this.pool = pool;
+        }
+        public boolean complete() {
+            return done;
+        }
+        @Override
+        public void run() {
+            try {
+                pool.invalidateObject(obj);
+            } catch (final IllegalStateException ex) {
+                // Ignore
+            } catch (final Exception ex) {
+                fail("Unexpected exception " + ex.toString());
+            } finally {
+                done = true;
+            }
+        }
+    }
+
+    private static class InvalidFactory
+            extends BasePooledObjectFactory<Object, RuntimeException> {
+
+        @Override
+        public Object create() {
+            return new Object();
+        }
+        @Override
+        public boolean validateObject(final PooledObject<Object> obj) {
+            Waiter.sleepQuietly(1000);
+            return false;
+        }
+
+        @Override
+        public PooledObject<Object> wrap(final Object value) {
+            return new DefaultPooledObject<>(value);
+        }
+    }
+
+    public static class SimpleFactory implements PooledObjectFactory<String, TestException> {
+        int makeCounter;
+
+        int activationCounter;
+
+        int validateCounter;
+
+        int activeCount;
+
+        boolean evenValid = true;
+
+        boolean oddValid = true;
+
+        boolean exceptionOnPassivate;
+
+        boolean exceptionOnActivate;
+
+        boolean exceptionOnDestroy;
+
+        boolean exceptionOnValidate;
+
+        boolean enableValidation = true;
+
+        long destroyLatency;
+
+        long makeLatency;
+
+        long validateLatency;
+
+        int maxTotal = Integer.MAX_VALUE;
+
+        public SimpleFactory() {
+            this(true);
+        }
+
+        public SimpleFactory(final boolean valid) {
+            this(valid,valid);
+        }
+
+        public SimpleFactory(final boolean evalid, final boolean ovalid) {
+            evenValid = evalid;
+            oddValid = ovalid;
+        }
+
+        @Override
+        public void activateObject(final PooledObject<String> obj) throws TestException {
+            final boolean hurl;
+            final boolean evenTest;
+            final boolean oddTest;
+            final int counter;
+            synchronized(this) {
+                hurl = exceptionOnActivate;
+                evenTest = evenValid;
+                oddTest = oddValid;
+                counter = activationCounter++;
+            }
+            if (hurl && !(counter%2 == 0 ? evenTest : oddTest)) {
+                throw new TestException();
+            }
+        }
+
+        @Override
+        public void destroyObject(final PooledObject<String> obj) throws TestException {
+            final long waitLatency;
+            final boolean hurl;
+            synchronized(this) {
+                waitLatency = destroyLatency;
+                hurl = exceptionOnDestroy;
+            }
+            if (waitLatency > 0) {
+                doWait(waitLatency);
+            }
+            synchronized(this) {
+                activeCount--;
+            }
+            if (hurl) {
+                throw new TestException();
+            }
+        }
+
+        private void doWait(final long latency) {
+            Waiter.sleepQuietly(latency);
+        }
+
+        public synchronized int getMakeCounter() {
+            return makeCounter;
+        }
+
+        public synchronized boolean isThrowExceptionOnActivate() {
+            return exceptionOnActivate;
+        }
+
+        public synchronized boolean isValidationEnabled() {
+            return enableValidation;
+        }
+
+        @Override
+        public PooledObject<String> makeObject() {
+            final long waitLatency;
+            synchronized(this) {
+                activeCount++;
+                if (activeCount > maxTotal) {
+                    throw new IllegalStateException(
+                        "Too many active instances: " + activeCount);
+                }
+                waitLatency = makeLatency;
+            }
+            if (waitLatency > 0) {
+                doWait(waitLatency);
+            }
+            final int counter;
+            synchronized(this) {
+                counter = makeCounter++;
+            }
+            return new DefaultPooledObject<>(String.valueOf(counter));
+        }
+
+        @Override
+        public void passivateObject(final PooledObject<String> obj) throws TestException {
+            final boolean hurl;
+            synchronized(this) {
+                hurl = exceptionOnPassivate;
+            }
+            if (hurl) {
+                throw new TestException();
+            }
+        }
+
+        public synchronized void setDestroyLatency(final long destroyLatency) {
+            this.destroyLatency = destroyLatency;
+        }
+
+        public synchronized void setEvenValid(final boolean valid) {
+            evenValid = valid;
+        }
+
+        public synchronized void setMakeLatency(final long makeLatency) {
+            this.makeLatency = makeLatency;
+        }
+
+        public synchronized void setMaxTotal(final int maxTotal) {
+            this.maxTotal = maxTotal;
+        }
+
+        public synchronized void setOddValid(final boolean valid) {
+            oddValid = valid;
+        }
+
+        public synchronized void setThrowExceptionOnActivate(final boolean b) {
+            exceptionOnActivate = b;
+        }
+
+        public synchronized void setThrowExceptionOnDestroy(final boolean b) {
+            exceptionOnDestroy = b;
+        }
+
+        public synchronized void setThrowExceptionOnPassivate(final boolean bool) {
+            exceptionOnPassivate = bool;
+        }
+
+        public synchronized void setThrowExceptionOnValidate(final boolean bool) {
+            exceptionOnValidate = bool;
+        }
+
+        public synchronized void setValid(final boolean valid) {
+            setEvenValid(valid);
+            setOddValid(valid);
+        }
+
+        public synchronized void setValidateLatency(final long validateLatency) {
+            this.validateLatency = validateLatency;
+        }
+
+        public synchronized void setValidationEnabled(final boolean b) {
+            enableValidation = b;
+        }
+
+        @Override
+        public boolean validateObject(final PooledObject<String> obj) {
+            final boolean validate;
+            final boolean throwException;
+            final boolean evenTest;
+            final boolean oddTest;
+            final long waitLatency;
+            final int counter;
+            synchronized(this) {
+                validate = enableValidation;
+                throwException = exceptionOnValidate;
+                evenTest = evenValid;
+                oddTest = oddValid;
+                counter = validateCounter++;
+                waitLatency = validateLatency;
+            }
+            if (waitLatency > 0) {
+                doWait(waitLatency);
+            }
+            if (throwException) {
+                throw new RuntimeException("validation failed");
+            }
+            if (validate) {
+                return counter%2 == 0 ? evenTest : oddTest;
+            }
+            return true;
+        }
+    }
+
+    public static class TestEvictionPolicy<T> implements EvictionPolicy<T> {
+
+        private final AtomicInteger callCount = new AtomicInteger(0);
+
+        @Override
+        public boolean evict(final EvictionConfig config, final PooledObject<T> underTest,
+                final int idleCount) {
+            return callCount.incrementAndGet() > 1500;
+        }
+    }
+
+    static class TestThread<T, E extends Exception> implements Runnable {
+
+        /** source of random delay times */
+        private final java.util.Random random;
+
+        /** pool to borrow from */
+        private final ObjectPool<T, E> pool;
+
+        /** number of borrow attempts */
+        private final int iter;
+
+        /** delay before each borrow attempt */
+        private final int startDelay;
+
+        /** time to hold each borrowed object before returning it */
+        private final int holdTime;
+
+        /** whether or not start and hold time are randomly generated */
+        private final boolean randomDelay;
+
+        /** object expected to be borrowed (fail otherwise) */
+        private final Object expectedObject;
+
+        private volatile boolean complete;
+        private volatile boolean failed;
+        private volatile Throwable error;
+
+        public TestThread(final ObjectPool<T, E> pool) {
+            this(pool, 100, 50, true, null);
+        }
+
+        public TestThread(final ObjectPool<T, E> pool, final int iter) {
+            this(pool, iter, 50, true, null);
+        }
+
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay) {
+            this(pool, iter, delay, true, null);
+        }
+
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay,
+                final boolean randomDelay) {
+            this(pool, iter, delay, randomDelay, null);
+        }
+
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay,
+                final boolean randomDelay, final Object obj) {
+            this(pool, iter, delay, delay, randomDelay, obj);
+        }
+
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int startDelay,
+            final int holdTime, final boolean randomDelay, final Object obj) {
+       this.pool = pool;
+       this.iter = iter;
+       this.startDelay = startDelay;
+       this.holdTime = holdTime;
+       this.randomDelay = randomDelay;
+       this.random = this.randomDelay ? new Random() : null;
+       this.expectedObject = obj;
+    }
+
+        public boolean complete() {
+            return complete;
+        }
+
+        public boolean failed() {
+            return failed;
+        }
+
+        @Override
+        public void run() {
+            for (int i = 0; i < iter; i++) {
+                final long actualStartDelay = randomDelay ? (long) random.nextInt(startDelay) : startDelay;
+                final long actualHoldTime = randomDelay ? (long) random.nextInt(holdTime) : holdTime;
+                Waiter.sleepQuietly(actualStartDelay);
+                T obj = null;
+                try {
+                    obj = pool.borrowObject();
+                } catch (final Exception e) {
+                    error = e;
+                    failed = true;
+                    complete = true;
+                    break;
+                }
+
+                if (expectedObject != null && !expectedObject.equals(obj)) {
+                    error = new Throwable("Expected: " + expectedObject + " found: " + obj);
+                    failed = true;
+                    complete = true;
+                    break;
+                }
+
+                Waiter.sleepQuietly(actualHoldTime);
+                try {
+                    pool.returnObject(obj);
+                } catch (final Exception e) {
+                    error = e;
+                    failed = true;
+                    complete = true;
+                    break;
+                }
+            }
+            complete = true;
+        }
+    }
+
+    /*
+     * Very simple test thread that just tries to borrow an object from
+     * the provided pool returns it after a wait
+     */
+    static class WaitingTestThread<E extends Exception> extends Thread {
+        private final GenericObjectPool<String, E> pool;
+        private final long pause;
+        private Throwable thrown;
+
+        private long preBorrowMillis; // just before borrow
+        private long postBorrowMillis; //  borrow returned
+        private long postReturnMillis; // after object was returned
+        private long endedMillis;
+        private String objectId;
+
+        public WaitingTestThread(final GenericObjectPool<String, E> pool, final long pause) {
+            this.pool = pool;
+            this.pause = pause;
+            this.thrown = null;
+        }
+
+        @Override
+        public void run() {
+            try {
+                preBorrowMillis = System.currentTimeMillis();
+                final String obj = pool.borrowObject();
+                objectId = obj;
+                postBorrowMillis = System.currentTimeMillis();
+                Thread.sleep(pause);
+                pool.returnObject(obj);
+                postReturnMillis = System.currentTimeMillis();
+            } catch (final Throwable e) {
+                thrown = e;
+            } finally{
+                endedMillis = System.currentTimeMillis();
+            }
+        }
+    }
+
+    private static final boolean DISPLAY_THREAD_DETAILS=
+    Boolean.getBoolean("TestGenericObjectPool.display.thread.details");
+    // To pass this to a Maven test, use:
+    // mvn test -DargLine="-DTestGenericObjectPool.display.thread.details=true"
+    // @see https://issues.apache.org/jira/browse/SUREFIRE-121
+
+    protected GenericObjectPool<String, TestException> genericObjectPool;
+
+    private SimpleFactory simpleFactory;
+
+    @SuppressWarnings("deprecation")
+    private void assertConfiguration(final GenericObjectPoolConfig<?> expected, final GenericObjectPool<?, ?> actual) {
+        assertEquals(Boolean.valueOf(expected.getTestOnCreate()), Boolean.valueOf(actual.getTestOnCreate()),
+                "testOnCreate");
+        assertEquals(Boolean.valueOf(expected.getTestOnBorrow()), Boolean.valueOf(actual.getTestOnBorrow()),
+                "testOnBorrow");
+        assertEquals(Boolean.valueOf(expected.getTestOnReturn()), Boolean.valueOf(actual.getTestOnReturn()),
+                "testOnReturn");
+        assertEquals(Boolean.valueOf(expected.getTestWhileIdle()), Boolean.valueOf(actual.getTestWhileIdle()),
+                "testWhileIdle");
+        assertEquals(Boolean.valueOf(expected.getBlockWhenExhausted()), Boolean.valueOf(actual.getBlockWhenExhausted()),
+                "whenExhaustedAction");
+        assertEquals(expected.getMaxTotal(), actual.getMaxTotal(), "maxTotal");
+        assertEquals(expected.getMaxIdle(), actual.getMaxIdle(), "maxIdle");
+        assertEquals(expected.getMaxWaitMillis(), actual.getMaxWaitMillis(), "maxWaitDuration");
+        assertEquals(expected.getMaxWaitDuration(), actual.getMaxWaitDuration(), "maxWaitDuration");
+        assertEquals(expected.getMinEvictableIdleTimeMillis(), actual.getMinEvictableIdleTimeMillis(),
+                "minEvictableIdleTimeMillis");
+        assertEquals(expected.getMinEvictableIdleTime(), actual.getMinEvictableIdleTime(),
+                "minEvictableIdleTime");
+        assertEquals(expected.getMinEvictableIdleDuration(), actual.getMinEvictableIdleDuration(),
+                "minEvictableIdleDuration");
+        assertEquals(expected.getNumTestsPerEvictionRun(), actual.getNumTestsPerEvictionRun(),
+                "numTestsPerEvictionRun");
+        assertEquals(expected.getEvictorShutdownTimeoutDuration(), actual.getEvictorShutdownTimeoutDuration(),
+                "evictorShutdownTimeoutDuration");
+        assertEquals(expected.getEvictorShutdownTimeoutMillis(), actual.getEvictorShutdownTimeoutMillis(),
+                "evictorShutdownTimeoutMillis");
+        assertEquals(expected.getEvictorShutdownTimeout(), actual.getEvictorShutdownTimeout(),
+                "evictorShutdownTimeout");
+        assertEquals(expected.getTimeBetweenEvictionRunsMillis(), actual.getTimeBetweenEvictionRunsMillis(),
+                "timeBetweenEvictionRunsMillis");
+        assertEquals(expected.getDurationBetweenEvictionRuns(), actual.getTimeBetweenEvictionRuns(),
+                "timeBetweenEvictionRuns");
+        assertEquals(expected.getTimeBetweenEvictionRuns(), actual.getTimeBetweenEvictionRuns(),
+                "timeBetweenEvictionRuns");
+    }
+
+    private void checkEvict(final boolean lifo) throws Exception {
+        // yea this is hairy but it tests all the code paths in GOP.evict()
+        genericObjectPool.setSoftMinEvictableIdle(Duration.ofMillis(10));
+        genericObjectPool.setSoftMinEvictableIdleTime(Duration.ofMillis(10));
+        genericObjectPool.setMinIdle(2);
+        genericObjectPool.setTestWhileIdle(true);
+        genericObjectPool.setLifo(lifo);
+        genericObjectPool.addObjects(5);
+        genericObjectPool.evict();
+        simpleFactory.setEvenValid(false);
+        simpleFactory.setOddValid(false);
+        simpleFactory.setThrowExceptionOnActivate(true);
+        genericObjectPool.evict();
+        genericObjectPool.addObjects(5);
+        simpleFactory.setThrowExceptionOnActivate(false);
+        simpleFactory.setThrowExceptionOnPassivate(true);
+        genericObjectPool.evict();
+        simpleFactory.setThrowExceptionOnPassivate(false);
+        simpleFactory.setEvenValid(true);
+        simpleFactory.setOddValid(true);
+        Thread.sleep(125);
+        genericObjectPool.evict();
+        assertEquals(2, genericObjectPool.getNumIdle());
+    }
+
+    private void checkEvictionOrder(final boolean lifo) throws Exception {
+        checkEvictionOrderPart1(lifo);
+        tearDown();
+        setUp();
+        checkEvictionOrderPart2(lifo);
+    }
+
+    private void checkEvictionOrderPart1(final boolean lifo) throws Exception {
+        genericObjectPool.setNumTestsPerEvictionRun(2);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(100));
+        genericObjectPool.setLifo(lifo);
+        for (int i = 0; i < 5; i++) {
+            genericObjectPool.addObject();
+            Thread.sleep(100);
+        }
+        // Order, oldest to youngest, is "0", "1", ...,"4"
+        genericObjectPool.evict(); // Should evict "0" and "1"
+        final Object obj = genericObjectPool.borrowObject();
+        assertNotEquals("0", obj, "oldest not evicted");
+        assertNotEquals("1", obj, "second oldest not evicted");
+        // 2 should be next out for FIFO, 4 for LIFO
+        assertEquals(lifo ? "4" : "2" , obj,"Wrong instance returned");
+    }
+
+    private void checkEvictionOrderPart2(final boolean lifo) throws Exception {
+        // Two eviction runs in sequence
+        genericObjectPool.setNumTestsPerEvictionRun(2);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(100));
+        genericObjectPool.setLifo(lifo);
+        for (int i = 0; i < 5; i++) {
+            genericObjectPool.addObject();
+            Thread.sleep(100);
+        }
+        genericObjectPool.evict(); // Should evict "0" and "1"
+        genericObjectPool.evict(); // Should evict "2" and "3"
+        final Object obj = genericObjectPool.borrowObject();
+        assertEquals("4", obj,"Wrong instance remaining in pool");
+    }
+
+    private void checkEvictorVisiting(final boolean lifo) throws Exception {
+        VisitTracker<Object> obj;
+        VisitTrackerFactory<Object> trackerFactory = new VisitTrackerFactory<>();
+        try (GenericObjectPool<VisitTracker<Object>,RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
+            trackerPool.setNumTestsPerEvictionRun(2);
+            trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
+            trackerPool.setTestWhileIdle(true);
+            trackerPool.setLifo(lifo);
+            trackerPool.setTestOnReturn(false);
+            trackerPool.setTestOnBorrow(false);
+            for (int i = 0; i < 8; i++) {
+                trackerPool.addObject();
+            }
+            trackerPool.evict(); // Visit oldest 2 - 0 and 1
+            obj = trackerPool.borrowObject();
+            trackerPool.returnObject(obj);
+            obj = trackerPool.borrowObject();
+            trackerPool.returnObject(obj);
+            // borrow, return, borrow, return
+            // FIFO will move 0 and 1 to end
+            // LIFO, 7 out, then in, then out, then in
+            trackerPool.evict(); // Should visit 2 and 3 in either case
+            for (int i = 0; i < 8; i++) {
+                final VisitTracker<Object> tracker = trackerPool.borrowObject();
+                if (tracker.getId() >= 4) {
+                    assertEquals( 0, tracker.getValidateCount(),"Unexpected instance visited " + tracker.getId());
+                } else {
+                    assertEquals( 1, tracker.getValidateCount(),
+                            "Instance " + tracker.getId() + " visited wrong number of times.");
+                }
+            }
+        }
+
+        trackerFactory = new VisitTrackerFactory<>();
+        try (GenericObjectPool<VisitTracker<Object>, RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
+            trackerPool.setNumTestsPerEvictionRun(3);
+            trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
+            trackerPool.setTestWhileIdle(true);
+            trackerPool.setLifo(lifo);
+            trackerPool.setTestOnReturn(false);
+            trackerPool.setTestOnBorrow(false);
+            for (int i = 0; i < 8; i++) {
+                trackerPool.addObject();
+            }
+            trackerPool.evict(); // 0, 1, 2
+            trackerPool.evict(); // 3, 4, 5
+            obj = trackerPool.borrowObject();
+            trackerPool.returnObject(obj);
+            obj = trackerPool.borrowObject();
+            trackerPool.returnObject(obj);
+            obj = trackerPool.borrowObject();
+            trackerPool.returnObject(obj);
+            // borrow, return, borrow, return
+            // FIFO 3,4,5,6,7,0,1,2
+            // LIFO 7,6,5,4,3,2,1,0
+            // In either case, pointer should be at 6
+            trackerPool.evict();
+            // Should hit 6,7,0 - 0 for second time
+            for (int i = 0; i < 8; i++) {
+                final VisitTracker<Object> tracker = trackerPool.borrowObject();
+                if (tracker.getId() != 0) {
+                    assertEquals( 1, tracker.getValidateCount(),
+                            "Instance " + tracker.getId() + " visited wrong number of times.");
+                } else {
+                    assertEquals( 2, tracker.getValidateCount(),
+                            "Instance " + tracker.getId() + " visited wrong number of times.");
+                }
+            }
+        }
+
+        // Randomly generate a pools with random numTests
+        // and make sure evictor cycles through elements appropriately
+        final int[] smallPrimes = { 2, 3, 5, 7 };
+        final Random random = new Random();
+        random.setSeed(System.currentTimeMillis());
+        for (int i = 0; i < 4; i++) {
+            for (int j = 0; j < 5; j++) {
+                try (GenericObjectPool<VisitTracker<Object>, RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
+                    trackerPool.setNumTestsPerEvictionRun(smallPrimes[i]);
+                    trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
+                    trackerPool.setTestWhileIdle(true);
+                    trackerPool.setLifo(lifo);
+                    trackerPool.setTestOnReturn(false);
+                    trackerPool.setTestOnBorrow(false);
+                    trackerPool.setMaxIdle(-1);
+                    final int instanceCount = 10 + random.nextInt(20);
+                    trackerPool.setMaxTotal(instanceCount);
+                    for (int k = 0; k < instanceCount; k++) {
+                        trackerPool.addObject();
+                    }
+
+                    // Execute a random number of evictor runs
+                    final int runs = 10 + random.nextInt(50);
+                    for (int k = 0; k < runs; k++) {
+                        trackerPool.evict();
+                    }
+
+                    // Number of times evictor should have cycled through the pool
+                    final int cycleCount = runs * trackerPool.getNumTestsPerEvictionRun() / instanceCount;
+
+                    // Look at elements and make sure they are visited cycleCount
+                    // or cycleCount + 1 times
+                    VisitTracker<Object> tracker = null;
+                    int visitCount = 0;
+                    for (int k = 0; k < instanceCount; k++) {
+                        tracker = trackerPool.borrowObject();
+                        assertTrue(trackerPool.getNumActive() <= trackerPool.getMaxTotal());
+                        visitCount = tracker.getValidateCount();
+                        assertTrue(visitCount >= cycleCount && visitCount <= cycleCount + 1);
+                    }
+                }
+            }
+        }
+    }
+
+    private BasePooledObjectFactory<String, RuntimeException> createDefaultPooledObjectFactory() {
+        return new BasePooledObjectFactory<String, RuntimeException>() {
+            @Override
+            public String create() {
+                // fake
+                return null;
+            }
+
+            @Override
+            public PooledObject<String> wrap(final String obj) {
+                // fake
+                return new DefaultPooledObject<>(obj);
+            }
+        };
+    }
+
+    private BasePooledObjectFactory<String, RuntimeException> createNullPooledObjectFactory() {
+        return new BasePooledObjectFactory<String, RuntimeException>() {
+            @Override
+            public String create() {
+                // fake
+                return null;
+            }
+
+            @Override
+            public PooledObject<String> wrap(final String obj) {
+                // fake
+                return null;
+            }
+        };
+    }
+
+    private BasePooledObjectFactory<String, InterruptedException> createSlowObjectFactory(final long elapsedTimeMillis) {
+        return new BasePooledObjectFactory<String, InterruptedException>() {
+            @Override
+            public String create() throws InterruptedException {
+                Thread.sleep(elapsedTimeMillis);
+                return "created";
+            }
+
+            @Override
+            public PooledObject<String> wrap(final String obj) {
+                // fake
+                return new DefaultPooledObject<>(obj);
+            }
+        };
+    }
+
+    @Override
+    protected Object getNthObject(final int n) {
+        return String.valueOf(n);
+    }
+
+    @Override
+    protected boolean isFifo() {
+        return false;
+    }
+
+    @Override
+    protected boolean isLifo() {
+        return true;
+    }
+
+    @Override
+    protected ObjectPool<String, TestException> makeEmptyPool(final int minCap) {
+        final GenericObjectPool<String, TestException> mtPool = new GenericObjectPool<>(new SimpleFactory());
+        mtPool.setMaxTotal(minCap);
+       mtPool.setMaxIdle(minCap);
+       return mtPool;
+    }
+
+    @Override
+    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> fac) {
+        return new GenericObjectPool<>(fac);
+    }
+
+    /**
+     * Kicks off <numThreads> test threads, each of which will go through
+     * <iterations> borrow-return cycles with random delay times <= delay
+     * in between.
+     */
+    private <T, E extends Exception> void runTestThreads(final int numThreads, final int iterations, final int delay, final GenericObjectPool<T, E> testPool) {
+        final TestThread<T, E>[] threads = new TestThread[numThreads];
+        for (int i = 0; i < numThreads; i++) {
+            threads[i] = new TestThread<>(testPool, iterations, delay);
+            final Thread t = new Thread(threads[i]);
+            t.start();
+        }
+        for (int i = 0; i < numThreads; i++) {
+            while (!threads[i].complete()) {
+                Waiter.sleepQuietly(500L);
+            }
+            if (threads[i].failed()) {
+                fail("Thread " + i + " failed: " + threads[i].error.toString());
+            }
+        }
+    }
+
+    @BeforeEach
+    public void setUp() {
+        simpleFactory = new SimpleFactory();
+        genericObjectPool = new GenericObjectPool<>(simpleFactory);
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        final ObjectName jmxName = genericObjectPool.getJmxName();
+        final String poolName = Objects.toString(jmxName, null);
+
+        genericObjectPool.clear();
+        genericObjectPool.close();
+        genericObjectPool = null;
+        simpleFactory = null;
+
+        final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+        final Set<ObjectName> result = mbs.queryNames(new ObjectName("org.apache.commoms.pool2:type=GenericObjectPool,*"), null);
+        // There should be no registered pools at this point
+        final int registeredPoolCount = result.size();
+        final StringBuilder msg = new StringBuilder("Current pool is: ");
+        msg.append(poolName);
+        msg.append("  Still open pools are: ");
+        for (final ObjectName name : result) {
+            // Clean these up ready for the next test
+            msg.append(name.toString());
+            msg.append(" created via\n");
+            msg.append(mbs.getAttribute(name, "CreationStackTrace"));
+            msg.append('\n');
+            mbs.unregisterMBean(name);
+        }
+        assertEquals(0, registeredPoolCount, msg.toString());
+
+        // Make sure that EvictionTimer executor is shut down.
+        Thread.yield();
+        if (EvictionTimer.getExecutor() != null) {
+            Thread.sleep(1000);
+        }
+        assertNull(EvictionTimer.getExecutor(), "EvictionTimer.getExecutor()");
+    }
+
+    /**
+     * Check that a pool that starts an evictor, but is never closed does not leave EvictionTimer executor running. Confirmation check is in
+     * {@link #tearDown()}.
+     *
+     * @throws TestException Custom exception
+     * @throws InterruptedException if any thread has interrupted the current thread. The <em>interrupted status</em> of the current thread is cleared when this
+     *         exception is thrown.
+     */
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testAbandonedPool() throws TestException, InterruptedException {
+        final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
+        config.setJmxEnabled(false);
+        GenericObjectPool<String, TestException> abandoned = new GenericObjectPool<>(simpleFactory, config);
+        abandoned.setTimeBetweenEvictionRuns(Duration.ofMillis(100)); // Starts evictor
+        assertEquals(abandoned.getRemoveAbandonedTimeout(), abandoned.getRemoveAbandonedTimeoutDuration().getSeconds());
+
+        // This is ugly, but forces GC to hit the pool
+        final WeakReference<GenericObjectPool<String, TestException>> ref = new WeakReference<>(abandoned);
+        abandoned = null;
+        while (ref.get() != null) {
+            System.gc();
+            Thread.sleep(100);
+        }
+    }
+
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testAddObject() throws Exception {
+        assertEquals( 0, genericObjectPool.getNumIdle(),"should be zero idle");
+        genericObjectPool.addObject();
+        assertEquals( 1, genericObjectPool.getNumIdle(),"should be one idle");
+        assertEquals( 0, genericObjectPool.getNumActive(),"should be zero active");
+        final String obj = genericObjectPool.borrowObject();
+        assertEquals( 0, genericObjectPool.getNumIdle(),"should be zero idle");
+        assertEquals( 1, genericObjectPool.getNumActive(),"should be one active");
+        genericObjectPool.returnObject(obj);
+        assertEquals( 1, genericObjectPool.getNumIdle(),"should be one idle");
+        assertEquals( 0, genericObjectPool.getNumActive(),"should be zero active");
+    }
+
+    @Test
+    public void testAppendStats() {
+        assertFalse(genericObjectPool.getMessageStatistics());
+        assertEquals("foo", genericObjectPool.appendStats("foo"));
+        try (final GenericObjectPool<?, TestException> pool = new GenericObjectPool<>(new SimpleFactory())) {
+            pool.setMessagesStatistics(true);
+            assertNotEquals("foo", pool.appendStats("foo"));
+            pool.setMessagesStatistics(false);
+            assertEquals("foo", pool.appendStats("foo"));
+        }
+    }
+
+    /*
+     * Note: This test relies on timing for correct execution. There *should* be
+     * enough margin for this to work correctly on most (all?) systems but be
+     * aware of this if you see a failure of this test.
+     */
+    @SuppressWarnings({
+        "rawtypes", "unchecked"
+    })
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testBorrowObjectFairness() throws Exception {
+
+        final int numThreads = 40;
+        final int maxTotal = 40;
+
+        final GenericObjectPoolConfig config = new GenericObjectPoolConfig();
+        config.setMaxTotal(maxTotal);
+        config.setMaxIdle(maxTotal);
+        config.setFairness(true);
+        config.setLifo(false);
+
+        genericObjectPool = new GenericObjectPool(simpleFactory, config);
+
+        // Exhaust the pool
+        final String[] objects = new String[maxTotal];
+        for (int i = 0; i < maxTotal; i++) {
+            objects[i] = genericObjectPool.borrowObject();
+        }
+
+        // Start and park threads waiting to borrow objects
+        final TestThread[] threads = new TestThread[numThreads];
+        for(int i=0;i<numThreads;i++) {
+            threads[i] = new TestThread(genericObjectPool, 1, 0, 2000, false, String.valueOf(i % maxTotal));
+            final Thread t = new Thread(threads[i]);
+            t.start();
+            // Short delay to ensure threads start in correct order
+            try {
+                Thread.sleep(10);
+            } catch (final InterruptedException e) {
+                fail(e.toString());
+            }
+        }
+
+        // Return objects, other threads should get served in order
+        for (int i = 0; i < maxTotal; i++) {
+            genericObjectPool.returnObject(objects[i]);
+        }
+
+        // Wait for threads to finish
+        for (int i = 0; i < numThreads; i++) {
+            while (!threads[i].complete()) {
+                Waiter.sleepQuietly(500L);
+            }
+            if (threads[i].failed()) {
+                fail("Thread " + i + " failed: " + threads[i].error.toString());
+            }
+        }
+    }
+
+    @Test
+    public void testBorrowTimings() throws Exception {
+        // Borrow
+        final String object = genericObjectPool.borrowObject();
+        final PooledObject<String> po = genericObjectPool.getPooledObject(object);
+        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
+        // In the initial state, the active duration is the time between "now" and the creation time.
+        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
+        // But... this PO might have already been used in other tests in this class.
+
+        final Instant lastBorrowInstant1 = po.getLastBorrowInstant();
+        final Instant lastReturnInstant1 = po.getLastReturnInstant();
+        final Instant lastUsedInstant1 = po.getLastUsedInstant();
+
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastBorrowInstant1));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastReturnInstant1));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastUsedInstant1));
+        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastBorrowInstant1.toEpochMilli()));
+        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastReturnInstant1.toEpochMilli()));
+        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastUsedInstant1.toEpochMilli()));
+
+        // Sleep MUST be "long enough" to detect that more than 0 milliseconds have elapsed.
+        // Need an API in Java 8 to get the clock granularity.
+        Thread.sleep(200);
+
+        assertFalse(po.getActiveDuration().isNegative());
+        assertFalse(po.getActiveDuration().isZero());
+        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
+        assertThat(1L, lessThanOrEqualTo(2L)); // sanity check
+        assertThat(Duration.ZERO, lessThanOrEqualTo(Duration.ZERO.plusNanos(1))); // sanity check
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleDuration()));
+        // Deprecated
+        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
+        //
+        // TODO How to compare ID with AD since other tests may have touched the PO?
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleTime()));
+        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getIdleTimeMillis()));
+        //
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastBorrowInstant()));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastReturnInstant()));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastUsedInstant()));
+
+        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
+        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
+        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
+
+        genericObjectPool.returnObject(object);
+
+        assertFalse(po.getActiveDuration().isNegative());
+        assertFalse(po.getActiveDuration().isZero());
+        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
+
+        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
+        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
+        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
+    }
+
+    /**
+     * On first borrow, first object fails validation, second object is OK.
+     * Subsequent borrows are OK. This was POOL-152.
+     * @throws Exception
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testBrokenFactoryShouldNotBlockPool() throws Exception {
+        final int maxTotal = 1;
+
+        simpleFactory.setMaxTotal(maxTotal);
+        genericObjectPool.setMaxTotal(maxTotal);
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setTestOnBorrow(true);
+
+        // First borrow object will need to create a new object which will fail
+        // validation.
+        String obj = null;
+        Exception ex = null;
+        simpleFactory.setValid(false);
+        try {
+            obj = genericObjectPool.borrowObject();
+        } catch (final Exception e) {
+            ex = e;
+        }
+        // Failure expected
+        assertNotNull(ex);
+        assertTrue(ex instanceof NoSuchElementException);
+        assertNull(obj);
+
+        // Configure factory to create valid objects so subsequent borrows work
+        simpleFactory.setValid(true);
+
+        // Subsequent borrows should be OK
+        obj = genericObjectPool.borrowObject();
+        assertNotNull(obj);
+        genericObjectPool.returnObject(obj);
+    }
+
+    // POOL-259
+    @Test
+    public void testClientWaitStats() throws TestException {
+        final SimpleFactory factory = new SimpleFactory();
+        // Give makeObject a little latency
+        factory.setMakeLatency(200);
+        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(factory, new GenericObjectPoolConfig<>())) {
+            final String s = pool.borrowObject();
+            // First borrow waits on create, so wait time should be at least 200 ms
+            // Allow 100ms error in clock times
+            assertTrue(pool.getMaxBorrowWaitTimeMillis() >= 100);
+            assertTrue(pool.getMeanBorrowWaitTimeMillis() >= 100);
+            pool.returnObject(s);
+            pool.borrowObject();
+            // Second borrow does not have to wait on create, average should be about 100
+            assertTrue(pool.getMaxBorrowWaitTimeMillis() > 100);
+            assertTrue(pool.getMeanBorrowWaitTimeMillis() < 200);
+            assertTrue(pool.getMeanBorrowWaitTimeMillis() > 20);
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testCloseMultiplePools1() {
+        try (final GenericObjectPool<String, TestException> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
+            genericObjectPool.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
+            genericObjectPool2.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
+        }
+        genericObjectPool.close();
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testCloseMultiplePools2() throws Exception {
+        try (final GenericObjectPool<String, TestException> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
+            // Ensure eviction takes a long time, during which time EvictionTimer.executor's queue is empty
+            simpleFactory.setDestroyLatency(1000L);
+            // Ensure there is an object to evict, so that above latency takes effect
+            genericObjectPool.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
+            genericObjectPool2.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
+            genericObjectPool.setMinEvictableIdleTime(TestConstants.ONE_MILLISECOND_DURATION);
+            genericObjectPool2.setMinEvictableIdleTime(TestConstants.ONE_MILLISECOND_DURATION);
+            genericObjectPool.addObject();
+            genericObjectPool2.addObject();
+            // Close both pools
+        }
+        genericObjectPool.close();
+    }
+
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testConcurrentBorrowAndEvict() throws Exception {
+
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.addObject();
+
+        for (int i = 0; i < 5000; i++) {
+            final ConcurrentBorrowAndEvictThread one =
+                    new ConcurrentBorrowAndEvictThread(true);
+            final ConcurrentBorrowAndEvictThread two =
+                    new ConcurrentBorrowAndEvictThread(false);
+
+            one.start();
+            two.start();
+            one.join();
+            two.join();
+
+            genericObjectPool.returnObject(one.obj);
+
+            /* Uncomment this for a progress indication
+            if (i % 10 == 0) {
+                System.out.println(i/10);
+            }
+            */
+        }
+    }
+
+    /**
+     * POOL-231 - verify that concurrent invalidates of the same object do not
+     * corrupt pool destroyCount.
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    public void testConcurrentInvalidate() throws Exception {
+        // Get allObjects and idleObjects loaded with some instances
+        final int nObjects = 1000;
+        genericObjectPool.setMaxTotal(nObjects);
+        genericObjectPool.setMaxIdle(nObjects);
+        final String[] obj = new String[nObjects];
+        for (int i = 0; i < nObjects; i++) {
+            obj[i] = genericObjectPool.borrowObject();
+        }
+        for (int i = 0; i < nObjects; i++) {
+            if (i % 2 == 0) {
+                genericObjectPool.returnObject(obj[i]);
+            }
+        }
+        final int nThreads = 20;
+        final int nIterations = 60;
+        final InvalidateThread[] threads = new InvalidateThread[nThreads];
+        // Randomly generated list of distinct invalidation targets
+        final ArrayList<Integer> targets = new ArrayList<>();
+        final Random random = new Random();
+        for (int j = 0; j < nIterations; j++) {
+            // Get a random invalidation target
+            Integer targ = Integer.valueOf(random.nextInt(nObjects));
+            while (targets.contains(targ)) {
+                targ = Integer.valueOf(random.nextInt(nObjects));
+            }
+            targets.add(targ);
+            // Launch nThreads threads all trying to invalidate the target
+            for (int i = 0; i < nThreads; i++) {
+                threads[i] = new InvalidateThread(genericObjectPool, obj[targ.intValue()]);
+            }
+            for (int i = 0; i < nThreads; i++) {
+                new Thread(threads[i]).start();
+            }
+            boolean done = false;
+            while (!done) {
+                done = true;
+                for (int i = 0; i < nThreads; i++) {
+                    done = done && threads[i].complete();
+                }
+                Thread.sleep(100);
+            }
+        }
+        assertEquals(nIterations, genericObjectPool.getDestroyedCount());
+    }
+
+    @Test
+    public void testConstructorNullFactory() {
+        // add dummy assert (won't be invoked because of IAE) to avoid "unused" warning
+        assertThrows(IllegalArgumentException.class,
+                () -> new GenericObjectPool<>(null));
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testConstructors() {
+
+        // Make constructor arguments all different from defaults
+        final int minIdle = 2;
+        final Duration maxWaitDuration = Duration.ofMillis(3);
+        final long maxWaitMillis = maxWaitDuration.toMillis();
+        final int maxIdle = 4;
+        final int maxTotal = 5;
+        final Duration minEvictableIdleDuration = Duration.ofMillis(6);
+        final long minEvictableIdleMillis = minEvictableIdleDuration.toMillis();
+        final int numTestsPerEvictionRun = 7;
+        final boolean testOnBorrow = true;
+        final boolean testOnReturn = true;
+        final boolean testWhileIdle = true;
+        final long timeBetweenEvictionRunsMillis = 8;
+        final boolean blockWhenExhausted = false;
+        final boolean lifo = false;
+        final PooledObjectFactory<Object, RuntimeException> dummyFactory = new DummyFactory();
+        try (GenericObjectPool<Object, RuntimeException> dummyPool = new GenericObjectPool<>(dummyFactory)) {
+            assertEquals(GenericObjectPoolConfig.DEFAULT_MAX_IDLE, dummyPool.getMaxIdle());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS, dummyPool.getMaxWaitMillis());
+            assertEquals(GenericObjectPoolConfig.DEFAULT_MIN_IDLE, dummyPool.getMinIdle());
+            assertEquals(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL, dummyPool.getMaxTotal());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,
+                    dummyPool.getMinEvictableIdleTimeMillis());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME,
+                    dummyPool.getMinEvictableIdleTime());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME,
+                    dummyPool.getMinEvictableIdleDuration());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN,
+                    dummyPool.getNumTestsPerEvictionRun());
+            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW),
+                    Boolean.valueOf(dummyPool.getTestOnBorrow()));
+            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN),
+                    Boolean.valueOf(dummyPool.getTestOnReturn()));
+            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE),
+                    Boolean.valueOf(dummyPool.getTestWhileIdle()));
+            assertEquals(BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS,
+                    dummyPool.getDurationBetweenEvictionRuns());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,
+                    dummyPool.getTimeBetweenEvictionRunsMillis());
+            assertEquals(BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS,
+                    dummyPool.getTimeBetweenEvictionRuns());
+            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_BLOCK_WHEN_EXHAUSTED),
+                    Boolean.valueOf(dummyPool.getBlockWhenExhausted()));
+            assertEquals(Boolean.valueOf(BaseObjectPoolConfig.DEFAULT_LIFO), Boolean.valueOf(dummyPool.getLifo()));
+        }
+
+        final GenericObjectPoolConfig<Object> config = new GenericObjectPoolConfig<>();
+        config.setLifo(lifo);
+        config.setMaxIdle(maxIdle);
+        config.setMinIdle(minIdle);
+        config.setMaxTotal(maxTotal);
+        config.setMaxWait(maxWaitDuration);
+        config.setMinEvictableIdleTimeMillis(minEvictableIdleMillis);
+        assertEquals(minEvictableIdleMillis, config.getMinEvictableIdleTime().toMillis());
+        config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
+        config.setTestOnBorrow(testOnBorrow);
+        config.setTestOnReturn(testOnReturn);
+        config.setTestWhileIdle(testWhileIdle);
+        config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+        assertEquals(timeBetweenEvictionRunsMillis, config.getTimeBetweenEvictionRuns().toMillis());
+        config.setBlockWhenExhausted(blockWhenExhausted);
+        try (GenericObjectPool<Object, RuntimeException> dummyPool = new GenericObjectPool<>(dummyFactory, config)) {
+            assertEquals(maxIdle, dummyPool.getMaxIdle());
+            assertEquals(maxWaitDuration, dummyPool.getMaxWaitDuration());
+            assertEquals(maxWaitMillis, dummyPool.getMaxWaitMillis());
+            assertEquals(minIdle, dummyPool.getMinIdle());
+            assertEquals(maxTotal, dummyPool.getMaxTotal());
+            assertEquals(minEvictableIdleMillis, dummyPool.getMinEvictableIdleTimeMillis());
+            assertEquals(numTestsPerEvictionRun, dummyPool.getNumTestsPerEvictionRun());
+            assertEquals(Boolean.valueOf(testOnBorrow), Boolean.valueOf(dummyPool.getTestOnBorrow()));
+            assertEquals(Boolean.valueOf(testOnReturn), Boolean.valueOf(dummyPool.getTestOnReturn()));
+            assertEquals(Boolean.valueOf(testWhileIdle), Boolean.valueOf(dummyPool.getTestWhileIdle()));
+            assertEquals(timeBetweenEvictionRunsMillis, dummyPool.getTimeBetweenEvictionRunsMillis());
+            assertEquals(Boolean.valueOf(blockWhenExhausted), Boolean.valueOf(dummyPool.getBlockWhenExhausted()));
+            assertEquals(Boolean.valueOf(lifo), Boolean.valueOf(dummyPool.getLifo()));
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testDefaultConfiguration() {
+        assertConfiguration(new GenericObjectPoolConfig<>(),genericObjectPool);
+    }
+
+    /**
+     * Verifies that when a factory's makeObject produces instances that are not
+     * discernible by equals, the pool can handle them.
+     *
+     * JIRA: POOL-283
+     */
+    @Test
+    public void testEqualsIndiscernible() throws Exception {
+        final HashSetFactory factory = new HashSetFactory();
+        try (final GenericObjectPool<HashSet<String>, RuntimeException> pool = new GenericObjectPool<>(factory,
+                new GenericObjectPoolConfig<>())) {
+            final HashSet<String> s1 = pool.borrowObject();
+            final HashSet<String> s2 = pool.borrowObject();
+            pool.returnObject(s1);
+            pool.returnObject(s2);
+        }
+    }
+
+    @Test
+    public void testErrorFactoryDoesNotBlockThreads() throws Exception {
+
+        final CreateErrorFactory factory = new CreateErrorFactory();
+        try (final GenericObjectPool<String, InterruptedException> createFailFactoryPool = new GenericObjectPool<>(factory)) {
+
+            createFailFactoryPool.setMaxTotal(1);
+
+            // Try and borrow the first object from the pool
+            final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createFailFactoryPool, 0);
+            thread1.start();
+
+            // Wait for thread to reach semaphore
+            while (!factory.hasQueuedThreads()) {
+                Thread.sleep(200);
+            }
+
+            // Try and borrow the second object from the pool
+            final WaitingTestThread<InterruptedException> thread2 = new WaitingTestThread<>(createFailFactoryPool, 0);
+            thread2.start();
+            // Pool will not call factory since maximum number of object creations
+            // are already queued.
+
+            // Thread 2 will wait on an object being returned to the pool
+            // Give thread 2 a chance to reach this state
+            Thread.sleep(1000);
+
+            // Release thread1
+            factory.release();
+            // Pre-release thread2
+            factory.release();
+
+            // Both threads should now complete.
+            boolean threadRunning = true;
+            int count = 0;
+            while (threadRunning && count < 15) {
+                threadRunning = thread1.isAlive();
+                threadRunning = thread2.isAlive();
+                Thread.sleep(200);
+                count++;
+            }
+            assertFalse(thread1.isAlive());
+            assertFalse(thread2.isAlive());
+
+            assertTrue(thread1.thrown instanceof UnknownError);
+            assertTrue(thread2.thrown instanceof UnknownError);
+        }
+    }
+
+    /**
+     * Tests addObject contention between ensureMinIdle triggered by
+     * the Evictor with minIdle &gt; 0 and borrowObject.
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictAddObjects() throws Exception {
+        simpleFactory.setMakeLatency(300);
+        simpleFactory.setMaxTotal(2);
+        genericObjectPool.setMaxTotal(2);
+        genericObjectPool.setMinIdle(1);
+        genericObjectPool.borrowObject(); // numActive = 1, numIdle = 0
+        // Create a test thread that will run once and try a borrow after
+        // 150ms fixed delay
+        final TestThread<String, TestException> borrower = new TestThread<>(genericObjectPool, 1, 150, false);
+        final Thread borrowerThread = new Thread(borrower);
+        // Set evictor to run in 100 ms - will create idle instance
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
+        borrowerThread.start();  // Off to the races
+        borrowerThread.join();
+        assertFalse(borrower.failed());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictFIFO() throws Exception {
+        checkEvict(false);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEviction() throws Exception {
+        genericObjectPool.setMaxIdle(500);
+        genericObjectPool.setMaxTotal(500);
+        genericObjectPool.setNumTestsPerEvictionRun(100);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(250));
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
+        genericObjectPool.setTestWhileIdle(true);
+
+        final String[] active = new String[500];
+        for (int i = 0; i < 500; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+        for (int i = 0; i < 500; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        Waiter.sleepQuietly(1000L);
+        assertTrue(genericObjectPool.getNumIdle() < 500,"Should be less than 500 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 400,"Should be less than 400 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 300,"Should be less than 300 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 200,"Should be less than 200 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 100,"Should be less than 100 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertEquals(0,genericObjectPool.getNumIdle(),"Should be zero idle, found " + genericObjectPool.getNumIdle());
+
+        for (int i = 0; i < 500; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+        for (int i = 0; i < 500; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        Waiter.sleepQuietly(1000L);
+        assertTrue(genericObjectPool.getNumIdle() < 500,"Should be less than 500 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 400,"Should be less than 400 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 300,"Should be less than 300 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 200,"Should be less than 200 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertTrue(genericObjectPool.getNumIdle() < 100,"Should be less than 100 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(600L);
+        assertEquals(0,genericObjectPool.getNumIdle(),"Should be zero idle, found " + genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictionInvalid() throws Exception {
+
+        try (final GenericObjectPool<Object, RuntimeException> invalidFactoryPool = new GenericObjectPool<>(new InvalidFactory())) {
+
+            invalidFactoryPool.setMaxIdle(1);
+            invalidFactoryPool.setMaxTotal(1);
+            invalidFactoryPool.setTestOnBorrow(false);
+            invalidFactoryPool.setTestOnReturn(false);
+            invalidFactoryPool.setTestWhileIdle(true);
+            invalidFactoryPool.setMinEvictableIdleTime(Duration.ofSeconds(100));
+            invalidFactoryPool.setNumTestsPerEvictionRun(1);
+
+            final Object p = invalidFactoryPool.borrowObject();
+            invalidFactoryPool.returnObject(p);
+
+            // Run eviction in a separate thread
+            final Thread t = new EvictionThread<>(invalidFactoryPool);
+            t.start();
+
+            // Sleep to make sure evictor has started
+            Thread.sleep(300);
+
+            try {
+                invalidFactoryPool.borrowObject(1);
+            } catch (final NoSuchElementException nsee) {
+                // Ignore
+            }
+
+            // Make sure evictor has finished
+            Thread.sleep(1000);
+
+            // Should have an empty pool
+            assertEquals( 0, invalidFactoryPool.getNumIdle(),"Idle count different than expected.");
+            assertEquals( 0, invalidFactoryPool.getNumActive(),"Total count different than expected.");
+        }
+    }
+
+    /**
+     * Test to make sure evictor visits least recently used objects first,
+     * regardless of FIFO/LIFO.
+     *
+     * JIRA: POOL-86
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictionOrder() throws Exception {
+        checkEvictionOrder(false);
+        tearDown();
+        setUp();
+        checkEvictionOrder(true);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictionPolicy() throws Exception {
+        genericObjectPool.setMaxIdle(500);
+        genericObjectPool.setMaxTotal(500);
+        genericObjectPool.setNumTestsPerEvictionRun(500);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(250));
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
+        genericObjectPool.setTestWhileIdle(true);
+
+        // ClassNotFoundException
+        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(Long.toString(System.currentTimeMillis())),
+                "setEvictionPolicyClassName must throw an error if the class name is invalid.");
+
+        // InstantiationException
+        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(java.io.Serializable.class.getName()),
+                "setEvictionPolicyClassName must throw an error if the class name is invalid.");
+
+        // IllegalAccessException
+        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(java.util.Collections.class.getName()),
+                "setEvictionPolicyClassName must throw an error if the class name is invalid.");
+
+        assertThrows(IllegalArgumentException.class, () -> genericObjectPool.setEvictionPolicyClassName(java.lang.String.class.getName()),
+                () -> "setEvictionPolicyClassName must throw an error if a class that does not implement EvictionPolicy is specified.");
+
+        genericObjectPool.setEvictionPolicy(new TestEvictionPolicy<>());
+        assertEquals(TestEvictionPolicy.class.getName(), genericObjectPool.getEvictionPolicyClassName());
+
+        genericObjectPool.setEvictionPolicyClassName(TestEvictionPolicy.class.getName());
+        assertEquals(TestEvictionPolicy.class.getName(), genericObjectPool.getEvictionPolicyClassName());
+
+        final String[] active = new String[500];
+        for (int i = 0; i < 500; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+        for (int i = 0; i < 500; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        // Eviction policy ignores first 1500 attempts to evict and then always
+        // evicts. After 1s, there should have been two runs of 500 tests so no
+        // evictions
+        Waiter.sleepQuietly(1000L);
+        assertEquals(500, genericObjectPool.getNumIdle(), "Should be 500 idle");
+        // A further 1s wasn't enough so allow 2s for the evictor to clear out
+        // all of the idle objects.
+        Waiter.sleepQuietly(2000L);
+        assertEquals(0, genericObjectPool.getNumIdle(), "Should be 0 idle");
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictionSoftMinIdle() throws Exception {
+        class TimeTest extends BasePooledObjectFactory<TimeTest, RuntimeException> {
+            private final long createTimeMillis;
+
+            public TimeTest() {
+                createTimeMillis = System.currentTimeMillis();
+            }
+
+            @Override
+            public TimeTest create() {
+                return new TimeTest();
+            }
+
+            public long getCreateTimeMillis() {
+                return createTimeMillis;
+            }
+
+            @Override
+            public PooledObject<TimeTest> wrap(final TimeTest value) {
+                return new DefaultPooledObject<>(value);
+            }
+        }
+
+        try (final GenericObjectPool<TimeTest, RuntimeException> timePool = new GenericObjectPool<>(new TimeTest())) {
+
+            timePool.setMaxIdle(5);
+            timePool.setMaxTotal(5);
+            timePool.setNumTestsPerEvictionRun(5);
+            timePool.setMinEvictableIdle(Duration.ofSeconds(3));
+            timePool.setMinEvictableIdleTime(Duration.ofSeconds(3));
+            timePool.setSoftMinEvictableIdleTime(TestConstants.ONE_SECOND_DURATION);
+            timePool.setMinIdle(2);
+
+            final TimeTest[] active = new TimeTest[5];
+            final Long[] creationTime = new Long[5];
+            for (int i = 0; i < 5; i++) {
+                active[i] = timePool.borrowObject();
+                creationTime[i] = Long.valueOf(active[i].getCreateTimeMillis());
+            }
+
+            for (int i = 0; i < 5; i++) {
+                timePool.returnObject(active[i]);
+            }
+
+            // Soft evict all but minIdle(2)
+            Thread.sleep(1500L);
+            timePool.evict();
+            assertEquals( 2, timePool.getNumIdle(),"Idle count different than expected.");
+
+            // Hard evict the rest.
+            Thread.sleep(2000L);
+            timePool.evict();
+            assertEquals( 0, timePool.getNumIdle(),"Idle count different than expected.");
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictionWithNegativeNumTests() throws Exception {
+        // when numTestsPerEvictionRun is negative, it represents a fraction of the idle objects to test
+        genericObjectPool.setMaxIdle(6);
+        genericObjectPool.setMaxTotal(6);
+        genericObjectPool.setNumTestsPerEvictionRun(-2);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(50));
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
+
+        final String[] active = new String[6];
+        for (int i = 0; i < 6; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+        for (int i = 0; i < 6; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        Waiter.sleepQuietly(100L);
+        assertTrue(genericObjectPool.getNumIdle() <= 6,"Should at most 6 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(100L);
+        assertTrue(genericObjectPool.getNumIdle() <= 3,"Should at most 3 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(100L);
+        assertTrue(genericObjectPool.getNumIdle() <= 2,"Should be at most 2 idle, found " + genericObjectPool.getNumIdle());
+        Waiter.sleepQuietly(100L);
+        assertEquals(0,genericObjectPool.getNumIdle(),"Should be zero idle, found " + genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictLIFO() throws Exception {
+        checkEvict(true);
+    }
+
+    /**
+     * Verifies that the evictor visits objects in expected order
+     * and frequency.
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    public void testEvictorVisiting() throws Exception {
+        checkEvictorVisiting(true);
+        checkEvictorVisiting(false);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testEvictWhileEmpty() throws Exception {
+        genericObjectPool.evict();
+        genericObjectPool.evict();
+        genericObjectPool.close();
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testExceptionInValidationDuringEviction() throws Exception {
+        genericObjectPool.setMaxIdle(1);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ZERO);
+        genericObjectPool.setTestWhileIdle(true);
+
+        final String active = genericObjectPool.borrowObject();
+        genericObjectPool.returnObject(active);
+
+        simpleFactory.setThrowExceptionOnValidate(true);
+
+        assertThrows(RuntimeException.class, () -> genericObjectPool.evict());
+        assertEquals(0, genericObjectPool.getNumActive());
+        assertEquals(0, genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testExceptionOnActivateDuringBorrow() throws Exception {
+        final String obj1 = genericObjectPool.borrowObject();
+        final String obj2 = genericObjectPool.borrowObject();
+        genericObjectPool.returnObject(obj1);
+        genericObjectPool.returnObject(obj2);
+        simpleFactory.setThrowExceptionOnActivate(true);
+        simpleFactory.setEvenValid(false);
+        // Activation will now throw every other time
+        // First attempt throws, but loop continues and second succeeds
+        final String obj = genericObjectPool.borrowObject();
+        assertEquals(1, genericObjectPool.getNumActive());
+        assertEquals(0, genericObjectPool.getNumIdle());
+
+        genericObjectPool.returnObject(obj);
+        simpleFactory.setValid(false);
+        // Validation will now fail on activation when borrowObject returns
+        // an idle instance, and then when attempting to create a new instance
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+        assertEquals(0, genericObjectPool.getNumActive());
+        assertEquals(0, genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testExceptionOnDestroyDuringBorrow() throws Exception {
+        simpleFactory.setThrowExceptionOnDestroy(true);
+        genericObjectPool.setTestOnBorrow(true);
+        genericObjectPool.borrowObject();
+        simpleFactory.setValid(false); // Make validation fail on next borrow attempt
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+        assertEquals(1, genericObjectPool.getNumActive());
+        assertEquals(0, genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testExceptionOnDestroyDuringReturn() throws Exception {
+        simpleFactory.setThrowExceptionOnDestroy(true);
+        genericObjectPool.setTestOnReturn(true);
+        final String obj1 = genericObjectPool.borrowObject();
+        genericObjectPool.borrowObject();
+        simpleFactory.setValid(false); // Make validation fail
+        genericObjectPool.returnObject(obj1);
+        assertEquals(1, genericObjectPool.getNumActive());
+        assertEquals(0, genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testExceptionOnPassivateDuringReturn() throws Exception {
+        final String obj = genericObjectPool.borrowObject();
+        simpleFactory.setThrowExceptionOnPassivate(true);
+        genericObjectPool.returnObject(obj);
+        assertEquals(0,genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    public void testFailingFactoryDoesNotBlockThreads() throws Exception {
+
+        final CreateFailFactory factory = new CreateFailFactory();
+        try (final GenericObjectPool<String, InterruptedException> createFailFactoryPool = new GenericObjectPool<>(factory)) {
+
+            createFailFactoryPool.setMaxTotal(1);
+
+            // Try and borrow the first object from the pool
+            final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createFailFactoryPool, 0);
+            thread1.start();
+
+            // Wait for thread to reach semaphore
+            while (!factory.hasQueuedThreads()) {
+                Thread.sleep(200);
+            }
+
+            // Try and borrow the second object from the pool
+            final WaitingTestThread<InterruptedException> thread2 = new WaitingTestThread<>(createFailFactoryPool, 0);
+            thread2.start();
+            // Pool will not call factory since maximum number of object creations
+            // are already queued.
+
+            // Thread 2 will wait on an object being returned to the pool
+            // Give thread 2 a chance to reach this state
+            Thread.sleep(1000);
+
+            // Release thread1
+            factory.release();
+            // Pre-release thread2
+            factory.release();
+
+            // Both threads should now complete.
+            boolean threadRunning = true;
+            int count = 0;
+            while (threadRunning && count < 15) {
+                threadRunning = thread1.isAlive();
+                threadRunning = thread2.isAlive();
+                Thread.sleep(200);
+                count++;
+            }
+            assertFalse(thread1.isAlive());
+            assertFalse(thread2.isAlive());
+
+            assertTrue(thread1.thrown instanceof UnsupportedCharsetException);
+            assertTrue(thread2.thrown instanceof UnsupportedCharsetException);
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testFIFO() throws Exception {
+        genericObjectPool.setLifo(false);
+        genericObjectPool.addObject(); // "0"
+        genericObjectPool.addObject(); // "1"
+        genericObjectPool.addObject(); // "2"
+        assertEquals( "0", genericObjectPool.borrowObject(),"Oldest");
+        assertEquals( "1", genericObjectPool.borrowObject(),"Middle");
+        assertEquals( "2", genericObjectPool.borrowObject(),"Youngest");
+        final String o = genericObjectPool.borrowObject();
+        assertEquals( "3", o,"new-3");
+        genericObjectPool.returnObject(o);
+        assertEquals( o, genericObjectPool.borrowObject(),"returned-3");
+        assertEquals( "4", genericObjectPool.borrowObject(),"new-4");
+    }
+
+    @Test
+    public void testGetFactoryType_DefaultPooledObjectFactory() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(createDefaultPooledObjectFactory())) {
+            assertNotNull(pool.getFactoryType());
+        }
+    }
+
+    @Test
+    public void testGetFactoryType_NullPooledObjectFactory() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(createNullPooledObjectFactory())) {
+            assertNotNull(pool.getFactoryType());
+        }
+    }
+
+    @Test
+    public void testGetFactoryType_PoolUtilsSynchronizedDefaultPooledFactory() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
+                PoolUtils.synchronizedPooledFactory(createDefaultPooledObjectFactory()))) {
+            assertNotNull(pool.getFactoryType());
+        }
+    }
+
+    @Test
+    public void testGetFactoryType_PoolUtilsSynchronizedNullPooledFactory() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
+                PoolUtils.synchronizedPooledFactory(createNullPooledObjectFactory()))) {
+            assertNotNull(pool.getFactoryType());
+        }
+    }
+
+    @Test
+    public void testGetFactoryType_SynchronizedDefaultPooledObjectFactory() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
+                new TestSynchronizedPooledObjectFactory<>(createDefaultPooledObjectFactory()))) {
+            assertNotNull(pool.getFactoryType());
+        }
+    }
+
+    @Test
+    public void testGetFactoryType_SynchronizedNullPooledObjectFactory() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
+                new TestSynchronizedPooledObjectFactory<>(createNullPooledObjectFactory()))) {
+            assertNotNull(pool.getFactoryType());
+        }
+    }
+
+    @Test
+    public void testGetStatsString() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
+                new TestSynchronizedPooledObjectFactory<>(createNullPooledObjectFactory()))) {
+            assertNotNull(pool.getStatsString());
+        }
+    }
+
+    /**
+     * Verify that threads waiting on a depleted pool get served when a checked out object is
+     * invalidated.
+     *
+     * JIRA: POOL-240
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    public void testInvalidateFreesCapacity() throws Exception {
+        final SimpleFactory factory = new SimpleFactory();
+        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(factory)) {
+            pool.setMaxTotal(2);
+            pool.setMaxWaitMillis(500);
+            // Borrow an instance and hold if for 5 seconds
+            final WaitingTestThread<TestException> thread1 = new WaitingTestThread<>(pool, 5000);
+            thread1.start();
+            // Borrow another instance
+            final String obj = pool.borrowObject();
+            // Launch another thread - will block, but fail in 500 ms
+            final WaitingTestThread<TestException> thread2 = new WaitingTestThread<>(pool, 100);
+            thread2.start();
+            // Invalidate the object borrowed by this thread - should allow thread2 to create
+            Thread.sleep(20);
+            pool.invalidateObject(obj);
+            Thread.sleep(600); // Wait for thread2 to timeout
+            if (thread2.thrown != null) {
+                fail(thread2.thrown.toString());
+            }
+        }
+    }
+
+    /**
+     * Ensure the pool is registered.
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testJmxRegistration() {
+        final ObjectName oname = genericObjectPool.getJmxName();
+        final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+        final Set<ObjectName> result = mbs.queryNames(oname, null);
+        assertEquals(1, result.size());
+        genericObjectPool.jmxUnregister();
+
+        final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
+        config.setJmxEnabled(false);
+        try (final GenericObjectPool<String, TestException> poolWithoutJmx = new GenericObjectPool<>(simpleFactory, config)) {
+            assertNull(poolWithoutJmx.getJmxName());
+            config.setJmxEnabled(true);
+            poolWithoutJmx.jmxUnregister();
+        }
+
+        config.setJmxNameBase(null);
+        try (final GenericObjectPool<String, TestException> poolWithDefaultJmxNameBase = new GenericObjectPool<>(simpleFactory, config)) {
+            assertNotNull(poolWithDefaultJmxNameBase.getJmxName());
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testLIFO() throws Exception {
+        final String o;
+        genericObjectPool.setLifo(true);
+        genericObjectPool.addObject(); // "0"
+        genericObjectPool.addObject(); // "1"
+        genericObjectPool.addObject(); // "2"
+        assertEquals( "2", genericObjectPool.borrowObject(),"Youngest");
+        assertEquals( "1", genericObjectPool.borrowObject(),"Middle");
+        assertEquals( "0", genericObjectPool.borrowObject(),"Oldest");
+        o = genericObjectPool.borrowObject();
+        assertEquals( "3", o,"new-3");
+        genericObjectPool.returnObject(o);
+        assertEquals( o, genericObjectPool.borrowObject(),"returned-3");
+        assertEquals( "4", genericObjectPool.borrowObject(),"new-4");
+    }
+
+    /**
+     * Test the following scenario:
+     *   Thread 1 borrows an instance
+     *   Thread 2 starts to borrow another instance before thread 1 returns its instance
+     *   Thread 1 returns its instance while thread 2 is validating its newly created instance
+     * The test verifies that the instance created by Thread 2 is not leaked.
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMakeConcurrentWithReturn() throws Exception {
+        genericObjectPool.setTestOnBorrow(true);
+        simpleFactory.setValid(true);
+        // Borrow and return an instance, with a short wait
+        final WaitingTestThread<TestException> thread1 = new WaitingTestThread<>(genericObjectPool, 200);
+        thread1.start();
+        Thread.sleep(50); // wait for validation to succeed
+        // Slow down validation and borrow an instance
+        simpleFactory.setValidateLatency(400);
+        final String instance = genericObjectPool.borrowObject();
+        // Now make sure that we have not leaked an instance
+        assertEquals(simpleFactory.getMakeCounter(), genericObjectPool.getNumIdle() + 1);
+        genericObjectPool.returnObject(instance);
+        assertEquals(simpleFactory.getMakeCounter(), genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxIdle() throws Exception {
+        genericObjectPool.setMaxTotal(100);
+        genericObjectPool.setMaxIdle(8);
+        final String[] active = new String[100];
+        for(int i=0;i<100;i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+        assertEquals(100,genericObjectPool.getNumActive());
+        assertEquals(0,genericObjectPool.getNumIdle());
+        for(int i=0;i<100;i++) {
+            genericObjectPool.returnObject(active[i]);
+            assertEquals(99 - i,genericObjectPool.getNumActive());
+            assertEquals(i < 8 ? i+1 : 8,genericObjectPool.getNumIdle());
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxIdleZero() throws Exception {
+        genericObjectPool.setMaxTotal(100);
+        genericObjectPool.setMaxIdle(0);
+        final String[] active = new String[100];
+        for(int i=0;i<100;i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+        assertEquals(100,genericObjectPool.getNumActive());
+        assertEquals(0,genericObjectPool.getNumIdle());
+        for(int i=0;i<100;i++) {
+            genericObjectPool.returnObject(active[i]);
+            assertEquals(99 - i,genericObjectPool.getNumActive());
+            assertEquals(0, genericObjectPool.getNumIdle());
+        }
+    }
+
+    /**
+     * Showcasing a possible deadlock situation as reported in POOL-356
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @SuppressWarnings("rawtypes")
+    public void testMaxIdleZeroUnderLoad() {
+        // Config
+        final int numThreads = 199; // And main thread makes a round 200.
+        final int numIter = 20;
+        final int delay = 25;
+        final int maxTotal = 10;
+
+        simpleFactory.setMaxTotal(maxTotal);
+        genericObjectPool.setMaxTotal(maxTotal);
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(-1));
+
+        // this is important to trigger POOL-356
+        genericObjectPool.setMaxIdle(0);
+
+        // Start threads to borrow objects
+        final TestThread[] threads = new TestThread[numThreads];
+        for(int i=0;i<numThreads;i++) {
+            // Factor of 2 on iterations so main thread does work whilst other
+            // threads are running. Factor of 2 on delay so average delay for
+            // other threads == actual delay for main thread
+            threads[i] = new TestThread<>(genericObjectPool, numIter * 2, delay * 2);
+            final Thread t = new Thread(threads[i]);
+            t.start();
+        }
+        // Give the threads a chance to start doing some work
+        Waiter.sleepQuietly(100L);
+
+        for (int i = 0; i < numIter; i++) {
+            String obj = null;
+            try {
+                Waiter.sleepQuietly(delay);
+                obj = genericObjectPool.borrowObject();
+                // Under load, observed numActive > maxTotal
+                if (genericObjectPool.getNumActive() > genericObjectPool.getMaxTotal()) {
+                    throw new IllegalStateException("Too many active objects");
+                }
+                Waiter.sleepQuietly(delay);
+            } catch (final Exception e) {
+                // Shouldn't happen
+                e.printStackTrace();
+                fail("Exception on borrow");
+            } finally {
+                if (obj != null) {
+                    try {
+                        genericObjectPool.returnObject(obj);
+                    } catch (final Exception e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        for (int i = 0; i < numThreads; i++) {
+            while (!threads[i].complete()) {
+                Waiter.sleepQuietly(500L);
+            }
+            if (threads[i].failed()) {
+                threads[i].error.printStackTrace();
+                fail("Thread " + i + " failed: " + threads[i].error.toString());
+            }
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxTotal() throws Exception {
+        genericObjectPool.setMaxTotal(3);
+        genericObjectPool.setBlockWhenExhausted(false);
+
+        genericObjectPool.borrowObject();
+        genericObjectPool.borrowObject();
+        genericObjectPool.borrowObject();
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+    }
+
+    /**
+     * Verifies that maxTotal is not exceeded when factory destroyObject
+     * has high latency, testOnReturn is set and there is high incidence of
+     * validation failures.
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxTotalInvariant() {
+        final int maxTotal = 15;
+        simpleFactory.setEvenValid(false);     // Every other validation fails
+        simpleFactory.setDestroyLatency(100);  // Destroy takes 100 ms
+        simpleFactory.setMaxTotal(maxTotal); // (makes - destroys) bound
+        simpleFactory.setValidationEnabled(true);
+        genericObjectPool.setMaxTotal(maxTotal);
+        genericObjectPool.setMaxIdle(-1);
+        genericObjectPool.setTestOnReturn(true);
+        genericObjectPool.setMaxWaitMillis(1000L);
+        runTestThreads(5, 10, 50, genericObjectPool);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @SuppressWarnings("rawtypes")
+    public void testMaxTotalUnderLoad() {
+        // Config
+        final int numThreads = 199; // And main thread makes a round 200.
+        final int numIter = 20;
+        final int delay = 25;
+        final int maxTotal = 10;
+
+        simpleFactory.setMaxTotal(maxTotal);
+        genericObjectPool.setMaxTotal(maxTotal);
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(-1));
+
+        // Start threads to borrow objects
+        final TestThread[] threads = new TestThread[numThreads];
+        for(int i=0;i<numThreads;i++) {
+            // Factor of 2 on iterations so main thread does work whilst other
+            // threads are running. Factor of 2 on delay so average delay for
+            // other threads == actual delay for main thread
+            threads[i] = new TestThread<>(genericObjectPool, numIter * 2, delay * 2);
+            final Thread t = new Thread(threads[i]);
+            t.start();
+        }
+        // Give the threads a chance to start doing some work
+        Waiter.sleepQuietly(5000);
+
+        for (int i = 0; i < numIter; i++) {
+            String obj = null;
+            try {
+                Waiter.sleepQuietly(delay);
+                obj = genericObjectPool.borrowObject();
+                // Under load, observed numActive > maxTotal
+                if (genericObjectPool.getNumActive() > genericObjectPool.getMaxTotal()) {
+                    throw new IllegalStateException("Too many active objects");
+                }
+                Waiter.sleepQuietly(delay);
+            } catch (final Exception e) {
+                // Shouldn't happen
+                e.printStackTrace();
+                fail("Exception on borrow");
+            } finally {
+                if (obj != null) {
+                    try {
+                        genericObjectPool.returnObject(obj);
+                    } catch (final Exception e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        for (int i = 0; i < numThreads; i++) {
+            while(!threads[i].complete()) {
+                Waiter.sleepQuietly(500L);
+            }
+            if(threads[i].failed()) {
+                fail("Thread " + i + " failed: " + threads[i].error.toString());
+            }
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxTotalZero() throws Exception {
+        genericObjectPool.setMaxTotal(0);
+        genericObjectPool.setBlockWhenExhausted(false);
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+    }
+
+    /*
+     * Test multi-threaded pool access.
+     * Multiple threads, but maxTotal only allows half the threads to succeed.
+     *
+     * This test was prompted by Continuum build failures in the Commons DBCP test case:
+     * TestPerUserPoolDataSource.testMultipleThreads2()
+     * Let's see if the this fails on Continuum too!
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxWaitMultiThreaded() throws Exception {
+        final long maxWait = 500; // wait for connection
+        final long holdTime = 2 * maxWait; // how long to hold connection
+        final int threads = 10; // number of threads to grab the object initially
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setMaxWaitMillis(maxWait);
+        genericObjectPool.setMaxTotal(threads);
+        // Create enough threads so half the threads will have to wait
+        final WaitingTestThread<TestException>[] wtt = new WaitingTestThread[threads * 2];
+        for (int i = 0; i < wtt.length; i++) {
+            wtt[i] = new WaitingTestThread<>(genericObjectPool, holdTime);
+        }
+        final long originMillis = System.currentTimeMillis() - 1000;
+        for (final WaitingTestThread<TestException> element : wtt) {
+            element.start();
+        }
+        int failed = 0;
+        for (final WaitingTestThread<TestException> element : wtt) {
+            element.join();
+            if (element.thrown != null){
+                failed++;
+            }
+        }
+        if (DISPLAY_THREAD_DETAILS || wtt.length/2 != failed){
+            System.out.println(
+                    "MaxWait: " + maxWait +
+                    " HoldTime: " + holdTime +
+                     " MaxTotal: " + threads +
+                    " Threads: " + wtt.length +
+                    " Failed: " + failed
+                    );
+            for (final WaitingTestThread<TestException> wt : wtt) {
+                System.out.println(
+                        "PreBorrow: " + (wt.preBorrowMillis - originMillis) +
+                        " PostBorrow: " + (wt.postBorrowMillis != 0 ? wt.postBorrowMillis - originMillis : -1) +
+                        " BorrowTime: " + (wt.postBorrowMillis != 0 ? wt.postBorrowMillis - wt.preBorrowMillis : -1) +
+                        " PostReturn: " + (wt.postReturnMillis != 0 ? wt.postReturnMillis - originMillis : -1) +
+                        " Ended: " + (wt.endedMillis - originMillis) +
+                        " ObjId: " + wt.objectId
+                        );
+            }
+        }
+        assertEquals(wtt.length / 2, failed,"Expected half the threads to fail");
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMinIdle() throws Exception {
+        genericObjectPool.setMaxIdle(500);
+        genericObjectPool.setMinIdle(5);
+        genericObjectPool.setMaxTotal(10);
+        genericObjectPool.setNumTestsPerEvictionRun(0);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(50));
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
+        genericObjectPool.setTestWhileIdle(true);
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
+
+        final String[] active = new String[5];
+        active[0] = genericObjectPool.borrowObject();
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
+
+        for (int i = 1; i < 5; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
+
+        for (int i = 0; i < 5; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(10, genericObjectPool.getNumIdle(), "Should be 10 idle, found " + genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testMinIdleMaxTotal() throws Exception {
+        genericObjectPool.setMaxIdle(500);
+        genericObjectPool.setMinIdle(5);
+        genericObjectPool.setMaxTotal(10);
+        genericObjectPool.setNumTestsPerEvictionRun(0);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(50));
+        genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
+        genericObjectPool.setTestWhileIdle(true);
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
+
+        final String[] active = new String[10];
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
+
+        for (int i = 0; i < 5; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(5, genericObjectPool.getNumIdle(), "Should be 5 idle, found " + genericObjectPool.getNumIdle());
+
+        for(int i = 0 ; i < 5 ; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(10, genericObjectPool.getNumIdle(), "Should be 10 idle, found " + genericObjectPool.getNumIdle());
+
+        for (int i = 0; i < 10; i++) {
+            active[i] = genericObjectPool.borrowObject();
+        }
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(0, genericObjectPool.getNumIdle(), "Should be 0 idle, found " + genericObjectPool.getNumIdle());
+
+        for (int i = 0; i < 10; i++) {
+            genericObjectPool.returnObject(active[i]);
+        }
+
+        Waiter.sleepQuietly(150L);
+        assertEquals(10, genericObjectPool.getNumIdle(), "Should be 10 idle, found " + genericObjectPool.getNumIdle());
+    }
+
+    /**
+     * Verifies that returning an object twice (without borrow in between) causes ISE
+     * but does not re-validate or re-passivate the instance.
+     *
+     * JIRA: POOL-285
+     */
+    @Test
+    public void testMultipleReturn() throws Exception {
+        final WaiterFactory<String> factory = new WaiterFactory<>(0, 0, 0, 0, 0, 0);
+        try (final GenericObjectPool<Waiter, IllegalStateException> pool = new GenericObjectPool<>(factory)) {
+            pool.setTestOnReturn(true);
+            final Waiter waiter = pool.borrowObject();
+            pool.returnObject(waiter);
+            assertEquals(1, waiter.getValidationCount());
+            assertEquals(1, waiter.getPassivationCount());
+            try {
+                pool.returnObject(waiter);
+                fail("Expecting IllegalStateException from multiple return");
+            } catch (final IllegalStateException ex) {
+                // Exception is expected, now check no repeat validation/passivation
+                assertEquals(1, waiter.getValidationCount());
+                assertEquals(1, waiter.getPassivationCount());
+            }
+        }
+    }
+
+    // POOL-248
+    @Test
+    public void testMultipleReturnOfSameObject() throws Exception {
+        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(simpleFactory, new GenericObjectPoolConfig<>())) {
+
+            assertEquals(0, pool.getNumActive());
+            assertEquals(0, pool.getNumIdle());
+
+            final String obj = pool.borrowObject();
+
+            assertEquals(1, pool.getNumActive());
+            assertEquals(0, pool.getNumIdle());
+
+            pool.returnObject(obj);
+
+            assertEquals(0, pool.getNumActive());
+            assertEquals(1, pool.getNumIdle());
+
+            assertThrows(IllegalStateException.class,
+                    () -> pool.returnObject(obj));
+
+            assertEquals(0, pool.getNumActive());
+            assertEquals(1, pool.getNumIdle());
+        }
+    }
+
+    /**
+     * Verifies that when a borrowed object is mutated in a way that does not
+     * preserve equality and hashcode, the pool can recognized it on return.
+     *
+     * JIRA: POOL-284
+     */
+    @Test
+    public void testMutable() throws Exception {
+        final HashSetFactory factory = new HashSetFactory();
+        try (final GenericObjectPool<HashSet<String>, RuntimeException> pool = new GenericObjectPool<>(factory,
+                new GenericObjectPoolConfig<>())) {
+            final HashSet<String> s1 = pool.borrowObject();
+            final HashSet<String> s2 = pool.borrowObject();
+            s1.add("One");
+            s2.add("One");
+            pool.returnObject(s1);
+            pool.returnObject(s2);
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testNegativeMaxTotal() throws Exception {
+        genericObjectPool.setMaxTotal(-1);
+        genericObjectPool.setBlockWhenExhausted(false);
+        final String obj = genericObjectPool.borrowObject();
+        assertEquals(getNthObject(0),obj);
+        genericObjectPool.returnObject(obj);
+    }
+
+    /**
+     * Verifies that concurrent threads never "share" instances
+     */
+    @Test
+    public void testNoInstanceOverlap() {
+        final int maxTotal = 5;
+        final int numThreads = 100;
+        final int delay = 1;
+        final int iterations = 1000;
+        final AtomicIntegerFactory factory = new AtomicIntegerFactory();
+        try (final GenericObjectPool<AtomicInteger, RuntimeException> pool = new GenericObjectPool<>(factory)) {
+            pool.setMaxTotal(maxTotal);
+            pool.setMaxIdle(maxTotal);
+            pool.setTestOnBorrow(true);
+            pool.setBlockWhenExhausted(true);
+            pool.setMaxWaitMillis(-1);
+            runTestThreads(numThreads, iterations, delay, pool);
+            assertEquals(0, pool.getDestroyedByBorrowValidationCount());
+        }
+    }
+
+    /**
+     * POOL-376
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testNoInvalidateNPE() throws Exception {
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setTestOnCreate(true);
+        genericObjectPool.setMaxWaitMillis(-1);
+        final String obj = genericObjectPool.borrowObject();
+        // Make validation fail - this will cause create() to return null
+        simpleFactory.setValid(false);
+        // Create a take waiter
+        final WaitingTestThread<TestException> wtt = new WaitingTestThread<>(genericObjectPool, 200);
+        wtt.start();
+        // Give wtt time to start
+        Thread.sleep(200);
+        genericObjectPool.invalidateObject(obj);
+        // Now allow create to succeed so waiter can be served
+        simpleFactory.setValid(true);
+    }
+
+    public void testPreparePool() throws Exception {
+        genericObjectPool.setMinIdle(1);
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.preparePool();
+        assertEquals(1, genericObjectPool.getNumIdle());
+        final String obj = genericObjectPool.borrowObject();
+        genericObjectPool.preparePool();
+        assertEquals(0, genericObjectPool.getNumIdle());
+        genericObjectPool.setMinIdle(0);
+        genericObjectPool.returnObject(obj);
+        genericObjectPool.preparePool();
+        assertEquals(0, genericObjectPool.getNumIdle());
+    }
+
+    @Test/* maxWaitMillis x2 + padding */
+    @Timeout(value = 1200, unit = TimeUnit.MILLISECONDS)
+    public void testReturnBorrowObjectWithingMaxWaitMillis() throws Exception {
+        final long maxWaitMillis = 500;
+
+        try (final GenericObjectPool<String, InterruptedException> createSlowObjectFactoryPool = new GenericObjectPool<>(
+                createSlowObjectFactory(60000))) {
+            createSlowObjectFactoryPool.setMaxTotal(1);
+            createSlowObjectFactoryPool.setMaxWaitMillis(maxWaitMillis);
+
+            // thread1 tries creating a slow object to make pool full.
+            final WaitingTestThread<InterruptedException> thread1 = new WaitingTestThread<>(createSlowObjectFactoryPool, 0);
+            thread1.start();
+
+            // Wait for thread1's reaching to create().
+            Thread.sleep(100);
+
+            // another one tries borrowObject. It should return within maxWaitMillis.
+            assertThrows(NoSuchElementException.class, () -> createSlowObjectFactoryPool.borrowObject(maxWaitMillis),
+                    "borrowObject must fail due to timeout by maxWaitMillis");
+
+            assertTrue(thread1.isAlive());
+        }
+    }
+
+    /**
+     * This is the test case for POOL-263. It is disabled since it will always
+     * pass without artificial delay being injected into GOP.returnObject() and
+     * a way to this hasn't currently been found that doesn't involve
+     * polluting the GOP implementation. The artificial delay needs to be
+     * inserted just before the final call to isLifo() in the returnObject()
+     * method.
+     */
+    //@Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testReturnObject() throws Exception {
+
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setMaxIdle(-1);
+        final String active = genericObjectPool.borrowObject();
+
+        assertEquals(1, genericObjectPool.getNumActive());
+        assertEquals(0, genericObjectPool.getNumIdle());
+
+        final Thread t = new Thread(() -> genericObjectPool.close());
+        t.start();
+
+        genericObjectPool.returnObject(active);
+
+        // Wait for the close() thread to complete
+        while (t.isAlive()) {
+            Thread.sleep(50);
+        }
+
+        assertEquals(0, genericObjectPool.getNumIdle());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testSetConfig() throws Exception {
+        final GenericObjectPoolConfig<String> expected = new GenericObjectPoolConfig<>();
+        assertConfiguration(expected,genericObjectPool);
+        expected.setMaxTotal(2);
+        expected.setMaxIdle(3);
+        expected.setMaxWait(Duration.ofMillis(5));
+        expected.setMinEvictableIdleTime(Duration.ofMillis(7L));
+        expected.setNumTestsPerEvictionRun(9);
+        expected.setTestOnCreate(true);
+        expected.setTestOnBorrow(true);
+        expected.setTestOnReturn(true);
+        expected.setTestWhileIdle(true);
+        expected.setTimeBetweenEvictionRuns(Duration.ofMillis(11L));
+        expected.setBlockWhenExhausted(false);
+        genericObjectPool.setConfig(expected);
+        assertConfiguration(expected,genericObjectPool);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testSettersAndGetters() throws Exception {
+        {
+            // The object receives an Exception during its creation to prevent
+            // memory leaks. See BaseGenericObjectPool constructor for more details.
+            assertNotEquals("", genericObjectPool.getCreationStackTrace());
+        }
+        {
+            assertEquals(0, genericObjectPool.getBorrowedCount());
+        }
+        {
+            assertEquals(0, genericObjectPool.getReturnedCount());
+        }
+        {
+            assertEquals(0, genericObjectPool.getCreatedCount());
+        }
+        {
+            assertEquals(0, genericObjectPool.getDestroyedCount());
+        }
+        {
+            assertEquals(0, genericObjectPool.getDestroyedByEvictorCount());
+        }
+        {
+            assertEquals(0, genericObjectPool.getDestroyedByBorrowValidationCount());
+        }
+        {
+            assertEquals(0, genericObjectPool.getMeanActiveTimeMillis());
+        }
+        {
+            assertEquals(0, genericObjectPool.getMeanIdleTimeMillis());
+        }
+        {
+            assertEquals(0, genericObjectPool.getMeanBorrowWaitTimeMillis());
+        }
+        {
+            assertEquals(0, genericObjectPool.getMaxBorrowWaitTimeMillis());
+        }
+        {
+            assertEquals(0, genericObjectPool.getNumIdle());
+        }
+        {
+            genericObjectPool.setMaxTotal(123);
+            assertEquals(123,genericObjectPool.getMaxTotal());
+        }
+        {
+            genericObjectPool.setMaxIdle(12);
+            assertEquals(12,genericObjectPool.getMaxIdle());
+        }
+        {
+            genericObjectPool.setMaxWaitMillis(1234L);
+            assertEquals(1234L,genericObjectPool.getMaxWaitMillis());
+        }
+        {
+            genericObjectPool.setMinEvictableIdleTimeMillis(12345L);
+            assertEquals(12345L,genericObjectPool.getMinEvictableIdleDuration().toMillis());
+            assertEquals(12345L,genericObjectPool.getMinEvictableIdleTimeMillis());
+            assertEquals(12345L,genericObjectPool.getMinEvictableIdleTime().toMillis());
+        }
+        {
+            genericObjectPool.setNumTestsPerEvictionRun(11);
+            assertEquals(11,genericObjectPool.getNumTestsPerEvictionRun());
+        }
+        {
+            genericObjectPool.setTestOnBorrow(true);
+            assertTrue(genericObjectPool.getTestOnBorrow());
+            genericObjectPool.setTestOnBorrow(false);
+            assertFalse(genericObjectPool.getTestOnBorrow());
+        }
+        {
+            genericObjectPool.setTestOnReturn(true);
+            assertTrue(genericObjectPool.getTestOnReturn());
+            genericObjectPool.setTestOnReturn(false);
+            assertFalse(genericObjectPool.getTestOnReturn());
+        }
+        {
+            genericObjectPool.setTestWhileIdle(true);
+            assertTrue(genericObjectPool.getTestWhileIdle());
+            genericObjectPool.setTestWhileIdle(false);
+            assertFalse(genericObjectPool.getTestWhileIdle());
+        }
+        {
+            genericObjectPool.setTimeBetweenEvictionRunsMillis(11235L);
+            assertEquals(11235L,genericObjectPool.getDurationBetweenEvictionRuns().toMillis());
+            assertEquals(11235L,genericObjectPool.getTimeBetweenEvictionRunsMillis());
+            assertEquals(11235L,genericObjectPool.getTimeBetweenEvictionRuns().toMillis());
+        }
+        {
+            genericObjectPool.setSoftMinEvictableIdleTimeMillis(12135L);
+            assertEquals(12135L,genericObjectPool.getSoftMinEvictableIdleDuration().toMillis());
+            assertEquals(12135L,genericObjectPool.getSoftMinEvictableIdleTimeMillis());
+            assertEquals(12135L,genericObjectPool.getSoftMinEvictableIdleTime().toMillis());
+        }
+        {
+            genericObjectPool.setBlockWhenExhausted(true);
+            assertTrue(genericObjectPool.getBlockWhenExhausted());
+            genericObjectPool.setBlockWhenExhausted(false);
+            assertFalse(genericObjectPool.getBlockWhenExhausted());
+        }
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testStartAndStopEvictor() throws Exception {
+        // set up pool without evictor
+        genericObjectPool.setMaxIdle(6);
+        genericObjectPool.setMaxTotal(6);
+        genericObjectPool.setNumTestsPerEvictionRun(6);
+        genericObjectPool.setMinEvictableIdleTime(Duration.ofMillis(100));
+
+        for (int j = 0; j < 2; j++) {
+            // populate the pool
+            {
+                final String[] active = new String[6];
+                for (int i = 0; i < 6; i++) {
+                    active[i] = genericObjectPool.borrowObject();
+                }
+                for (int i = 0; i < 6; i++) {
+                    genericObjectPool.returnObject(active[i]);
+                }
+            }
+
+            // note that it stays populated
+            assertEquals(6,genericObjectPool.getNumIdle(),"Should have 6 idle");
+
+            // start the evictor
+            genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(50));
+
+            // wait a second (well, .2 seconds)
+            Waiter.sleepQuietly(200L);
+
+            // assert that the evictor has cleared out the pool
+            assertEquals(0,genericObjectPool.getNumIdle(),"Should have 0 idle");
+
+            // stop the evictor
+            genericObjectPool.startEvictor(Duration.ZERO);
+        }
+    }
+
+    @Test
+    public void testSwallowedExceptionListener() {
+        genericObjectPool.setSwallowedExceptionListener(null); // must simply return
+        final List<Exception> swallowedExceptions = new ArrayList<>();
+        /*
+         * A simple listener, that will throw a OOM on 3rd exception.
+         */
+        final SwallowedExceptionListener listener = e -> {
+            if (swallowedExceptions.size() == 2) {
+                throw new OutOfMemoryError();
+            }
+            swallowedExceptions.add(e);
+        };
+        genericObjectPool.setSwallowedExceptionListener(listener);
+
+        final Exception e1 = new Exception();
+        final Exception e2 = new ArrayIndexOutOfBoundsException();
+
+        genericObjectPool.swallowException(e1);
+        genericObjectPool.swallowException(e2);
+
+        assertThrows(OutOfMemoryError.class, () -> genericObjectPool.swallowException(e1));
+
+        assertEquals(2, swallowedExceptions.size());
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testThreaded1() throws Exception {
+        genericObjectPool.setMaxTotal(15);
+        genericObjectPool.setMaxIdle(15);
+        genericObjectPool.setMaxWaitMillis(1000L);
+        runTestThreads(20, 100, 50, genericObjectPool);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testTimeoutNoLeak() throws Exception {
+        genericObjectPool.setMaxTotal(2);
+        genericObjectPool.setMaxWaitMillis(10);
+        genericObjectPool.setBlockWhenExhausted(true);
+        final String obj = genericObjectPool.borrowObject();
+        final String obj2 = genericObjectPool.borrowObject();
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+        genericObjectPool.returnObject(obj2);
+        genericObjectPool.returnObject(obj);
+
+        genericObjectPool.borrowObject();
+        genericObjectPool.borrowObject();
+    }
+
+    /**
+     * Tests POOL-361
+     */
+    @Test
+    public void testValidateOnCreate() throws Exception {
+        genericObjectPool.setTestOnCreate(true);
+        genericObjectPool.addObject();
+        assertEquals(1, simpleFactory.validateCounter);
+    }
+
+    /**
+     * Tests POOL-361
+     */
+    @Test
+    public void testValidateOnCreateFailure() throws Exception {
+        genericObjectPool.setTestOnCreate(true);
+        genericObjectPool.setTestOnBorrow(false);
+        genericObjectPool.setMaxTotal(2);
+        simpleFactory.setValid(false);
+        // Make sure failed validations do not leak capacity
+        genericObjectPool.addObject();
+        genericObjectPool.addObject();
+        assertEquals(0, genericObjectPool.getNumIdle());
+        assertEquals(0, genericObjectPool.getNumActive());
+        simpleFactory.setValid(true);
+        final String obj = genericObjectPool.borrowObject();
+        assertNotNull(obj);
+        genericObjectPool.addObject();
+        // Should have one idle, one out now
+        assertEquals(1, genericObjectPool.getNumIdle());
+        assertEquals(1, genericObjectPool.getNumActive());
+	}
+
+    /**
+     * Verify that threads waiting on a depleted pool get served when a returning object fails
+     * validation.
+     *
+     * JIRA: POOL-240
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    public void testValidationFailureOnReturnFreesCapacity() throws Exception {
+        final SimpleFactory factory = new SimpleFactory();
+        factory.setValid(false); // Validate will always fail
+        factory.setValidationEnabled(true);
+        try (final GenericObjectPool<String, TestException> pool = new GenericObjectPool<>(factory)) {
+            pool.setMaxTotal(2);
+            pool.setMaxWaitMillis(1500);
+            pool.setTestOnReturn(true);
+            pool.setTestOnBorrow(false);
+            // Borrow an instance and hold if for 5 seconds
+            final WaitingTestThread<TestException> thread1 = new WaitingTestThread<>(pool, 5000);
+            thread1.start();
+            // Borrow another instance and return it after 500 ms (validation will fail)
+            final WaitingTestThread<TestException> thread2 = new WaitingTestThread<>(pool, 500);
+            thread2.start();
+            Thread.sleep(50);
+            // Try to borrow an object
+            final String obj = pool.borrowObject();
+            pool.returnObject(obj);
+        }
+    }
+
+    // POOL-276
+    @Test
+    public void testValidationOnCreateOnly() throws Exception {
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setTestOnCreate(true);
+        genericObjectPool.setTestOnBorrow(false);
+        genericObjectPool.setTestOnReturn(false);
+        genericObjectPool.setTestWhileIdle(false);
+
+        final String o1 = genericObjectPool.borrowObject();
+        assertEquals("0", o1);
+        final Timer t = new Timer();
+        t.schedule(
+                new TimerTask() {
+                    @Override
+                    public void run() {
+                        genericObjectPool.returnObject(o1);
+                    }
+                }, 3000);
+
+        final String o2 = genericObjectPool.borrowObject();
+        assertEquals("0", o2);
+
+        assertEquals(1, simpleFactory.validateCounter);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testWhenExhaustedBlock() throws Exception {
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setMaxWaitMillis(10L);
+        final String obj1 = genericObjectPool.borrowObject();
+        assertNotNull(obj1);
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+        genericObjectPool.returnObject(obj1);
+        genericObjectPool.close();
+    }
+
+    /**
+     * POOL-189
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testWhenExhaustedBlockClosePool() throws Exception {
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setMaxWaitMillis(-1);
+        final Object obj1 = genericObjectPool.borrowObject();
+
+        // Make sure an object was obtained
+        assertNotNull(obj1);
+
+        // Create a separate thread to try and borrow another object
+        final WaitingTestThread<TestException> wtt = new WaitingTestThread<>(genericObjectPool, 200);
+        wtt.start();
+        // Give wtt time to start
+        Thread.sleep(200);
+
+        // close the pool (Bug POOL-189)
+        genericObjectPool.close();
+
+        // Give interrupt time to take effect
+        Thread.sleep(200);
+
+        // Check thread was interrupted
+        assertTrue(wtt.thrown instanceof InterruptedException);
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testWhenExhaustedBlockInterrupt() throws Exception {
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setBlockWhenExhausted(true);
+        genericObjectPool.setMaxWaitMillis(-1);
+        final String obj1 = genericObjectPool.borrowObject();
+
+        // Make sure on object was obtained
+        assertNotNull(obj1);
+
+        // Create a separate thread to try and borrow another object
+        final WaitingTestThread<TestException> wtt = new WaitingTestThread<>(genericObjectPool, 200000);
+        wtt.start();
+        // Give wtt time to start
+        Thread.sleep(200);
+        wtt.interrupt();
+
+        // Give interrupt time to take effect
+        Thread.sleep(200);
+
+        // Check thread was interrupted
+        assertTrue(wtt.thrown instanceof InterruptedException);
+
+        // Return object to the pool
+        genericObjectPool.returnObject(obj1);
+
+        // Bug POOL-162 - check there is now an object in the pool
+        genericObjectPool.setMaxWaitMillis(10L);
+        String obj2 = null;
+        try {
+            obj2 = genericObjectPool.borrowObject();
+            assertNotNull(obj2);
+        } catch (final NoSuchElementException e) {
+            // Not expected
+            fail("NoSuchElementException not expected");
+        }
+        genericObjectPool.returnObject(obj2);
+        genericObjectPool.close();
+
+    }
+
+    @Test
+    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    public void testWhenExhaustedFail() throws Exception {
+        genericObjectPool.setMaxTotal(1);
+        genericObjectPool.setBlockWhenExhausted(false);
+        final String obj1 = genericObjectPool.borrowObject();
+        assertNotNull(obj1);
+        assertThrows(NoSuchElementException.class, () -> genericObjectPool.borrowObject());
+        genericObjectPool.returnObject(obj1);
+        assertEquals(1, genericObjectPool.getNumIdle());
+        genericObjectPool.close();
+    }
+
+}