aboutsummaryrefslogtreecommitdiff
path: root/src/test/java/org/apache/commons/lang3/concurrent
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/org/apache/commons/lang3/concurrent')
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java121
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java37
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java77
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java309
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/BasicThreadFactoryTest.java297
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializerTest.java107
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/CircuitBreakingExceptionTest.java92
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/ConcurrentUtilsTest.java496
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java133
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java414
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/FutureTasksTest.java41
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java56
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/MemoizerComputableTest.java107
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/MemoizerFunctionTest.java109
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java399
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java88
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/TimedSemaphoreTest.java553
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/UncheckedExecutionExceptionTest.java35
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java124
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/UncheckedTimeoutExceptionTest.java35
-rw-r--r--src/test/java/org/apache/commons/lang3/concurrent/locks/LockingVisitorsTest.java142
21 files changed, 3772 insertions, 0 deletions
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java
new file mode 100644
index 000000000..d0b1db398
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * <p>
+ * An abstract base class for tests of concrete {@code ConcurrentInitializer}
+ * implementations.
+ * </p>
+ * <p>
+ * This class provides some basic tests for initializer implementations. Derived
+ * class have to create a {@link ConcurrentInitializer} object on which the
+ * tests are executed.
+ * </p>
+ */
+public abstract class AbstractConcurrentInitializerTest extends AbstractLangTest {
+ /**
+ * Tests a simple invocation of the get() method.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException because the object under test may throw it.
+ */
+ @Test
+ public void testGet() throws ConcurrentException {
+ assertNotNull(createInitializer().get(), "No managed object");
+ }
+
+ /**
+ * Tests whether sequential get() invocations always return the same
+ * instance.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException because the object under test may throw it.
+ */
+ @Test
+ public void testGetMultipleTimes() throws ConcurrentException {
+ final ConcurrentInitializer<Object> initializer = createInitializer();
+ final Object obj = initializer.get();
+ for (int i = 0; i < 10; i++) {
+ assertEquals(obj, initializer.get(), "Got different object at " + i);
+ }
+ }
+
+ /**
+ * Tests whether get() can be invoked from multiple threads concurrently.
+ * Always the same object should be returned.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException because the object under test may throw it.
+ * @throws InterruptedException because the threading API my throw it.
+ */
+ @Test
+ public void testGetConcurrent() throws ConcurrentException,
+ InterruptedException {
+ final ConcurrentInitializer<Object> initializer = createInitializer();
+ final int threadCount = 20;
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ class GetThread extends Thread {
+ Object object;
+
+ @Override
+ public void run() {
+ try {
+ // wait until all threads are ready for maximum parallelism
+ startLatch.await();
+ // access the initializer
+ object = initializer.get();
+ } catch (final InterruptedException iex) {
+ // ignore
+ } catch (final ConcurrentException cex) {
+ object = cex;
+ }
+ }
+ }
+
+ final GetThread[] threads = new GetThread[threadCount];
+ for (int i = 0; i < threadCount; i++) {
+ threads[i] = new GetThread();
+ threads[i].start();
+ }
+
+ // fire all threads and wait until they are ready
+ startLatch.countDown();
+ for (final Thread t : threads) {
+ t.join();
+ }
+
+ // check results
+ final Object managedObject = initializer.get();
+ for (final GetThread t : threads) {
+ assertEquals(managedObject, t.object, "Wrong object");
+ }
+ }
+
+ /**
+ * Creates the {@link ConcurrentInitializer} object to be tested. This
+ * method is called whenever the test fixture needs to be obtained.
+ *
+ * @return the initializer object to be tested
+ */
+ protected abstract ConcurrentInitializer<Object> createInitializer();
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java
new file mode 100644
index 000000000..9b05a1ae3
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.lang3.concurrent;
+
+/**
+ * Test class for {@code AtomicInitializer}.
+ */
+public class AtomicInitializerTest extends AbstractConcurrentInitializerTest {
+ /**
+ * Returns the initializer to be tested.
+ *
+ * @return the {@code AtomicInitializer}
+ */
+ @Override
+ protected ConcurrentInitializer<Object> createInitializer() {
+ return new AtomicInitializer<Object>() {
+ @Override
+ protected Object initialize() {
+ return new Object();
+ }
+ };
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java
new file mode 100644
index 000000000..3352f99dd
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code AtomicSafeInitializer} which also serves as a simple example.
+ */
+public class AtomicSafeInitializerTest extends AbstractConcurrentInitializerTest {
+
+ /** The instance to be tested. */
+ private AtomicSafeInitializerTestImpl initializer;
+
+ @BeforeEach
+ public void setUp() {
+ initializer = new AtomicSafeInitializerTestImpl();
+ }
+
+ /**
+ * Returns the initializer to be tested.
+ *
+ * @return the {@code AtomicSafeInitializer} under test
+ */
+ @Override
+ protected ConcurrentInitializer<Object> createInitializer() {
+ return initializer;
+ }
+
+ /**
+ * Tests that initialize() is called only once.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException because {@link #testGetConcurrent()} may throw it
+ * @throws InterruptedException because {@link #testGetConcurrent()} may throw it
+ */
+ @Test
+ public void testNumberOfInitializeInvocations() throws ConcurrentException, InterruptedException {
+ testGetConcurrent();
+ assertEquals(1, initializer.initCounter.get(), "Wrong number of invocations");
+ }
+
+ /**
+ * A concrete test implementation of {@code AtomicSafeInitializer} which also serves as a simple example.
+ * <p>
+ * This implementation also counts the number of invocations of the initialize() method.
+ * </p>
+ */
+ private static class AtomicSafeInitializerTestImpl extends AtomicSafeInitializer<Object> {
+ /** A counter for initialize() invocations. */
+ final AtomicInteger initCounter = new AtomicInteger();
+
+ @Override
+ protected Object initialize() {
+ initCounter.incrementAndGet();
+ return new Object();
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java
new file mode 100644
index 000000000..d1a80da81
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.ThreadUtils;
+import org.junit.jupiter.api.Test;
+
+public class BackgroundInitializerTest extends AbstractLangTest {
+ /**
+ * Helper method for checking whether the initialize() method was correctly
+ * called. start() must already have been invoked.
+ *
+ * @param init the initializer to test
+ */
+ private void checkInitialize(final BackgroundInitializerTestImpl init) throws ConcurrentException {
+ final Integer result = init.get();
+ assertEquals(1, result.intValue(), "Wrong result");
+ assertEquals(1, init.initializeCalls, "Wrong number of invocations");
+ assertNotNull(init.getFuture(), "No future");
+ }
+
+ /**
+ * Tests whether initialize() is invoked.
+ */
+ @Test
+ public void testInitialize() throws ConcurrentException {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ init.start();
+ checkInitialize(init);
+ }
+
+ /**
+ * Tries to obtain the executor before start(). It should not have been
+ * initialized yet.
+ */
+ @Test
+ public void testGetActiveExecutorBeforeStart() {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ assertNull(init.getActiveExecutor(), "Got an executor");
+ }
+
+ /**
+ * Tests whether an external executor is correctly detected.
+ */
+ @Test
+ public void testGetActiveExecutorExternal() throws InterruptedException, ConcurrentException {
+ final ExecutorService exec = Executors.newSingleThreadExecutor();
+ try {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(
+ exec);
+ init.start();
+ assertSame(exec, init.getActiveExecutor(), "Wrong executor");
+ checkInitialize(init);
+ } finally {
+ exec.shutdown();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Tests getActiveExecutor() for a temporary executor.
+ */
+ @Test
+ public void testGetActiveExecutorTemp() throws ConcurrentException {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ init.start();
+ assertNotNull(init.getActiveExecutor(), "No active executor");
+ checkInitialize(init);
+ }
+
+ /**
+ * Tests the execution of the background task if a temporary executor has to
+ * be created.
+ */
+ @Test
+ public void testInitializeTempExecutor() throws ConcurrentException {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ assertTrue(init.start(), "Wrong result of start()");
+ checkInitialize(init);
+ assertTrue(init.getActiveExecutor().isShutdown(), "Executor not shutdown");
+ }
+
+ /**
+ * Tests whether an external executor can be set using the
+ * setExternalExecutor() method.
+ */
+ @Test
+ public void testSetExternalExecutor() throws ConcurrentException {
+ final ExecutorService exec = Executors.newCachedThreadPool();
+ try {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ init.setExternalExecutor(exec);
+ assertEquals(exec, init.getExternalExecutor(), "Wrong executor service");
+ assertTrue(init.start(), "Wrong result of start()");
+ assertSame(exec, init.getActiveExecutor(), "Wrong active executor");
+ checkInitialize(init);
+ assertFalse(exec.isShutdown(), "Executor was shutdown");
+ } finally {
+ exec.shutdown();
+ }
+ }
+
+ /**
+ * Tests that setting an executor after start() causes an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException because the test implementation may throw it
+ */
+ @Test
+ public void testSetExternalExecutorAfterStart() throws ConcurrentException, InterruptedException {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ init.start();
+ final ExecutorService exec = Executors.newSingleThreadExecutor();
+ try {
+ assertThrows(IllegalStateException.class, () -> init.setExternalExecutor(exec));
+ init.get();
+ } finally {
+ exec.shutdown();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Tests invoking start() multiple times. Only the first invocation should
+ * have an effect.
+ */
+ @Test
+ public void testStartMultipleTimes() throws ConcurrentException {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ assertTrue(init.start(), "Wrong result for start()");
+ for (int i = 0; i < 10; i++) {
+ assertFalse(init.start(), "Could start again");
+ }
+ checkInitialize(init);
+ }
+
+ /**
+ * Tests calling get() before start(). This should cause an exception.
+ */
+ @Test
+ public void testGetBeforeStart() {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ assertThrows(IllegalStateException.class, init::get);
+ }
+
+ /**
+ * Tests the get() method if background processing causes a runtime
+ * exception.
+ */
+ @Test
+ public void testGetRuntimeException() {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ final RuntimeException rex = new RuntimeException();
+ init.ex = rex;
+ init.start();
+ final Exception ex = assertThrows(Exception.class, init::get);
+ assertEquals(rex, ex, "Runtime exception not thrown");
+ }
+
+ /**
+ * Tests the get() method if background processing causes a checked
+ * exception.
+ */
+ @Test
+ public void testGetCheckedException() {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ final Exception ex = new Exception();
+ init.ex = ex;
+ init.start();
+ final ConcurrentException cex = assertThrows(ConcurrentException.class, init::get);
+ assertEquals(ex, cex.getCause(), "Exception not thrown");
+ }
+
+ /**
+ * Tests the get() method if waiting for the initialization is interrupted.
+ *
+ * @throws InterruptedException because we're making use of Java's concurrent API
+ */
+ @Test
+ public void testGetInterruptedException() throws InterruptedException {
+ final ExecutorService exec = Executors.newSingleThreadExecutor();
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(
+ exec);
+ final CountDownLatch latch1 = new CountDownLatch(1);
+ init.shouldSleep = true;
+ init.start();
+ final AtomicReference<InterruptedException> iex = new AtomicReference<>();
+ final Thread getThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ init.get();
+ } catch (final ConcurrentException cex) {
+ if (cex.getCause() instanceof InterruptedException) {
+ iex.set((InterruptedException) cex.getCause());
+ }
+ } finally {
+ assertTrue(isInterrupted(), "Thread not interrupted");
+ latch1.countDown();
+ }
+ }
+ };
+ getThread.start();
+ getThread.interrupt();
+ latch1.await();
+ exec.shutdownNow();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ assertNotNull(iex.get(), "No interrupted exception");
+ }
+
+ /**
+ * Tests isStarted() before start() was called.
+ */
+ @Test
+ public void testIsStartedFalse() {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ assertFalse(init.isStarted(), "Already started");
+ }
+
+ /**
+ * Tests isStarted() after start().
+ */
+ @Test
+ public void testIsStartedTrue() {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ init.start();
+ assertTrue(init.isStarted(), "Not started");
+ }
+
+ /**
+ * Tests isStarted() after the background task has finished.
+ */
+ @Test
+ public void testIsStartedAfterGet() throws ConcurrentException {
+ final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+ init.start();
+ checkInitialize(init);
+ assertTrue(init.isStarted(), "Not started");
+ }
+
+ /**
+ * A concrete implementation of BackgroundInitializer. It also overloads
+ * some methods that simplify testing.
+ */
+ private static class BackgroundInitializerTestImpl extends
+ BackgroundInitializer<Integer> {
+ /** An exception to be thrown by initialize(). */
+ Exception ex;
+
+ /** A flag whether the background task should sleep a while. */
+ boolean shouldSleep;
+
+ /** The number of invocations of initialize(). */
+ volatile int initializeCalls;
+
+ BackgroundInitializerTestImpl() {
+ }
+
+ BackgroundInitializerTestImpl(final ExecutorService exec) {
+ super(exec);
+ }
+
+ /**
+ * Records this invocation. Optionally throws an exception or sleeps a
+ * while.
+ *
+ * @throws Exception in case of an error
+ */
+ @Override
+ protected Integer initialize() throws Exception {
+ if (ex != null) {
+ throw ex;
+ }
+ if (shouldSleep) {
+ ThreadUtils.sleep(Duration.ofMinutes(1));
+ }
+ return Integer.valueOf(++initializeCalls);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BasicThreadFactoryTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BasicThreadFactoryTest.java
new file mode 100644
index 000000000..db32e39b6
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/BasicThreadFactoryTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.ThreadFactory;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code BasicThreadFactory}.
+ */
+public class BasicThreadFactoryTest extends AbstractLangTest {
+ /** Constant for the test naming pattern. */
+ private static final String PATTERN = "testThread-%d";
+
+ /** The builder for creating a thread factory. */
+ private BasicThreadFactory.Builder builder;
+
+ @BeforeEach
+ public void setUp() {
+ builder = new BasicThreadFactory.Builder();
+ }
+
+ /**
+ * Tests the default options of a thread factory.
+ *
+ * @param factory the factory to be checked
+ */
+ private void checkFactoryDefaults(final BasicThreadFactory factory) {
+ assertNull(factory.getNamingPattern(), "Got a naming pattern");
+ assertNull(factory.getUncaughtExceptionHandler(), "Got an exception handler");
+ assertNull(factory.getPriority(), "Got a priority");
+ assertNull(factory.getDaemonFlag(), "Got a daemon flag");
+ assertNotNull(factory.getWrappedFactory(), "No wrapped factory");
+ }
+
+ /**
+ * Tests the default values used by the builder.
+ */
+ @Test
+ public void testBuildDefaults() {
+ final BasicThreadFactory factory = builder.build();
+ checkFactoryDefaults(factory);
+ }
+
+ /**
+ * Tries to set a null naming pattern.
+ */
+ @Test
+ public void testBuildNamingPatternNull() {
+ assertThrows(NullPointerException.class, () -> builder.namingPattern(null));
+ }
+
+ /**
+ * Tries to set a null wrapped factory.
+ */
+ @Test
+ public void testBuildWrappedFactoryNull() {
+ assertThrows(NullPointerException.class, () -> builder.wrappedFactory(null));
+ }
+
+ /**
+ * Tries to set a null exception handler.
+ */
+ @Test
+ public void testBuildUncaughtExceptionHandlerNull() {
+ assertThrows(NullPointerException.class, () -> builder.uncaughtExceptionHandler(null));
+ }
+
+ /**
+ * Tests the reset() method of the builder.
+ */
+ @Test
+ public void testBuilderReset() {
+ final ThreadFactory wrappedFactory = EasyMock.createMock(ThreadFactory.class);
+ final Thread.UncaughtExceptionHandler exHandler = EasyMock
+ .createMock(Thread.UncaughtExceptionHandler.class);
+ EasyMock.replay(wrappedFactory, exHandler);
+ builder.namingPattern(PATTERN).daemon(true).priority(
+ Thread.MAX_PRIORITY).uncaughtExceptionHandler(exHandler)
+ .wrappedFactory(wrappedFactory);
+ builder.reset();
+ final BasicThreadFactory factory = builder.build();
+ checkFactoryDefaults(factory);
+ assertNotSame(wrappedFactory, factory.getWrappedFactory(), "Wrapped factory not reset");
+ EasyMock.verify(wrappedFactory, exHandler);
+ }
+
+ /**
+ * Tests whether reset() is automatically called after build().
+ */
+ @Test
+ public void testBuilderResetAfterBuild() {
+ builder.wrappedFactory(EasyMock.createNiceMock(ThreadFactory.class))
+ .namingPattern(PATTERN).daemon(true).build();
+ checkFactoryDefaults(builder.build());
+ }
+
+ /**
+ * Tests whether the naming pattern is applied to new threads.
+ */
+ @Test
+ public void testNewThreadNamingPattern() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final int count = 12;
+ for (int i = 0; i < count; i++) {
+ EasyMock.expect(wrapped.newThread(r)).andReturn(new Thread());
+ }
+ EasyMock.replay(wrapped, r);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped)
+ .namingPattern(PATTERN).build();
+ for (int i = 0; i < count; i++) {
+ final Thread t = factory.newThread(r);
+ assertEquals(String.format(PATTERN, Long.valueOf(i + 1)), t.getName(), "Wrong thread name");
+ assertEquals(i + 1, factory.getThreadCount(), "Wrong thread count");
+ }
+ EasyMock.verify(wrapped, r);
+ }
+
+ /**
+ * Tests whether the thread name is not modified if no naming pattern is
+ * set.
+ */
+ @Test
+ public void testNewThreadNoNamingPattern() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final String name = "unchangedThreadName";
+ final Thread t = new Thread(name);
+ EasyMock.expect(wrapped.newThread(r)).andReturn(t);
+ EasyMock.replay(wrapped, r);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped).build();
+ assertSame(t, factory.newThread(r), "Wrong thread");
+ assertEquals(name, t.getName(), "Name was changed");
+ EasyMock.verify(wrapped, r);
+ }
+
+ /**
+ * Helper method for testing whether the daemon flag is taken into account.
+ *
+ * @param flag the value of the flag
+ */
+ private void checkDaemonFlag(final boolean flag) {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final Thread t = new Thread();
+ EasyMock.expect(wrapped.newThread(r)).andReturn(t);
+ EasyMock.replay(wrapped, r);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped).daemon(
+ flag).build();
+ assertSame(t, factory.newThread(r), "Wrong thread");
+ assertEquals(flag, t.isDaemon(), "Wrong daemon flag");
+ EasyMock.verify(wrapped, r);
+ }
+
+ /**
+ * Tests whether daemon threads can be created.
+ */
+ @Test
+ public void testNewThreadDaemonTrue() {
+ checkDaemonFlag(true);
+ }
+
+ /**
+ * Tests whether the daemon status of new threads can be turned off.
+ */
+ @Test
+ public void testNewThreadDaemonFalse() {
+ checkDaemonFlag(false);
+ }
+
+ /**
+ * Tests whether the daemon flag is not touched on newly created threads if
+ * it is not specified.
+ */
+ @Test
+ public void testNewThreadNoDaemonFlag() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r1 = EasyMock.createMock(Runnable.class);
+ final Runnable r2 = EasyMock.createMock(Runnable.class);
+ final Thread t1 = new Thread();
+ final Thread t2 = new Thread();
+ t1.setDaemon(true);
+ EasyMock.expect(wrapped.newThread(r1)).andReturn(t1);
+ EasyMock.expect(wrapped.newThread(r2)).andReturn(t2);
+ EasyMock.replay(wrapped, r1, r2);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped).build();
+ assertSame(t1, factory.newThread(r1), "Wrong thread 1");
+ assertTrue(t1.isDaemon(), "No daemon thread");
+ assertSame(t2, factory.newThread(r2), "Wrong thread 2");
+ assertFalse(t2.isDaemon(), "A daemon thread");
+ EasyMock.verify(wrapped, r1, r2);
+ }
+
+ /**
+ * Tests whether the priority is set on newly created threads.
+ */
+ @Test
+ public void testNewThreadPriority() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final Thread t = new Thread();
+ EasyMock.expect(wrapped.newThread(r)).andReturn(t);
+ EasyMock.replay(wrapped, r);
+ final int priority = Thread.NORM_PRIORITY + 1;
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped).priority(
+ priority).build();
+ assertSame(t, factory.newThread(r), "Wrong thread");
+ assertEquals(priority, t.getPriority(), "Wrong priority");
+ EasyMock.verify(wrapped, r);
+ }
+
+ /**
+ * Tests whether the original priority is not changed if no priority is
+ * specified.
+ */
+ @Test
+ public void testNewThreadNoPriority() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final int orgPriority = Thread.NORM_PRIORITY + 1;
+ final Thread t = new Thread();
+ t.setPriority(orgPriority);
+ EasyMock.expect(wrapped.newThread(r)).andReturn(t);
+ EasyMock.replay(wrapped, r);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped).build();
+ assertSame(t, factory.newThread(r), "Wrong thread");
+ assertEquals(orgPriority, t.getPriority(), "Wrong priority");
+ EasyMock.verify(wrapped, r);
+ }
+
+ /**
+ * Tests whether the exception handler is set if one is provided.
+ */
+ @Test
+ public void testNewThreadExHandler() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final Thread.UncaughtExceptionHandler handler = EasyMock
+ .createMock(Thread.UncaughtExceptionHandler.class);
+ final Thread t = new Thread();
+ EasyMock.expect(wrapped.newThread(r)).andReturn(t);
+ EasyMock.replay(wrapped, r, handler);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped)
+ .uncaughtExceptionHandler(handler).build();
+ assertSame(t, factory.newThread(r), "Wrong thread");
+ assertEquals(handler, t.getUncaughtExceptionHandler(), "Wrong exception handler");
+ EasyMock.verify(wrapped, r, handler);
+ }
+
+ /**
+ * Tests whether the original exception handler is not touched if none is
+ * specified.
+ */
+ @Test
+ public void testNewThreadNoExHandler() {
+ final ThreadFactory wrapped = EasyMock.createMock(ThreadFactory.class);
+ final Runnable r = EasyMock.createMock(Runnable.class);
+ final Thread.UncaughtExceptionHandler handler = EasyMock
+ .createMock(Thread.UncaughtExceptionHandler.class);
+ final Thread t = new Thread();
+ t.setUncaughtExceptionHandler(handler);
+ EasyMock.expect(wrapped.newThread(r)).andReturn(t);
+ EasyMock.replay(wrapped, r, handler);
+ final BasicThreadFactory factory = builder.wrappedFactory(wrapped).build();
+ assertSame(t, factory.newThread(r), "Wrong thread");
+ assertEquals(handler, t.getUncaughtExceptionHandler(), "Wrong exception handler");
+ EasyMock.verify(wrapped, r, handler);
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializerTest.java
new file mode 100644
index 000000000..2a2e724ef
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializerTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code CallableBackgroundInitializer}
+ */
+public class CallableBackgroundInitializerTest extends AbstractLangTest {
+ /** Constant for the result of the call() invocation. */
+ private static final Integer RESULT = Integer.valueOf(42);
+
+ /**
+ * Tries to create an instance without a Callable. This should cause an
+ * exception.
+ */
+ @Test()
+ public void testInitNullCallable() {
+ assertThrows(NullPointerException.class, () -> new CallableBackgroundInitializer<>(null));
+ }
+
+ /**
+ * Tests whether the executor service is correctly passed to the super
+ * class.
+ */
+ @Test
+ public void testInitExecutor() throws InterruptedException {
+ final ExecutorService exec = Executors.newSingleThreadExecutor();
+ final CallableBackgroundInitializer<Integer> init = new CallableBackgroundInitializer<>(
+ new TestCallable(), exec);
+ assertEquals(exec, init.getExternalExecutor(), "Executor not set");
+ exec.shutdown();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Tries to pass a null Callable to the constructor that takes an executor.
+ * This should cause an exception.
+ */
+ @Test
+ public void testInitExecutorNullCallable() throws InterruptedException {
+ final ExecutorService exec = Executors.newSingleThreadExecutor();
+ try {
+ assertThrows(NullPointerException.class, () -> new CallableBackgroundInitializer<Integer>(null, exec));
+ } finally {
+ exec.shutdown();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ }
+
+ }
+
+ /**
+ * Tests the implementation of initialize().
+ *
+ * @throws Exception so we don't have to catch it
+ */
+ @Test
+ public void testInitialize() throws Exception {
+ final TestCallable call = new TestCallable();
+ final CallableBackgroundInitializer<Integer> init = new CallableBackgroundInitializer<>(
+ call);
+ assertEquals(RESULT, init.initialize(), "Wrong result");
+ assertEquals(1, call.callCount, "Wrong number of invocations");
+ }
+
+ /**
+ * A test Callable implementation for checking the initializer's
+ * implementation of the initialize() method.
+ */
+ private static class TestCallable implements Callable<Integer> {
+ /** A counter for the number of call() invocations. */
+ int callCount;
+
+ /**
+ * Records this invocation and returns the test result.
+ */
+ @Override
+ public Integer call() {
+ callCount++;
+ return RESULT;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/CircuitBreakingExceptionTest.java b/src/test/java/org/apache/commons/lang3/concurrent/CircuitBreakingExceptionTest.java
new file mode 100644
index 000000000..bf19b49c4
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/CircuitBreakingExceptionTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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 org.apache.commons.lang3.exception.AbstractExceptionTest;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * JUnit tests for {@link CircuitBreakingException}.
+ */
+public class CircuitBreakingExceptionTest extends AbstractExceptionTest {
+
+ @Test
+ public void testThrowingInformativeException() {
+ assertThrows(CircuitBreakingException.class, () -> {
+ throw new CircuitBreakingException(EXCEPTION_MESSAGE, generateCause());
+ });
+ }
+
+ @Test
+ public void testThrowingExceptionWithMessage() {
+ assertThrows(CircuitBreakingException.class, () -> {
+ throw new CircuitBreakingException(EXCEPTION_MESSAGE);
+ });
+ }
+
+ @Test
+ public void testThrowingExceptionWithCause() {
+ assertThrows(CircuitBreakingException.class, () -> {
+ throw new CircuitBreakingException(generateCause());
+ });
+ }
+
+ @Test
+ public void testThrowingEmptyException() {
+ assertThrows(CircuitBreakingException.class, () -> {
+ throw new CircuitBreakingException();
+ });
+ }
+
+ @Test
+ public void testWithCauseAndMessage() {
+ final Exception exception = new CircuitBreakingException(EXCEPTION_MESSAGE, generateCause());
+ assertNotNull(exception);
+ assertEquals(EXCEPTION_MESSAGE, exception.getMessage(), WRONG_EXCEPTION_MESSAGE);
+
+ final Throwable cause = exception.getCause();
+ assertNotNull(cause);
+ assertEquals(CAUSE_MESSAGE, cause.getMessage(), WRONG_CAUSE_MESSAGE);
+ }
+
+ @Test
+ public void testWithoutCause() {
+ final Exception exception = new CircuitBreakingException(EXCEPTION_MESSAGE);
+ assertNotNull(exception);
+ assertEquals(EXCEPTION_MESSAGE, exception.getMessage(), WRONG_EXCEPTION_MESSAGE);
+
+ final Throwable cause = exception.getCause();
+ assertNull(cause);
+ }
+
+ @Test
+ public void testWithoutMessage() {
+ final Exception exception = new CircuitBreakingException(generateCause());
+ assertNotNull(exception);
+ assertNotNull(exception.getMessage());
+
+ final Throwable cause = exception.getCause();
+ assertNotNull(cause);
+ assertEquals(CAUSE_MESSAGE, cause.getMessage(), WRONG_CAUSE_MESSAGE);
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/ConcurrentUtilsTest.java b/src/test/java/org/apache/commons/lang3/concurrent/ConcurrentUtilsTest.java
new file mode 100644
index 000000000..831c33349
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/ConcurrentUtilsTest.java
@@ -0,0 +1,496 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@link ConcurrentUtils}.
+ */
+public class ConcurrentUtilsTest extends AbstractLangTest {
+ /**
+ * Tests creating a ConcurrentException with a runtime exception as cause.
+ */
+ @Test
+ public void testConcurrentExceptionCauseUnchecked() {
+ assertThrows(IllegalArgumentException.class, () -> new ConcurrentException(new RuntimeException()));
+ }
+
+ /**
+ * Tests creating a ConcurrentException with an error as cause.
+ */
+ @Test
+ public void testConcurrentExceptionCauseError() {
+ assertThrows(IllegalArgumentException.class, () -> new ConcurrentException("An error", new Error()));
+ }
+
+ /**
+ * Tests creating a ConcurrentException with null as cause.
+ */
+ @Test
+ public void testConcurrentExceptionCauseNull() {
+ assertThrows(IllegalArgumentException.class, () -> new ConcurrentException(null));
+ }
+
+ /**
+ * Tries to create a ConcurrentRuntimeException with a runtime as cause.
+ */
+ @Test
+ public void testConcurrentRuntimeExceptionCauseUnchecked() {
+ assertThrows(IllegalArgumentException.class, () -> new ConcurrentRuntimeException(new RuntimeException()));
+ }
+
+ /**
+ * Tries to create a ConcurrentRuntimeException with an error as cause.
+ */
+ @Test
+ public void testConcurrentRuntimeExceptionCauseError() {
+ assertThrows(IllegalArgumentException.class, () -> new ConcurrentRuntimeException("An error", new Error()));
+ }
+
+ /**
+ * Tries to create a ConcurrentRuntimeException with null as cause.
+ */
+ @Test
+ public void testConcurrentRuntimeExceptionCauseNull() {
+ assertThrows(IllegalArgumentException.class, () -> new ConcurrentRuntimeException(null));
+ }
+
+ /**
+ * Tests extractCause() for a null exception.
+ */
+ @Test
+ public void testExtractCauseNull() {
+ assertNull(ConcurrentUtils.extractCause(null), "Non null result");
+ }
+
+ /**
+ * Tests extractCause() if the cause of the passed in exception is null.
+ */
+ @Test
+ public void testExtractCauseNullCause() {
+ assertNull(ConcurrentUtils.extractCause(new ExecutionException("Test", null)), "Non null result");
+ }
+
+ /**
+ * Tests extractCause() if the cause is an error.
+ */
+ @Test
+ public void testExtractCauseError() {
+ final Error err = new AssertionError("Test");
+ final AssertionError e = assertThrows(AssertionError.class, () -> ConcurrentUtils.extractCause(new ExecutionException(err)));
+ assertEquals(err, e, "Wrong error");
+ }
+
+ /**
+ * Tests extractCause() if the cause is an unchecked exception.
+ */
+ @Test
+ public void testExtractCauseUncheckedException() {
+ final RuntimeException rex = new RuntimeException("Test");
+ assertThrows(RuntimeException.class, () -> ConcurrentUtils.extractCause(new ExecutionException(rex)));
+ }
+
+ /**
+ * Tests extractCause() if the cause is a checked exception.
+ */
+ @Test
+ public void testExtractCauseChecked() {
+ final Exception ex = new Exception("Test");
+ final ConcurrentException cex = ConcurrentUtils.extractCause(new ExecutionException(ex));
+ assertSame(ex, cex.getCause(), "Wrong cause");
+ }
+
+ /**
+ * Tests extractCauseUnchecked() for a null exception.
+ */
+ @Test
+ public void testExtractCauseUncheckedNull() {
+ assertNull(ConcurrentUtils.extractCauseUnchecked(null), "Non null result");
+ }
+
+ /**
+ * Tests extractCauseUnchecked() if the cause of the passed in exception is null.
+ */
+ @Test
+ public void testExtractCauseUncheckedNullCause() {
+ assertNull(ConcurrentUtils.extractCauseUnchecked(new ExecutionException("Test", null)), "Non null result");
+ }
+
+ /**
+ * Tests extractCauseUnchecked() if the cause is an error.
+ */
+ @Test
+ public void testExtractCauseUncheckedError() {
+ final Error err = new AssertionError("Test");
+ final Error e = assertThrows(Error.class, () -> ConcurrentUtils.extractCauseUnchecked(new ExecutionException(err)));
+ assertEquals(err, e, "Wrong error");
+ }
+
+ /**
+ * Tests extractCauseUnchecked() if the cause is an unchecked exception.
+ */
+ @Test
+ public void testExtractCauseUncheckedUncheckedException() {
+ final RuntimeException rex = new RuntimeException("Test");
+ final RuntimeException r = assertThrows(RuntimeException.class, () -> ConcurrentUtils.extractCauseUnchecked(new ExecutionException(rex)));
+ assertEquals(rex, r, "Wrong exception");
+ }
+
+ /**
+ * Tests extractCauseUnchecked() if the cause is a checked exception.
+ */
+ @Test
+ public void testExtractCauseUncheckedChecked() {
+ final Exception ex = new Exception("Test");
+ final ConcurrentRuntimeException cex = ConcurrentUtils.extractCauseUnchecked(new ExecutionException(ex));
+ assertSame(ex, cex.getCause(), "Wrong cause");
+ }
+
+ /**
+ * Tests handleCause() if the cause is an error.
+ */
+ @Test
+ public void testHandleCauseError() {
+ final Error err = new AssertionError("Test");
+ final Error e = assertThrows(Error.class, () -> ConcurrentUtils.handleCause(new ExecutionException(err)));
+ assertEquals(err, e, "Wrong error");
+ }
+
+ /**
+ * Tests handleCause() if the cause is an unchecked exception.
+ */
+ @Test
+ public void testHandleCauseUncheckedException() {
+ final RuntimeException rex = new RuntimeException("Test");
+ final RuntimeException r = assertThrows(RuntimeException.class, () -> ConcurrentUtils.handleCause(new ExecutionException(rex)));
+ assertEquals(rex, r, "Wrong exception");
+ }
+
+ /**
+ * Tests handleCause() if the cause is a checked exception.
+ */
+ @Test
+ public void testHandleCauseChecked() {
+ final Exception ex = new Exception("Test");
+ final ConcurrentException cex = assertThrows(ConcurrentException.class, () -> ConcurrentUtils.handleCause(new ExecutionException(ex)));
+ assertEquals(ex, cex.getCause(), "Wrong cause");
+ }
+
+ /**
+ * Tests handleCause() for a null parameter or a null cause. In this case the method should do nothing. We can only test
+ * that no exception is thrown.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testHandleCauseNull() throws ConcurrentException {
+ ConcurrentUtils.handleCause(null);
+ ConcurrentUtils.handleCause(new ExecutionException("Test", null));
+ }
+
+ /**
+ * Tests handleCauseUnchecked() if the cause is an error.
+ */
+ @Test
+ public void testHandleCauseUncheckedError() {
+ final Error err = new AssertionError("Test");
+ final Error e = assertThrows(Error.class, () -> ConcurrentUtils.handleCauseUnchecked(new ExecutionException(err)));
+ assertEquals(err, e, "Wrong error");
+ }
+
+ /**
+ * Tests handleCauseUnchecked() if the cause is an unchecked exception.
+ */
+ @Test
+ public void testHandleCauseUncheckedUncheckedException() {
+ final RuntimeException rex = new RuntimeException("Test");
+ final RuntimeException r = assertThrows(RuntimeException.class, () -> ConcurrentUtils.handleCauseUnchecked(new ExecutionException(rex)));
+ assertEquals(rex, r, "Wrong exception");
+ }
+
+ /**
+ * Tests handleCauseUnchecked() if the cause is a checked exception.
+ */
+ @Test
+ public void testHandleCauseUncheckedChecked() {
+ final Exception ex = new Exception("Test");
+ final ConcurrentRuntimeException crex = assertThrows(ConcurrentRuntimeException.class,
+ () -> ConcurrentUtils.handleCauseUnchecked(new ExecutionException(ex)));
+ assertEquals(ex, crex.getCause(), "Wrong cause");
+ }
+
+ /**
+ * Tests handleCauseUnchecked() for a null parameter or a null cause. In this case the method should do nothing. We can
+ * only test that no exception is thrown.
+ */
+ @Test
+ public void testHandleCauseUncheckedNull() {
+ ConcurrentUtils.handleCauseUnchecked(null);
+ ConcurrentUtils.handleCauseUnchecked(new ExecutionException("Test", null));
+ }
+
+ /**
+ * Tests initialize() for a null argument.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeNull() throws ConcurrentException {
+ assertNull(ConcurrentUtils.initialize(null), "Got a result");
+ }
+
+ /**
+ * Tests a successful initialize() operation.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitialize() throws ConcurrentException {
+ final ConcurrentInitializer<Object> init = EasyMock.createMock(ConcurrentInitializer.class);
+ final Object result = new Object();
+ EasyMock.expect(init.get()).andReturn(result);
+ EasyMock.replay(init);
+ assertSame(result, ConcurrentUtils.initialize(init), "Wrong result object");
+ EasyMock.verify(init);
+ }
+
+ /**
+ * Tests initializeUnchecked() for a null argument.
+ */
+ @Test
+ public void testInitializeUncheckedNull() {
+ assertNull(ConcurrentUtils.initializeUnchecked(null), "Got a result");
+ }
+
+ /**
+ * Tests creating ConcurrentRuntimeException with no arguments.
+ */
+ @Test
+ public void testUninitializedConcurrentRuntimeException() {
+ assertNotNull(new ConcurrentRuntimeException(), "Error creating empty ConcurrentRuntimeException");
+ }
+
+ /**
+ * Tests a successful initializeUnchecked() operation.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeUnchecked() throws ConcurrentException {
+ final ConcurrentInitializer<Object> init = EasyMock.createMock(ConcurrentInitializer.class);
+ final Object result = new Object();
+ EasyMock.expect(init.get()).andReturn(result);
+ EasyMock.replay(init);
+ assertSame(result, ConcurrentUtils.initializeUnchecked(init), "Wrong result object");
+ EasyMock.verify(init);
+ }
+
+ /**
+ * Tests whether exceptions are correctly handled by initializeUnchecked().
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeUncheckedEx() throws ConcurrentException {
+ final ConcurrentInitializer<Object> init = EasyMock.createMock(ConcurrentInitializer.class);
+ final Exception cause = new Exception();
+ EasyMock.expect(init.get()).andThrow(new ConcurrentException(cause));
+ EasyMock.replay(init);
+ final ConcurrentRuntimeException crex = assertThrows(ConcurrentRuntimeException.class, () -> ConcurrentUtils.initializeUnchecked(init));
+ assertSame(cause, crex.getCause(), "Wrong cause");
+ EasyMock.verify(init);
+ }
+
+ /**
+ * Tests constant future.
+ *
+ * @throws Exception so we don't have to catch it
+ */
+ @Test
+ public void testConstantFuture_Integer() throws Exception {
+ final Integer value = Integer.valueOf(5);
+ final Future<Integer> test = ConcurrentUtils.constantFuture(value);
+ assertTrue(test.isDone());
+ assertSame(value, test.get());
+ assertSame(value, test.get(1000, TimeUnit.SECONDS));
+ assertSame(value, test.get(1000, null));
+ assertFalse(test.isCancelled());
+ assertFalse(test.cancel(true));
+ assertFalse(test.cancel(false));
+ }
+
+ /**
+ * Tests constant future.
+ *
+ * @throws Exception so we don't have to catch it
+ */
+ @Test
+ public void testConstantFuture_null() throws Exception {
+ final Integer value = null;
+ final Future<Integer> test = ConcurrentUtils.constantFuture(value);
+ assertTrue(test.isDone());
+ assertSame(value, test.get());
+ assertSame(value, test.get(1000, TimeUnit.SECONDS));
+ assertSame(value, test.get(1000, null));
+ assertFalse(test.isCancelled());
+ assertFalse(test.cancel(true));
+ assertFalse(test.cancel(false));
+ }
+
+ /**
+ * Tests putIfAbsent() if the map contains the key in question.
+ */
+ @Test
+ public void testPutIfAbsentKeyPresent() {
+ final String key = "testKey";
+ final Integer value = 42;
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ map.put(key, value);
+ assertEquals(value, ConcurrentUtils.putIfAbsent(map, key, 0), "Wrong result");
+ assertEquals(value, map.get(key), "Wrong value in map");
+ }
+
+ /**
+ * Tests putIfAbsent() if the map does not contain the key in question.
+ */
+ @Test
+ public void testPutIfAbsentKeyNotPresent() {
+ final String key = "testKey";
+ final Integer value = 42;
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ assertEquals(value, ConcurrentUtils.putIfAbsent(map, key, value), "Wrong result");
+ assertEquals(value, map.get(key), "Wrong value in map");
+ }
+
+ /**
+ * Tests putIfAbsent() if a null map is passed in.
+ */
+ @Test
+ public void testPutIfAbsentNullMap() {
+ assertNull(ConcurrentUtils.putIfAbsent(null, "test", 100), "Wrong result");
+ }
+
+ /**
+ * Tests createIfAbsent() if the key is found in the map.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testCreateIfAbsentKeyPresent() throws ConcurrentException {
+ final ConcurrentInitializer<Integer> init = EasyMock.createMock(ConcurrentInitializer.class);
+ EasyMock.replay(init);
+ final String key = "testKey";
+ final Integer value = 42;
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ map.put(key, value);
+ assertEquals(value, ConcurrentUtils.createIfAbsent(map, key, init), "Wrong result");
+ assertEquals(value, map.get(key), "Wrong value in map");
+ EasyMock.verify(init);
+ }
+
+ /**
+ * Tests createIfAbsent() if the map does not contain the key in question.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testCreateIfAbsentKeyNotPresent() throws ConcurrentException {
+ final ConcurrentInitializer<Integer> init = EasyMock.createMock(ConcurrentInitializer.class);
+ final String key = "testKey";
+ final Integer value = 42;
+ EasyMock.expect(init.get()).andReturn(value);
+ EasyMock.replay(init);
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ assertEquals(value, ConcurrentUtils.createIfAbsent(map, key, init), "Wrong result");
+ assertEquals(value, map.get(key), "Wrong value in map");
+ EasyMock.verify(init);
+ }
+
+ /**
+ * Tests createIfAbsent() if a null map is passed in.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testCreateIfAbsentNullMap() throws ConcurrentException {
+ final ConcurrentInitializer<Integer> init = EasyMock.createMock(ConcurrentInitializer.class);
+ EasyMock.replay(init);
+ assertNull(ConcurrentUtils.createIfAbsent(null, "test", init), "Wrong result");
+ EasyMock.verify(init);
+ }
+
+ /**
+ * Tests createIfAbsent() if a null initializer is passed in.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testCreateIfAbsentNullInit() throws ConcurrentException {
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ final String key = "testKey";
+ final Integer value = 42;
+ map.put(key, value);
+ assertNull(ConcurrentUtils.createIfAbsent(map, key, null), "Wrong result");
+ assertEquals(value, map.get(key), "Map was changed");
+ }
+
+ /**
+ * Tests createIfAbsentUnchecked() if no exception is thrown.
+ */
+ @Test
+ public void testCreateIfAbsentUncheckedSuccess() {
+ final String key = "testKey";
+ final Integer value = 42;
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ assertEquals(value, ConcurrentUtils.createIfAbsentUnchecked(map, key, new ConstantInitializer<>(value)), "Wrong result");
+ assertEquals(value, map.get(key), "Wrong value in map");
+ }
+
+ /**
+ * Tests createIfAbsentUnchecked() if an exception is thrown.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testCreateIfAbsentUncheckedException() throws ConcurrentException {
+ final ConcurrentInitializer<Integer> init = EasyMock.createMock(ConcurrentInitializer.class);
+ final Exception ex = new Exception();
+ EasyMock.expect(init.get()).andThrow(new ConcurrentException(ex));
+ EasyMock.replay(init);
+ final ConcurrentRuntimeException crex = assertThrows(ConcurrentRuntimeException.class,
+ () -> ConcurrentUtils.createIfAbsentUnchecked(new ConcurrentHashMap<>(), "test", init));
+ assertEquals(ex, crex.getCause(), "Wrong cause");
+ EasyMock.verify(init);
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java
new file mode 100644
index 000000000..c296f056f
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code ConstantInitializer}.
+ */
+public class ConstantInitializerTest extends AbstractLangTest {
+ /** Constant for the object managed by the initializer. */
+ private static final Integer VALUE = 42;
+
+ /** The initializer to be tested. */
+ private ConstantInitializer<Integer> init;
+
+ @BeforeEach
+ public void setUp() {
+ init = new ConstantInitializer<>(VALUE);
+ }
+
+ /**
+ * Helper method for testing equals() and hashCode().
+ *
+ * @param obj the object to compare with the test instance
+ * @param expected the expected result
+ */
+ private void checkEquals(final Object obj, final boolean expected) {
+ assertEquals(expected, init.equals(obj), "Wrong result of equals");
+ if (obj != null) {
+ assertEquals(expected, obj.equals(init), "Not symmetric");
+ if (expected) {
+ assertEquals(init.hashCode(), obj.hashCode(), "Different hash codes");
+ }
+ }
+ }
+
+ /**
+ * Tests whether the correct object is returned.
+ */
+ @Test
+ public void testGetObject() {
+ assertEquals(VALUE, init.getObject(), "Wrong object");
+ }
+
+ /**
+ * Tests whether get() returns the correct object.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testGet() throws ConcurrentException {
+ assertEquals(VALUE, init.get(), "Wrong object");
+ }
+
+ /**
+ * Tests equals() if the expected result is true.
+ */
+ @Test
+ public void testEqualsTrue() {
+ checkEquals(init, true);
+ ConstantInitializer<Integer> init2 = new ConstantInitializer<>(
+ Integer.valueOf(VALUE.intValue()));
+ checkEquals(init2, true);
+ init = new ConstantInitializer<>(null);
+ init2 = new ConstantInitializer<>(null);
+ checkEquals(init2, true);
+ }
+
+ /**
+ * Tests equals() if the expected result is false.
+ */
+ @Test
+ public void testEqualsFalse() {
+ ConstantInitializer<Integer> init2 = new ConstantInitializer<>(
+ null);
+ checkEquals(init2, false);
+ init2 = new ConstantInitializer<>(VALUE + 1);
+ checkEquals(init2, false);
+ }
+
+ /**
+ * Tests equals() with objects of other classes.
+ */
+ @Test
+ public void testEqualsWithOtherObjects() {
+ checkEquals(null, false);
+ checkEquals(this, false);
+ checkEquals(new ConstantInitializer<>("Test"), false);
+ }
+
+ /**
+ * Tests the string representation.
+ */
+ @Test
+ public void testToString() {
+ final String s = init.toString();
+ final Pattern pattern = Pattern
+ .compile("ConstantInitializer@-?\\d+ \\[ object = " + VALUE
+ + " \\]");
+ assertTrue(pattern.matcher(s).matches(), "Wrong string: " + s);
+ }
+
+ /**
+ * Tests the string representation if the managed object is null.
+ */
+ @Test
+ public void testToStringNull() {
+ final String s = new ConstantInitializer<>(null).toString();
+ assertTrue(s.indexOf("object = null") > 0, "Object not found: " + s);
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java
new file mode 100644
index 000000000..ef9bf2561
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java
@@ -0,0 +1,414 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+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.assertTrue;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.ArrayUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code EventCountCircuitBreaker}.
+ */
+public class EventCountCircuitBreakerTest extends AbstractLangTest {
+ /** Constant for the opening threshold. */
+ private static final int OPENING_THRESHOLD = 10;
+
+ /** Constant for the closing threshold. */
+ private static final int CLOSING_THRESHOLD = 5;
+
+ /** Constant for the factor for converting nanoseconds. */
+ private static final long NANO_FACTOR = 1000L * 1000L * 1000L;
+
+ /**
+ * Tests that time units are correctly taken into account by constructors.
+ */
+ @Test
+ public void testIntervalCalculation() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 2, TimeUnit.MILLISECONDS);
+ assertEquals(NANO_FACTOR, breaker.getOpeningInterval(), "Wrong opening interval");
+ assertEquals(2 * NANO_FACTOR / 1000, breaker.getClosingInterval(), "Wrong closing interval");
+ }
+
+ /**
+ * Tests that the closing interval is the same as the opening interval if it is not
+ * specified.
+ */
+ @Test
+ public void testDefaultClosingInterval() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD);
+ assertEquals(NANO_FACTOR, breaker.getClosingInterval(), "Wrong closing interval");
+ }
+
+ /**
+ * Tests that the closing threshold is the same as the opening threshold if not
+ * specified otherwise.
+ */
+ @Test
+ public void testDefaultClosingThreshold() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS);
+ assertEquals(NANO_FACTOR, breaker.getClosingInterval(), "Wrong closing interval");
+ assertEquals(OPENING_THRESHOLD, breaker.getClosingThreshold(), "Wrong closing threshold");
+ }
+
+ /**
+ * Tests that a circuit breaker is closed after its creation.
+ */
+ @Test
+ public void testInitiallyClosed() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS);
+ assertFalse(breaker.isOpen(), "Open");
+ assertTrue(breaker.isClosed(), "Not closed");
+ }
+
+ /**
+ * Tests whether the current time is correctly determined.
+ */
+ @Test
+ public void testNow() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS);
+ final long nowNanos = breaker.nanoTime();
+ final long deltaNanos = Math.abs(System.nanoTime() - nowNanos);
+ assertTrue(deltaNanos < 100_000, String.format("Delta %,d ns to current time too large", deltaNanos));
+ }
+
+ /**
+ * Tests that the circuit breaker stays closed if the number of received events stays
+ * below the threshold.
+ */
+ @Test
+ public void testNotOpeningUnderThreshold() {
+ long startTime = 1000;
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ for (int i = 0; i < OPENING_THRESHOLD - 1; i++) {
+ assertTrue(breaker.at(startTime).incrementAndCheckState(), "In open state");
+ startTime++;
+ }
+ assertTrue(breaker.isClosed(), "Not closed");
+ }
+
+ /**
+ * Tests that the circuit breaker stays closed if there are a number of received
+ * events, but not in a single check interval.
+ */
+ @Test
+ public void testNotOpeningCheckIntervalExceeded() {
+ long startTime = 0L;
+ final long timeIncrement = 3 * NANO_FACTOR / (2 * OPENING_THRESHOLD);
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ for (int i = 0; i < 5 * OPENING_THRESHOLD; i++) {
+ assertTrue(breaker.at(startTime).incrementAndCheckState(), "In open state");
+ startTime += timeIncrement;
+ }
+ assertTrue(breaker.isClosed(), "Not closed");
+ }
+
+ /**
+ * Tests that the circuit breaker opens if all conditions are met.
+ */
+ @Test
+ public void testOpeningWhenThresholdReached() {
+ long startTime = 0;
+ final long timeIncrement = NANO_FACTOR / OPENING_THRESHOLD - 1;
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ boolean open = false;
+ for (int i = 0; i < OPENING_THRESHOLD + 1; i++) {
+ open = !breaker.at(startTime).incrementAndCheckState();
+ startTime += timeIncrement;
+ }
+ assertTrue(open, "Not open");
+ assertFalse(breaker.isClosed(), "Closed");
+ }
+
+ /**
+ * Tests that the circuit breaker opens if all conditions are met when using
+ * {@link EventCountCircuitBreaker#incrementAndCheckState(Integer increment)}.
+ */
+ @Test
+ public void testOpeningWhenThresholdReachedThroughBatch() {
+ final long timeIncrement = NANO_FACTOR / OPENING_THRESHOLD - 1;
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ final long startTime = timeIncrement * (OPENING_THRESHOLD + 1);
+ final boolean open = !breaker.at(startTime).incrementAndCheckState(OPENING_THRESHOLD + 1);
+ assertTrue(open, "Not open");
+ assertFalse(breaker.isClosed(), "Closed");
+ }
+
+ /**
+ * Tests that an open circuit breaker does not close itself when the number of events
+ * received is over the threshold.
+ */
+ @Test
+ public void testNotClosingOverThreshold() {
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD,
+ 10, TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ long startTime = 0;
+ breaker.open();
+ for (int i = 0; i <= CLOSING_THRESHOLD; i++) {
+ assertFalse(breaker.at(startTime).incrementAndCheckState(), "Not open");
+ startTime += 1000;
+ }
+ assertFalse(breaker.at(startTime + NANO_FACTOR).incrementAndCheckState(), "Closed in new interval");
+ assertTrue(breaker.isOpen(), "Not open at end");
+ }
+
+ /**
+ * Tests that the circuit breaker closes automatically if the number of events
+ * received goes under the closing threshold.
+ */
+ @Test
+ public void testClosingWhenThresholdReached() {
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD,
+ 10, TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ breaker.open();
+ breaker.at(1000).incrementAndCheckState();
+ assertFalse(breaker.at(2000).checkState(), "Already closed");
+ assertFalse(breaker.at(NANO_FACTOR).checkState(), "Closed at interval end");
+ assertTrue(breaker.at(NANO_FACTOR + 1).checkState(), "Not closed after interval end");
+ assertTrue(breaker.isClosed(), "Not closed at end");
+ }
+
+ /**
+ * Tests whether an explicit open operation fully initializes the internal check data
+ * object. Otherwise, the circuit breaker may close itself directly afterwards.
+ */
+ @Test
+ public void testOpenStartsNewCheckInterval() {
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ breaker.at(NANO_FACTOR - 1000).open();
+ assertTrue(breaker.isOpen(), "Not open");
+ assertFalse(breaker.at(NANO_FACTOR + 100).checkState(), "Already closed");
+ }
+
+ /**
+ * Tests whether a new check interval is started if the circuit breaker has a
+ * transition to open state.
+ */
+ @Test
+ public void testAutomaticOpenStartsNewCheckInterval() {
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ long time = 10 * NANO_FACTOR;
+ for (int i = 0; i <= OPENING_THRESHOLD; i++) {
+ breaker.at(time++).incrementAndCheckState();
+ }
+ assertTrue(breaker.isOpen(), "Not open");
+ time += NANO_FACTOR - 1000;
+ assertFalse(breaker.at(time).incrementAndCheckState(), "Already closed");
+ time += 1001;
+ assertTrue(breaker.at(time).checkState(), "Not closed in time interval");
+ }
+
+ /**
+ * Tests whether the circuit breaker can be closed explicitly.
+ */
+ @Test
+ public void testClose() {
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ long time = 0;
+ for (int i = 0; i <= OPENING_THRESHOLD; i++, time += 1000) {
+ breaker.at(time).incrementAndCheckState();
+ }
+ assertTrue(breaker.isOpen(), "Not open");
+ breaker.close();
+ assertTrue(breaker.isClosed(), "Not closed");
+ assertTrue(breaker.at(time + 1000).incrementAndCheckState(), "Open again");
+ }
+
+ /**
+ * Tests whether events are generated when the state is changed.
+ */
+ @Test
+ public void testChangeEvents() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS);
+ final ChangeListener listener = new ChangeListener(breaker);
+ breaker.addChangeListener(listener);
+ breaker.open();
+ breaker.close();
+ listener.verify(Boolean.TRUE, Boolean.FALSE);
+ }
+
+ /**
+ * Tests whether a change listener can be removed.
+ */
+ @Test
+ public void testRemoveChangeListener() {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS);
+ final ChangeListener listener = new ChangeListener(breaker);
+ breaker.addChangeListener(listener);
+ breaker.open();
+ breaker.removeChangeListener(listener);
+ breaker.close();
+ listener.verify(Boolean.TRUE);
+ }
+
+ /**
+ * Tests that a state transition triggered by multiple threads is handled correctly.
+ * Only the first transition should cause an event to be sent.
+ */
+ @Test
+ public void testStateTransitionGuarded() throws InterruptedException {
+ final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
+ TimeUnit.SECONDS);
+ final ChangeListener listener = new ChangeListener(breaker);
+ breaker.addChangeListener(listener);
+
+ final int threadCount = 128;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Thread[] threads = new Thread[threadCount];
+ for (int i = 0; i < threadCount; i++) {
+ threads[i] = new Thread() {
+ @Override
+ public void run() {
+ try {
+ latch.await();
+ } catch (final InterruptedException iex) {
+ // ignore
+ }
+ breaker.open();
+ }
+ };
+ threads[i].start();
+ }
+ latch.countDown();
+ for (final Thread thread : threads) {
+ thread.join();
+ }
+ listener.verify(Boolean.TRUE);
+ }
+
+ /**
+ * Tests that automatic state transitions generate change events as well.
+ */
+ @Test
+ public void testChangeEventsGeneratedByAutomaticTransitions() {
+ final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
+ TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
+ final ChangeListener listener = new ChangeListener(breaker);
+ breaker.addChangeListener(listener);
+ long time = 0;
+ for (int i = 0; i <= OPENING_THRESHOLD; i++, time += 1000) {
+ breaker.at(time).incrementAndCheckState();
+ }
+ breaker.at(NANO_FACTOR + 1).checkState();
+ breaker.at(3 * NANO_FACTOR).checkState();
+ listener.verify(Boolean.TRUE, Boolean.FALSE);
+ }
+
+ /**
+ * A test implementation of {@code EventCountCircuitBreaker} which supports mocking the timer.
+ * This is useful for the creation of deterministic tests for switching the circuit
+ * breaker's state.
+ */
+ private static class EventCountCircuitBreakerTestImpl extends EventCountCircuitBreaker {
+ /** The current time in nanoseconds. */
+ private long currentTime;
+
+ EventCountCircuitBreakerTestImpl(final int openingThreshold, final long openingInterval,
+ final TimeUnit openingUnit, final int closingThreshold, final long closingInterval,
+ final TimeUnit closingUnit) {
+ super(openingThreshold, openingInterval, openingUnit, closingThreshold,
+ closingInterval, closingUnit);
+ }
+
+ /**
+ * Sets the current time to be used by this test object for the next operation.
+ *
+ * @param time the time to set
+ * @return a reference to this object
+ */
+ public EventCountCircuitBreakerTestImpl at(final long time) {
+ currentTime = time;
+ return this;
+ }
+
+ /**
+ * {@inheritDoc} This implementation returns the value passed to the {@code at()}
+ * method.
+ */
+ @Override
+ long nanoTime() {
+ return currentTime;
+ }
+ }
+
+ /**
+ * A test change listener for checking whether correct change events are generated.
+ */
+ private static class ChangeListener implements PropertyChangeListener {
+ /** The expected event source. */
+ private final Object expectedSource;
+
+ /** A list with the updated values extracted from received change events. */
+ private final List<Boolean> changedValues;
+
+ /**
+ * Creates a new instance of {@code ChangeListener} and sets the expected event
+ * source.
+ *
+ * @param source the expected event source
+ */
+ ChangeListener(final Object source) {
+ expectedSource = source;
+ changedValues = new ArrayList<>();
+ }
+
+ @Override
+ public void propertyChange(final PropertyChangeEvent evt) {
+ assertEquals(expectedSource, evt.getSource(), "Wrong event source");
+ assertEquals("open", evt.getPropertyName(), "Wrong property name");
+ final Boolean newValue = (Boolean) evt.getNewValue();
+ final Boolean oldValue = (Boolean) evt.getOldValue();
+ assertNotEquals(newValue, oldValue, "Old and new value are equal");
+ changedValues.add(newValue);
+ }
+
+ /**
+ * Verifies that change events for the expected values have been received.
+ *
+ * @param values the expected values
+ */
+ public void verify(final Boolean... values) {
+ assertArrayEquals(values, changedValues.toArray(ArrayUtils.EMPTY_BOOLEAN_OBJECT_ARRAY));
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/FutureTasksTest.java b/src/test/java/org/apache/commons/lang3/concurrent/FutureTasksTest.java
new file mode 100644
index 000000000..08b83a103
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/FutureTasksTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link FutureTask}.
+ */
+public class FutureTasksTest extends AbstractLangTest {
+
+ @Test
+ public void testRun() throws InterruptedException, ExecutionException {
+ final String data = "Hello";
+ final FutureTask<String> f = FutureTasks.run(() -> data);
+ assertTrue(f.isDone());
+ assertEquals(data, f.get());
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java
new file mode 100644
index 000000000..16eb12451
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.lang3.concurrent;
+
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Test class for {@code LazyInitializer}.
+ */
+public class LazyInitializerTest extends AbstractConcurrentInitializerTest {
+ /** The initializer to be tested. */
+ private LazyInitializerTestImpl initializer;
+
+ @BeforeEach
+ public void setUp() {
+ initializer = new LazyInitializerTestImpl();
+ }
+
+ /**
+ * Returns the initializer to be tested. This implementation returns the
+ * {@code LazyInitializer} created in the {@code setUp()} method.
+ *
+ * @return the initializer to be tested
+ */
+ @Override
+ protected ConcurrentInitializer<Object> createInitializer() {
+ return initializer;
+ }
+
+ /**
+ * A test implementation of LazyInitializer. This class creates a plain
+ * Object. As Object does not provide a specific equals() method, it is easy
+ * to check whether multiple instances were created.
+ */
+ private static class LazyInitializerTestImpl extends
+ LazyInitializer<Object> {
+ @Override
+ protected Object initialize() {
+ return new Object();
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MemoizerComputableTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerComputableTest.java
new file mode 100644
index 000000000..30ed9f5a9
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerComputableTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class MemoizerComputableTest extends AbstractLangTest {
+
+ private Computable<Integer, Integer> computable;
+
+ @BeforeEach
+ public void setUpComputableMock() {
+ computable = EasyMock.mock(Computable.class);
+ }
+
+ @Test
+ public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+ final InterruptedException interruptedException = new InterruptedException();
+ expect(computable.compute(input)).andThrow(interruptedException);
+ replay(computable);
+
+ assertThrows(Throwable.class, () -> memoizer.compute(input));
+ assertThrows(IllegalStateException.class, () -> memoizer.compute(input));
+ }
+
+ @Test
+ public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable, false);
+ final InterruptedException interruptedException = new InterruptedException();
+ expect(computable.compute(input)).andThrow(interruptedException);
+ replay(computable);
+
+ assertThrows(Throwable.class, () -> memoizer.compute(input));
+ assertThrows(IllegalStateException.class, () -> memoizer.compute(input));
+ }
+
+ @Test
+ public void testDoesRecalculateWhenSetToTrue() throws Exception {
+ final Integer input = 1;
+ final Integer answer = 3;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable, true);
+ final InterruptedException interruptedException = new InterruptedException();
+ expect(computable.compute(input)).andThrow(interruptedException).andReturn(answer);
+ replay(computable);
+
+ assertThrows(Throwable.class, () -> memoizer.compute(input));
+ assertEquals(answer, memoizer.compute(input));
+ }
+
+ @Test
+ public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+ expect(computable.compute(input)).andReturn(input);
+ replay(computable);
+
+ assertEquals(input, memoizer.compute(input), "Should call computable first time");
+ assertEquals(input, memoizer.compute(input), "Should not call the computable the second time");
+ }
+
+ @Test
+ public void testWhenComputableThrowsError() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+ final Error error = new Error();
+ expect(computable.compute(input)).andThrow(error);
+ replay(computable);
+
+ assertThrows(Error.class, () -> memoizer.compute(input));
+ }
+
+ @Test
+ public void testWhenComputableThrowsRuntimeException() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+ final RuntimeException runtimeException = new RuntimeException("Some runtime exception");
+ expect(computable.compute(input)).andThrow(runtimeException);
+ replay(computable);
+
+ assertThrows(RuntimeException.class, () -> memoizer.compute(input));
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MemoizerFunctionTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerFunctionTest.java
new file mode 100644
index 000000000..4b2ff52dc
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerFunctionTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.function.Function;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class MemoizerFunctionTest extends AbstractLangTest {
+
+ private Function<Integer, Integer> function;
+
+ @BeforeEach
+ public void setUpComputableMock() {
+ function = EasyMock.mock(Function.class);
+ }
+
+ @Test
+ public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
+ final IllegalArgumentException interruptedException = new IllegalArgumentException();
+ expect(function.apply(input)).andThrow(interruptedException);
+ replay(function);
+
+ assertThrows(Throwable.class, () -> memoizer.compute(input));
+ assertThrows(IllegalArgumentException.class, () -> memoizer.compute(input));
+ }
+
+ @Test
+ public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function, false);
+ final IllegalArgumentException interruptedException = new IllegalArgumentException();
+ expect(function.apply(input)).andThrow(interruptedException);
+ replay(function);
+
+ assertThrows(Throwable.class, () -> memoizer.compute(input));
+ assertThrows(IllegalArgumentException.class, () -> memoizer.compute(input));
+ }
+
+ @Test
+ public void testDoesRecalculateWhenSetToTrue() throws Exception {
+ final Integer input = 1;
+ final Integer answer = 3;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function, true);
+ final IllegalArgumentException interruptedException = new IllegalArgumentException();
+ expect(function.apply(input)).andThrow(interruptedException).andReturn(answer);
+ replay(function);
+
+ assertThrows(Throwable.class, () -> memoizer.compute(input));
+ assertEquals(answer, memoizer.compute(input));
+ }
+
+ @Test
+ public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
+ expect(function.apply(input)).andReturn(input);
+ replay(function);
+
+ assertEquals(input, memoizer.compute(input), "Should call computable first time");
+ assertEquals(input, memoizer.compute(input), "Should not call the computable the second time");
+ }
+
+ @Test
+ public void testWhenComputableThrowsError() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
+ final Error error = new Error();
+ expect(function.apply(input)).andThrow(error);
+ replay(function);
+
+ assertThrows(Error.class, () -> memoizer.compute(input));
+ }
+
+ @Test
+ public void testWhenComputableThrowsRuntimeException() throws Exception {
+ final Integer input = 1;
+ final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
+ final RuntimeException runtimeException = new RuntimeException("Some runtime exception");
+ expect(function.apply(input)).andThrow(runtimeException);
+ replay(function);
+
+ assertThrows(RuntimeException.class, () -> memoizer.compute(input));
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java
new file mode 100644
index 000000000..e5d46a4e7
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@link MultiBackgroundInitializer}.
+ */
+public class MultiBackgroundInitializerTest extends AbstractLangTest {
+ /** Constant for the names of the child initializers. */
+ private static final String CHILD_INIT = "childInitializer";
+
+ /** The initializer to be tested. */
+ private MultiBackgroundInitializer initializer;
+
+ @BeforeEach
+ public void setUp() {
+ initializer = new MultiBackgroundInitializer();
+ }
+
+ /**
+ * Tests whether a child initializer has been executed. Optionally the
+ * expected executor service can be checked, too.
+ *
+ * @param child the child initializer
+ * @param expExec the expected executor service (null if the executor should
+ * not be checked)
+ * @throws ConcurrentException if an error occurs
+ */
+ private void checkChild(final BackgroundInitializer<?> child,
+ final ExecutorService expExec) throws ConcurrentException {
+ final ChildBackgroundInitializer cinit = (ChildBackgroundInitializer) child;
+ final Integer result = cinit.get();
+ assertEquals(1, result.intValue(), "Wrong result");
+ assertEquals(1, cinit.initializeCalls, "Wrong number of executions");
+ if (expExec != null) {
+ assertEquals(expExec, cinit.currentExecutor, "Wrong executor service");
+ }
+ }
+
+ /**
+ * Tests addInitializer() if a null name is passed in. This should cause an
+ * exception.
+ */
+ @Test
+ public void testAddInitializerNullName() {
+ assertThrows(NullPointerException.class, () -> initializer.addInitializer(null, new ChildBackgroundInitializer()));
+ }
+
+ /**
+ * Tests addInitializer() if a null initializer is passed in. This should
+ * cause an exception.
+ */
+ @Test
+ public void testAddInitializerNullInit() {
+ assertThrows(NullPointerException.class, () -> initializer.addInitializer(CHILD_INIT, null));
+ }
+
+ /**
+ * Tests the background processing if there are no child initializers.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeNoChildren() throws ConcurrentException {
+ assertTrue(initializer.start(), "Wrong result of start()");
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ assertTrue(res.initializerNames().isEmpty(), "Got child initializers");
+ assertTrue(initializer.getActiveExecutor().isShutdown(), "Executor not shutdown");
+ }
+
+ /**
+ * Helper method for testing the initialize() method. This method can
+ * operate with both an external and a temporary executor service.
+ *
+ * @return the result object produced by the initializer
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ private MultiBackgroundInitializer.MultiBackgroundInitializerResults checkInitialize()
+ throws ConcurrentException {
+ final int count = 5;
+ for (int i = 0; i < count; i++) {
+ initializer.addInitializer(CHILD_INIT + i,
+ new ChildBackgroundInitializer());
+ }
+ initializer.start();
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ assertEquals(count, res.initializerNames().size(), "Wrong number of child initializers");
+ for (int i = 0; i < count; i++) {
+ final String key = CHILD_INIT + i;
+ assertTrue(res.initializerNames().contains(key), "Name not found: " + key);
+ assertEquals(Integer.valueOf(1), res.getResultObject(key), "Wrong result object");
+ assertFalse(res.isException(key), "Exception flag");
+ assertNull(res.getException(key), "Got an exception");
+ checkChild(res.getInitializer(key), initializer.getActiveExecutor());
+ }
+ return res;
+ }
+
+ /**
+ * Tests background processing if a temporary executor is used.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeTempExec() throws ConcurrentException {
+ checkInitialize();
+ assertTrue(initializer.getActiveExecutor().isShutdown(), "Executor not shutdown");
+ }
+
+ /**
+ * Tests background processing if an external executor service is provided.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeExternalExec() throws ConcurrentException, InterruptedException {
+ final ExecutorService exec = Executors.newCachedThreadPool();
+ try {
+ initializer = new MultiBackgroundInitializer(exec);
+ checkInitialize();
+ assertEquals(exec, initializer.getActiveExecutor(), "Wrong executor");
+ assertFalse(exec.isShutdown(), "Executor was shutdown");
+ } finally {
+ exec.shutdown();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Tests the behavior of initialize() if a child initializer has a specific
+ * executor service. Then this service should not be overridden.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeChildWithExecutor() throws ConcurrentException, InterruptedException {
+ final String initExec = "childInitializerWithExecutor";
+ final ExecutorService exec = Executors.newSingleThreadExecutor();
+ try {
+ final ChildBackgroundInitializer c1 = new ChildBackgroundInitializer();
+ final ChildBackgroundInitializer c2 = new ChildBackgroundInitializer();
+ c2.setExternalExecutor(exec);
+ initializer.addInitializer(CHILD_INIT, c1);
+ initializer.addInitializer(initExec, c2);
+ initializer.start();
+ initializer.get();
+ checkChild(c1, initializer.getActiveExecutor());
+ checkChild(c2, exec);
+ } finally {
+ exec.shutdown();
+ exec.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Tries to add another child initializer after the start() method has been
+ * called. This should not be allowed.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testAddInitializerAfterStart() throws ConcurrentException {
+ initializer.start();
+ assertThrows(
+ IllegalStateException.class,
+ () -> initializer.addInitializer(CHILD_INIT, new ChildBackgroundInitializer()),
+ "Could add initializer after start()!");
+ initializer.get();
+ }
+
+ /**
+ * Tries to query an unknown child initializer from the results object. This
+ * should cause an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testResultGetInitializerUnknown() throws ConcurrentException {
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
+ assertThrows(NoSuchElementException.class, () -> res.getInitializer("unknown"));
+ }
+
+ /**
+ * Tries to query the results of an unknown child initializer from the
+ * results object. This should cause an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testResultGetResultObjectUnknown() throws ConcurrentException {
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
+ assertThrows(NoSuchElementException.class, () -> res.getResultObject("unknown"));
+ }
+
+ /**
+ * Tries to query the exception of an unknown child initializer from the
+ * results object. This should cause an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testResultGetExceptionUnknown() throws ConcurrentException {
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
+ assertThrows(NoSuchElementException.class, () -> res.getException("unknown"));
+ }
+
+ /**
+ * Tries to query the exception flag of an unknown child initializer from
+ * the results object. This should cause an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testResultIsExceptionUnknown() throws ConcurrentException {
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
+ assertThrows(NoSuchElementException.class, () -> res.isException("unknown"));
+ }
+
+ /**
+ * Tests that the set with the names of the initializers cannot be modified.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testResultInitializerNamesModify() throws ConcurrentException {
+ checkInitialize();
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ final Iterator<String> it = res.initializerNames().iterator();
+ it.next();
+ assertThrows(UnsupportedOperationException.class, it::remove);
+ }
+
+ /**
+ * Tests the behavior of the initializer if one of the child initializers
+ * throws a runtime exception.
+ */
+ @Test
+ public void testInitializeRuntimeEx() {
+ final ChildBackgroundInitializer child = new ChildBackgroundInitializer();
+ child.ex = new RuntimeException();
+ initializer.addInitializer(CHILD_INIT, child);
+ initializer.start();
+ final Exception ex = assertThrows(Exception.class, initializer::get);
+ assertEquals(child.ex, ex, "Wrong exception");
+ }
+
+ /**
+ * Tests the behavior of the initializer if one of the child initializers
+ * throws a checked exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeEx() throws ConcurrentException {
+ final ChildBackgroundInitializer child = new ChildBackgroundInitializer();
+ child.ex = new Exception();
+ initializer.addInitializer(CHILD_INIT, child);
+ initializer.start();
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ assertTrue(res.isException(CHILD_INIT), "No exception flag");
+ assertNull(res.getResultObject(CHILD_INIT), "Got a results object");
+ final ConcurrentException cex = res.getException(CHILD_INIT);
+ assertEquals(child.ex, cex.getCause(), "Wrong cause");
+ }
+
+ /**
+ * Tests the isSuccessful() method of the result object if no child
+ * initializer has thrown an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeResultsIsSuccessfulTrue()
+ throws ConcurrentException {
+ final ChildBackgroundInitializer child = new ChildBackgroundInitializer();
+ initializer.addInitializer(CHILD_INIT, child);
+ initializer.start();
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ assertTrue(res.isSuccessful(), "Wrong success flag");
+ }
+
+ /**
+ * Tests the isSuccessful() method of the result object if at least one
+ * child initializer has thrown an exception.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeResultsIsSuccessfulFalse()
+ throws ConcurrentException {
+ final ChildBackgroundInitializer child = new ChildBackgroundInitializer();
+ child.ex = new Exception();
+ initializer.addInitializer(CHILD_INIT, child);
+ initializer.start();
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ assertFalse(res.isSuccessful(), "Wrong success flag");
+ }
+
+ /**
+ * Tests whether MultiBackgroundInitializers can be combined in a nested
+ * way.
+ *
+ * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
+ */
+ @Test
+ public void testInitializeNested() throws ConcurrentException {
+ final String nameMulti = "multiChildInitializer";
+ initializer
+ .addInitializer(CHILD_INIT, new ChildBackgroundInitializer());
+ final MultiBackgroundInitializer mi2 = new MultiBackgroundInitializer();
+ final int count = 3;
+ for (int i = 0; i < count; i++) {
+ mi2
+ .addInitializer(CHILD_INIT + i,
+ new ChildBackgroundInitializer());
+ }
+ initializer.addInitializer(nameMulti, mi2);
+ initializer.start();
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
+ .get();
+ final ExecutorService exec = initializer.getActiveExecutor();
+ checkChild(res.getInitializer(CHILD_INIT), exec);
+ final MultiBackgroundInitializer.MultiBackgroundInitializerResults res2 = (MultiBackgroundInitializer.MultiBackgroundInitializerResults) res
+ .getResultObject(nameMulti);
+ assertEquals(count, res2.initializerNames().size(), "Wrong number of initializers");
+ for (int i = 0; i < count; i++) {
+ checkChild(res2.getInitializer(CHILD_INIT + i), exec);
+ }
+ assertTrue(exec.isShutdown(), "Executor not shutdown");
+ }
+
+ /**
+ * A concrete implementation of {@code BackgroundInitializer} used for
+ * defining background tasks for {@code MultiBackgroundInitializer}.
+ */
+ private static class ChildBackgroundInitializer extends
+ BackgroundInitializer<Integer> {
+ /** Stores the current executor service. */
+ volatile ExecutorService currentExecutor;
+
+ /** A counter for the invocations of initialize(). */
+ volatile int initializeCalls;
+
+ /** An exception to be thrown by initialize(). */
+ Exception ex;
+
+ /**
+ * Records this invocation. Optionally throws an exception.
+ */
+ @Override
+ protected Integer initialize() throws Exception {
+ currentExecutor = getActiveExecutor();
+ initializeCalls++;
+
+ if (ex != null) {
+ throw ex;
+ }
+
+ return Integer.valueOf(initializeCalls);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java
new file mode 100644
index 000000000..c6a97fe44
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/ThresholdCircuitBreakerTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code ThresholdCircuitBreaker}.
+ */
+public class ThresholdCircuitBreakerTest extends AbstractLangTest {
+
+ /**
+ * Threshold used in tests.
+ */
+ private static final long threshold = 10L;
+
+ private static final long zeroThreshold = 0L;
+
+ /**
+ * Tests that the threshold is working as expected when incremented and no exception is thrown.
+ */
+ @Test
+ public void testThreshold() {
+ final ThresholdCircuitBreaker circuit = new ThresholdCircuitBreaker(threshold);
+ circuit.incrementAndCheckState(9L);
+ assertFalse(circuit.incrementAndCheckState(1L), "Circuit opened before reaching the threshold");
+ }
+
+ /**
+ * Tests that exceeding the threshold raises an exception.
+ */
+ @Test
+ public void testThresholdCircuitBreakingException() {
+ final ThresholdCircuitBreaker circuit = new ThresholdCircuitBreaker(threshold);
+ circuit.incrementAndCheckState(9L);
+ assertTrue(circuit.incrementAndCheckState(2L), "The circuit was supposed to be open after increment above the threshold");
+ }
+
+ /**
+ * Test that when threshold is zero, the circuit breaker is always open.
+ */
+ @Test
+ public void testThresholdEqualsZero() {
+ final ThresholdCircuitBreaker circuit = new ThresholdCircuitBreaker(zeroThreshold);
+ assertTrue(circuit.incrementAndCheckState(0L), "When the threshold is zero, the circuit is supposed to be always open");
+ }
+
+ /**
+ * Tests that closing a {@code ThresholdCircuitBreaker} resets the internal counter.
+ */
+ @Test
+ public void testClosingThresholdCircuitBreaker() {
+ final ThresholdCircuitBreaker circuit = new ThresholdCircuitBreaker(threshold);
+ circuit.incrementAndCheckState(9L);
+ circuit.close();
+ // now the internal counter is back at zero, not 9 anymore. So it is safe to increment 9 again
+ assertFalse(circuit.incrementAndCheckState(9L), "Internal counter was not reset back to zero");
+ }
+
+ /**
+ * Tests that we can get the threshold value correctly.
+ */
+ @Test
+ public void testGettingThreshold() {
+ final ThresholdCircuitBreaker circuit = new ThresholdCircuitBreaker(threshold);
+ assertEquals(Long.valueOf(threshold), Long.valueOf(circuit.getThreshold()), "Wrong value of threshold");
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/TimedSemaphoreTest.java b/src/test/java/org/apache/commons/lang3/concurrent/TimedSemaphoreTest.java
new file mode 100644
index 000000000..5f158660c
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/TimedSemaphoreTest.java
@@ -0,0 +1,553 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.ThreadUtils;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for TimedSemaphore.
+ */
+public class TimedSemaphoreTest extends AbstractLangTest {
+ /** Constant for the time period. */
+ private static final long PERIOD_MILLIS = 500;
+
+ private static final Duration DURATION = Duration.ofMillis(PERIOD_MILLIS);
+
+ /** Constant for the time unit. */
+ private static final TimeUnit UNIT = TimeUnit.MILLISECONDS;
+
+ /** Constant for the default limit. */
+ private static final int LIMIT = 10;
+
+ /**
+ * Tests creating a new instance.
+ */
+ @Test
+ public void testInit() {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ EasyMock.replay(service);
+ final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD_MILLIS, UNIT,
+ LIMIT);
+ EasyMock.verify(service);
+ assertEquals(service, semaphore.getExecutorService(), "Wrong service");
+ assertEquals(PERIOD_MILLIS, semaphore.getPeriod(), "Wrong period");
+ assertEquals(UNIT, semaphore.getUnit(), "Wrong unit");
+ assertEquals(0, semaphore.getLastAcquiresPerPeriod(), "Statistic available");
+ assertEquals(0.0, semaphore.getAverageCallsPerPeriod(), .05, "Average available");
+ assertFalse(semaphore.isShutdown(), "Already shutdown");
+ assertEquals(LIMIT, semaphore.getLimit(), "Wrong limit");
+ }
+
+ /**
+ * Tries to create an instance with a negative period. This should cause an
+ * exception.
+ */
+ @Test
+ public void testInitInvalidPeriod() {
+ assertThrows(IllegalArgumentException.class, () -> new TimedSemaphore(0L, UNIT, LIMIT));
+ }
+
+ /**
+ * Tests whether a default executor service is created if no service is
+ * provided.
+ */
+ @Test
+ public void testInitDefaultService() {
+ final TimedSemaphore semaphore = new TimedSemaphore(PERIOD_MILLIS, UNIT, LIMIT);
+ final ScheduledThreadPoolExecutor exec = (ScheduledThreadPoolExecutor) semaphore
+ .getExecutorService();
+ assertFalse(exec.getContinueExistingPeriodicTasksAfterShutdownPolicy(), "Wrong periodic task policy");
+ assertFalse(exec.getExecuteExistingDelayedTasksAfterShutdownPolicy(), "Wrong delayed task policy");
+ assertFalse(exec.isShutdown(), "Already shutdown");
+ semaphore.shutdown();
+ }
+
+ /**
+ * Tests starting the timer.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testStartTimer() throws InterruptedException {
+ final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(PERIOD_MILLIS,
+ UNIT, LIMIT);
+ final ScheduledFuture<?> future = semaphore.startTimer();
+ assertNotNull(future, "No future returned");
+ ThreadUtils.sleepQuietly(DURATION);
+ final int trials = 10;
+ int count = 0;
+ do {
+ Thread.sleep(PERIOD_MILLIS);
+ assertFalse(count++ > trials, "endOfPeriod() not called!");
+ } while (semaphore.getPeriodEnds() <= 0);
+ semaphore.shutdown();
+ }
+
+ /**
+ * Tests the shutdown() method if the executor belongs to the semaphore. In
+ * this case it has to be shut down.
+ */
+ @Test
+ public void testShutdownOwnExecutor() {
+ final TimedSemaphore semaphore = new TimedSemaphore(PERIOD_MILLIS, UNIT, LIMIT);
+ semaphore.shutdown();
+ assertTrue(semaphore.isShutdown(), "Not shutdown");
+ assertTrue(semaphore.getExecutorService().isShutdown(), "Executor not shutdown");
+ }
+
+ /**
+ * Tests the shutdown() method for a shared executor service before a task
+ * was started. This should do pretty much nothing.
+ */
+ @Test
+ public void testShutdownSharedExecutorNoTask() {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ EasyMock.replay(service);
+ final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD_MILLIS, UNIT,
+ LIMIT);
+ semaphore.shutdown();
+ assertTrue(semaphore.isShutdown(), "Not shutdown");
+ EasyMock.verify(service);
+ }
+
+ /**
+ * Prepares an executor service mock to expect the start of the timer.
+ *
+ * @param service the mock
+ * @param future the future
+ */
+ private void prepareStartTimer(final ScheduledExecutorService service,
+ final ScheduledFuture<?> future) {
+ service.scheduleAtFixedRate((Runnable) EasyMock.anyObject(), EasyMock
+ .eq(PERIOD_MILLIS), EasyMock.eq(PERIOD_MILLIS), EasyMock.eq(UNIT));
+ EasyMock.expectLastCall().andReturn(future);
+ }
+
+ /**
+ * Tests the shutdown() method for a shared executor after the task was
+ * started. In this case the task must be canceled.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testShutdownSharedExecutorTask() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.expect(Boolean.valueOf(future.cancel(false))).andReturn(Boolean.TRUE);
+ EasyMock.replay(service, future);
+ final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
+ PERIOD_MILLIS, UNIT, LIMIT);
+ semaphore.acquire();
+ semaphore.shutdown();
+ assertTrue(semaphore.isShutdown(), "Not shutdown");
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tests multiple invocations of the shutdown() method.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testShutdownMultipleTimes() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.expect(Boolean.valueOf(future.cancel(false))).andReturn(Boolean.TRUE);
+ EasyMock.replay(service, future);
+ final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
+ PERIOD_MILLIS, UNIT, LIMIT);
+ semaphore.acquire();
+ for (int i = 0; i < 10; i++) {
+ semaphore.shutdown();
+ }
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tests the acquire() method if a limit is set.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testAcquireLimit() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.replay(service, future);
+ final int count = 10;
+ final CountDownLatch latch = new CountDownLatch(count - 1);
+ final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD_MILLIS, UNIT, 1);
+ final SemaphoreThread t = new SemaphoreThread(semaphore, latch, count,
+ count - 1);
+ semaphore.setLimit(count - 1);
+
+ // start a thread that calls the semaphore count times
+ t.start();
+ latch.await();
+ // now the semaphore's limit should be reached and the thread blocked
+ assertEquals(count - 1, semaphore.getAcquireCount(), "Wrong semaphore count");
+
+ // this wakes up the thread, it should call the semaphore once more
+ semaphore.endOfPeriod();
+ t.join();
+ assertEquals(1, semaphore.getAcquireCount(), "Wrong semaphore count (2)");
+ assertEquals(count - 1, semaphore.getLastAcquiresPerPeriod(), "Wrong acquire() count");
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tests the acquire() method if more threads are involved than the limit.
+ * This method starts a number of threads that all invoke the semaphore. The
+ * semaphore's limit is set to 1, so in each period only a single thread can
+ * acquire the semaphore.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testAcquireMultipleThreads() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.replay(service, future);
+ final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
+ PERIOD_MILLIS, UNIT, 1);
+ semaphore.latch = new CountDownLatch(1);
+ final int count = 10;
+ final SemaphoreThread[] threads = new SemaphoreThread[count];
+ for (int i = 0; i < count; i++) {
+ threads[i] = new SemaphoreThread(semaphore, null, 1, 0);
+ threads[i].start();
+ }
+ for (int i = 0; i < count; i++) {
+ semaphore.latch.await();
+ assertEquals(1, semaphore.getAcquireCount(), "Wrong count");
+ semaphore.latch = new CountDownLatch(1);
+ semaphore.endOfPeriod();
+ assertEquals(1, semaphore.getLastAcquiresPerPeriod(), "Wrong acquire count");
+ }
+ for (int i = 0; i < count; i++) {
+ threads[i].join();
+ }
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tests the acquire() method if no limit is set. A test thread is started
+ * that calls the semaphore a large number of times. Even if the semaphore's
+ * period does not end, the thread should never block.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testAcquireNoLimit() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.replay(service, future);
+ final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
+ PERIOD_MILLIS, UNIT, TimedSemaphore.NO_LIMIT);
+ final int count = 1000;
+ final CountDownLatch latch = new CountDownLatch(count);
+ final SemaphoreThread t = new SemaphoreThread(semaphore, latch, count, count);
+ t.start();
+ latch.await();
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tries to call acquire() after shutdown(). This should cause an exception.
+ */
+ @Test
+ public void testPassAfterShutdown() {
+ final TimedSemaphore semaphore = new TimedSemaphore(PERIOD_MILLIS, UNIT, LIMIT);
+ semaphore.shutdown();
+ assertThrows(IllegalStateException.class, semaphore::acquire);
+ }
+
+ /**
+ * Tests a bigger number of invocations that span multiple periods. The
+ * period is set to a very short time. A background thread calls the
+ * semaphore a large number of times. While it runs at last one end of a
+ * period should be reached.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testAcquireMultiplePeriods() throws InterruptedException {
+ final int count = 1000;
+ final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(
+ PERIOD_MILLIS / 10, TimeUnit.MILLISECONDS, 1);
+ semaphore.setLimit(count / 4);
+ final CountDownLatch latch = new CountDownLatch(count);
+ final SemaphoreThread t = new SemaphoreThread(semaphore, latch, count, count);
+ t.start();
+ latch.await();
+ semaphore.shutdown();
+ assertTrue(semaphore.getPeriodEnds() > 0, "End of period not reached");
+ }
+
+ /**
+ * Tests the methods for statistics.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testGetAverageCallsPerPeriod() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.replay(service, future);
+ final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD_MILLIS, UNIT,
+ LIMIT);
+ semaphore.acquire();
+ semaphore.endOfPeriod();
+ assertEquals(1.0, semaphore.getAverageCallsPerPeriod(), .005, "Wrong average (1)");
+ semaphore.acquire();
+ semaphore.acquire();
+ semaphore.endOfPeriod();
+ assertEquals(1.5, semaphore.getAverageCallsPerPeriod(), .005, "Wrong average (2)");
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tests whether the available non-blocking calls can be queried.
+ *
+ * @throws InterruptedException so we don't have to catch it
+ */
+ @Test
+ public void testGetAvailablePermits() throws InterruptedException {
+ final ScheduledExecutorService service = EasyMock
+ .createMock(ScheduledExecutorService.class);
+ final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
+ prepareStartTimer(service, future);
+ EasyMock.replay(service, future);
+ final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD_MILLIS, UNIT,
+ LIMIT);
+ for (int i = 0; i < LIMIT; i++) {
+ assertEquals(LIMIT - i, semaphore.getAvailablePermits(), "Wrong available count at " + i);
+ semaphore.acquire();
+ }
+ semaphore.endOfPeriod();
+ assertEquals(LIMIT, semaphore.getAvailablePermits(), "Wrong available count in new period");
+ EasyMock.verify(service, future);
+ }
+
+ /**
+ * Tests the tryAcquire() method. It is checked whether the semaphore can be acquired
+ * by a bunch of threads the expected number of times and not more.
+ */
+ @Test
+ public void testTryAcquire() throws InterruptedException {
+ final TimedSemaphore semaphore = new TimedSemaphore(PERIOD_MILLIS, TimeUnit.SECONDS,
+ LIMIT);
+ final TryAcquireThread[] threads = new TryAcquireThread[3 * LIMIT];
+ final CountDownLatch latch = new CountDownLatch(1);
+ for (int i = 0; i < threads.length; i++) {
+ threads[i] = new TryAcquireThread(semaphore, latch);
+ threads[i].start();
+ }
+
+ latch.countDown();
+ int permits = 0;
+ for (final TryAcquireThread t : threads) {
+ t.join();
+ if (t.acquired) {
+ permits++;
+ }
+ }
+ assertEquals(LIMIT, permits, "Wrong number of permits granted");
+ }
+
+ /**
+ * Tries to call tryAcquire() after shutdown(). This should cause an exception.
+ */
+ @Test
+ public void testTryAcquireAfterShutdown() {
+ final TimedSemaphore semaphore = new TimedSemaphore(PERIOD_MILLIS, UNIT, LIMIT);
+ semaphore.shutdown();
+ assertThrows(IllegalStateException.class, semaphore::tryAcquire);
+ }
+
+ /**
+ * A specialized implementation of {@code TimedSemaphore} that is easier to
+ * test.
+ */
+ private static class TimedSemaphoreTestImpl extends TimedSemaphore {
+ /** A mock scheduled future. */
+ ScheduledFuture<?> schedFuture;
+
+ /** A latch for synchronizing with the main thread. */
+ volatile CountDownLatch latch;
+
+ /** Counter for the endOfPeriod() invocations. */
+ private int periodEnds;
+
+ TimedSemaphoreTestImpl(final long timePeriod, final TimeUnit timeUnit,
+ final int limit) {
+ super(timePeriod, timeUnit, limit);
+ }
+
+ TimedSemaphoreTestImpl(final ScheduledExecutorService service,
+ final long timePeriod, final TimeUnit timeUnit, final int limit) {
+ super(service, timePeriod, timeUnit, limit);
+ }
+
+ /**
+ * Returns the number of invocations of the endOfPeriod() method.
+ *
+ * @return the endOfPeriod() invocations
+ */
+ int getPeriodEnds() {
+ synchronized (this) {
+ return periodEnds;
+ }
+ }
+
+ /**
+ * Invokes the latch if one is set.
+ *
+ * @throws InterruptedException because it is declared that way in TimedSemaphore
+ */
+ @Override
+ public synchronized void acquire() throws InterruptedException {
+ super.acquire();
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ /**
+ * Counts the number of invocations.
+ */
+ @Override
+ protected synchronized void endOfPeriod() {
+ super.endOfPeriod();
+ periodEnds++;
+ }
+
+ /**
+ * Either returns the mock future or calls the super method.
+ */
+ @Override
+ protected ScheduledFuture<?> startTimer() {
+ return schedFuture != null ? schedFuture : super.startTimer();
+ }
+ }
+
+ /**
+ * A test thread class that will be used by tests for triggering the
+ * semaphore. The thread calls the semaphore a configurable number of times.
+ * When this is done, it can notify the main thread.
+ */
+ private static class SemaphoreThread extends Thread {
+ /** The semaphore. */
+ private final TimedSemaphore semaphore;
+
+ /** A latch for communication with the main thread. */
+ private final CountDownLatch latch;
+
+ /** The number of acquire() calls. */
+ private final int count;
+
+ /** The number of invocations of the latch. */
+ private final int latchCount;
+
+ SemaphoreThread(final TimedSemaphore b, final CountDownLatch l, final int c, final int lc) {
+ semaphore = b;
+ latch = l;
+ count = c;
+ latchCount = lc;
+ }
+
+ /**
+ * Calls acquire() on the semaphore for the specified number of times.
+ * Optionally the latch will also be triggered to synchronize with the
+ * main test thread.
+ */
+ @Override
+ public void run() {
+ try {
+ for (int i = 0; i < count; i++) {
+ semaphore.acquire();
+
+ if (i < latchCount) {
+ latch.countDown();
+ }
+ }
+ } catch (final InterruptedException iex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * A test thread class which invokes {@code tryAcquire()} on the test semaphore and
+ * records the return value.
+ */
+ private static class TryAcquireThread extends Thread {
+ /** The semaphore. */
+ private final TimedSemaphore semaphore;
+
+ /** A latch for communication with the main thread. */
+ private final CountDownLatch latch;
+
+ /** Flag whether a permit could be acquired. */
+ private boolean acquired;
+
+ TryAcquireThread(final TimedSemaphore s, final CountDownLatch l) {
+ semaphore = s;
+ latch = l;
+ }
+
+ @Override
+ public void run() {
+ try {
+ if (latch.await(10, TimeUnit.SECONDS)) {
+ acquired = semaphore.tryAcquire();
+ }
+ } catch (final InterruptedException iex) {
+ // ignore
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedExecutionExceptionTest.java b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedExecutionExceptionTest.java
new file mode 100644
index 000000000..b9115fda9
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedExecutionExceptionTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link UncheckedExecutionException}.
+ */
+public class UncheckedExecutionExceptionTest extends AbstractLangTest {
+
+ @Test
+ public void testConstructWithCause() {
+ final Exception e = new Exception();
+ assertSame(e, new UncheckedExecutionException(e).getCause());
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java
new file mode 100644
index 000000000..d583680be
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.exception.UncheckedInterruptedException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link UncheckedFuture}.
+ */
+public class UncheckedFutureTest extends AbstractLangTest {
+
+ private static class TestFuture<V> extends AbstractFutureProxy<V> {
+
+ private final Exception exception;
+
+ TestFuture(final Exception throwable) {
+ super(ConcurrentUtils.constantFuture(null));
+ this.exception = throwable;
+ }
+
+ TestFuture(final V value) {
+ super(ConcurrentUtils.constantFuture(value));
+ this.exception = null;
+ }
+
+ @SuppressWarnings("unchecked") // Programming error if call site blows up at runtime.
+ private <T extends Exception> void checkException() throws T {
+ if (exception != null) {
+ throw (T) exception;
+ }
+ }
+
+ @Override
+ public V get() throws InterruptedException, ExecutionException {
+ checkException();
+ return super.get();
+ }
+
+ @Override
+ public V get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ checkException();
+ return super.get(timeout, unit);
+ }
+
+ }
+
+ @Test
+ public void testGetExecutionException() {
+ final ExecutionException e = new ExecutionException(new Exception());
+ assertThrows(UncheckedExecutionException.class, () -> UncheckedFuture.on(new TestFuture<>(e)).get());
+ }
+
+ @Test
+ public void testGetInterruptedException() {
+ final InterruptedException e = new InterruptedException();
+ assertThrows(UncheckedInterruptedException.class, () -> UncheckedFuture.on(new TestFuture<>(e)).get());
+ }
+
+ @Test
+ public void testGetLongExecutionException() {
+ final ExecutionException e = new ExecutionException(new Exception());
+ assertThrows(UncheckedExecutionException.class, () -> UncheckedFuture.on(new TestFuture<>(e)).get(1, TimeUnit.MICROSECONDS));
+ }
+
+ @Test
+ public void testGetLongInterruptedException() {
+ final InterruptedException e = new InterruptedException();
+ assertThrows(UncheckedInterruptedException.class, () -> UncheckedFuture.on(new TestFuture<>(e)).get(1, TimeUnit.MICROSECONDS));
+ }
+
+ @Test
+ public void testGetLongTimeoutException() {
+ final TimeoutException e = new TimeoutException();
+ assertThrows(UncheckedTimeoutException.class, () -> UncheckedFuture.on(new TestFuture<>(e)).get(1, TimeUnit.MICROSECONDS));
+ }
+
+ @Test
+ public void testMap() {
+ final List<String> expected = Arrays.asList("Y", "Z");
+ final List<Future<String>> input = Arrays.asList(new TestFuture<>("Y"), new TestFuture<>("Z"));
+ assertEquals(expected, UncheckedFuture.map(input).map(UncheckedFuture::get).collect(Collectors.toList()));
+ }
+
+ @Test
+ public void testOnCollection() {
+ final List<String> expected = Arrays.asList("Y", "Z");
+ final List<Future<String>> input = Arrays.asList(new TestFuture<>("Y"), new TestFuture<>("Z"));
+ assertEquals(expected, UncheckedFuture.on(input).stream().map(UncheckedFuture::get).collect(Collectors.toList()));
+ }
+
+ @Test
+ public void testOnFuture() {
+ assertEquals("Z", UncheckedFuture.on(new TestFuture<>("Z")).get());
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedTimeoutExceptionTest.java b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedTimeoutExceptionTest.java
new file mode 100644
index 000000000..5b0e98954
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedTimeoutExceptionTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link UncheckedTimeoutException}.
+ */
+public class UncheckedTimeoutExceptionTest extends AbstractLangTest {
+
+ @Test
+ public void testConstructWithCause() {
+ final Exception e = new Exception();
+ assertSame(e, new UncheckedTimeoutException(e).getCause());
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/locks/LockingVisitorsTest.java b/src/test/java/org/apache/commons/lang3/concurrent/locks/LockingVisitorsTest.java
new file mode 100644
index 000000000..d8d1a453d
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/concurrent/locks/LockingVisitorsTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.lang3.concurrent.locks;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import java.util.function.LongConsumer;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ThreadUtils;
+import org.apache.commons.lang3.concurrent.locks.LockingVisitors.LockVisitor;
+import org.apache.commons.lang3.concurrent.locks.LockingVisitors.StampedLockVisitor;
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.junit.jupiter.api.Test;
+
+public class LockingVisitorsTest extends AbstractLangTest {
+
+ private static final Duration SHORT_DELAY = Duration.ofMillis(100);
+ private static final Duration DELAY = Duration.ofMillis(1500);
+ private static final int NUMBER_OF_THREADS = 10;
+ private static final Duration TOTAL_DELAY = DELAY.multipliedBy(NUMBER_OF_THREADS);
+
+ protected boolean containsTrue(final boolean[] booleanArray) {
+ synchronized (booleanArray) {
+ return ArrayUtils.contains(booleanArray, true);
+ }
+ }
+
+ private void runTest(final Duration delay, final boolean exclusiveLock, final LongConsumer runTimeCheck,
+ final boolean[] booleanValues, final LockVisitor<boolean[], ?> visitor) throws InterruptedException {
+ final boolean[] runningValues = new boolean[10];
+
+ final long startTimeMillis = System.currentTimeMillis();
+ for (int i = 0; i < booleanValues.length; i++) {
+ final int index = i;
+ final FailableConsumer<boolean[], ?> consumer = b -> {
+ b[index] = false;
+ ThreadUtils.sleep(delay);
+ b[index] = true;
+ set(runningValues, index, false);
+ };
+ final Thread t = new Thread(() -> {
+ if (exclusiveLock) {
+ visitor.acceptWriteLocked(consumer);
+ } else {
+ visitor.acceptReadLocked(consumer);
+ }
+ });
+ set(runningValues, i, true);
+ t.start();
+ }
+ while (containsTrue(runningValues)) {
+ ThreadUtils.sleep(SHORT_DELAY);
+ }
+ final long endTimeMillis = System.currentTimeMillis();
+ for (final boolean booleanValue : booleanValues) {
+ assertTrue(booleanValue);
+ }
+ // WRONG assumption
+ // runTimeCheck.accept(endTimeMillis - startTimeMillis);
+ }
+
+ protected void set(final boolean[] booleanArray, final int offset, final boolean value) {
+ synchronized (booleanArray) {
+ booleanArray[offset] = value;
+ }
+ }
+
+ @Test
+ public void testReentrantReadWriteLockExclusive() throws Exception {
+
+ /*
+ * If our threads are running concurrently, then we expect to be no faster than running one after the other.
+ */
+ final boolean[] booleanValues = new boolean[10];
+ runTest(DELAY, true, millis -> assertTrue(millis >= TOTAL_DELAY.toMillis()), booleanValues,
+ LockingVisitors.reentrantReadWriteLockVisitor(booleanValues));
+ }
+
+ @Test
+ public void testReentrantReadWriteLockNotExclusive() throws Exception {
+
+ /*
+ * If our threads are running concurrently, then we expect to be faster than running one after the other.
+ */
+ final boolean[] booleanValues = new boolean[10];
+ runTest(DELAY, false, millis -> assertTrue(millis < TOTAL_DELAY.toMillis()), booleanValues,
+ LockingVisitors.reentrantReadWriteLockVisitor(booleanValues));
+ }
+
+ @Test
+ public void testResultValidation() {
+ final Object hidden = new Object();
+ final StampedLockVisitor<Object> lock = LockingVisitors.stampedLockVisitor(hidden);
+ final Object o1 = lock.applyReadLocked(h -> new Object());
+ assertNotNull(o1);
+ assertNotSame(hidden, o1);
+ final Object o2 = lock.applyWriteLocked(h -> new Object());
+ assertNotNull(o2);
+ assertNotSame(hidden, o2);
+ }
+
+ @Test
+ public void testStampedLockExclusive() throws Exception {
+
+ /*
+ * If our threads are running concurrently, then we expect to be no faster than running one after the other.
+ */
+ final boolean[] booleanValues = new boolean[10];
+ runTest(DELAY, true, millis -> assertTrue(millis >= TOTAL_DELAY.toMillis()), booleanValues,
+ LockingVisitors.stampedLockVisitor(booleanValues));
+ }
+
+ @Test
+ public void testStampedLockNotExclusive() throws Exception {
+
+ /*
+ * If our threads are running concurrently, then we expect to be faster than running one after the other.
+ */
+ final boolean[] booleanValues = new boolean[10];
+ runTest(DELAY, false, millis -> assertTrue(millis < TOTAL_DELAY.toMillis()), booleanValues,
+ LockingVisitors.stampedLockVisitor(booleanValues));
+ }
+}