diff options
Diffstat (limited to 'src/test/java/org/apache/commons/lang3/concurrent')
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)); + } +} |