/* Copyright 2013 Jonatan Jönsson * * Licensed 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 se.softhouse.common.testlib; import static org.fest.assertions.Assertions.assertThat; import static org.fest.assertions.Fail.fail; import static se.softhouse.common.testlib.ConcurrencyTester.NR_OF_CONCURRENT_RUNNERS; import static se.softhouse.common.testlib.ConcurrencyTester.verify; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.junit.Test; import se.softhouse.common.testlib.ConcurrencyTester.RunnableFactory; import com.google.common.base.Throwables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.testing.NullPointerTester; import com.google.common.testing.NullPointerTester.Visibility; import com.google.common.util.concurrent.Atomics; /** * Tests for {@link ConcurrencyTester} */ public class ConcurrencyTesterTest { @Test public void testThatRunsAreInterleavedBetweenDifferentThreadsForEachIteration() throws Throwable { final Vector<Integer> invocationIdentifiers = new Vector<Integer>(); final int iterationCount = 100; ConcurrencyTester.verify(new RunnableFactory(){ @Override public int iterationCount() { return iterationCount; } @Override public Runnable create(final int identifierForThread) { return new Runnable(){ @Override public void run() { invocationIdentifiers.add(identifierForThread); } }; } }, Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS); int maxInARow = 0; int currentStreak = 0; int lastIdentifier = 0; for(int invocationIdentifier : invocationIdentifiers) { if(invocationIdentifier != lastIdentifier) { if(currentStreak > maxInARow) { maxInARow = currentStreak; } currentStreak = 0; } lastIdentifier = invocationIdentifier; currentStreak++; } // As there is a barrier in between each run, no thread should be able to run more than // two iterations without another thread executing in between. // 2 because when all threads have waited for each other the last one to execute could be // the first one after the startSignal have been sent to be executed. assertThat(maxInARow) // .as("At least two of " + NR_OF_CONCURRENT_RUNNERS + " threads should be interleaved " + "with each other, otherwise the concurrency test harness is not acceptable") // .isLessThanOrEqualTo(2); } @Test public void testThatExceptionsFromRunnableIsPropagatedAsIs() throws Throwable { try { final SimulatedException error = new SimulatedException(); ConcurrencyTester.verify(new RunnableFactory(){ @Override public int iterationCount() { return 1; } @Override public Runnable create(int uniqueNumber) { return new Runnable(){ @Override public void run() { throw error; } }; } }, Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS); fail(error.getClass() + " was not propagated correctly, this could potentially hide AssertionErrors thrown when bugs are introduced"); } catch(SimulatedException expected) { } } @Test public void testThatEachRunnableIsRunIterationCountTimes() throws Throwable { final Map<Integer, AtomicInteger> counters = Maps.newHashMap(); final int iterationCount = 100; ConcurrencyTester.verify(new RunnableFactory(){ @Override public int iterationCount() { return iterationCount; } @Override public Runnable create(int uniqueNumber) { final AtomicInteger counter = new AtomicInteger(); counters.put(uniqueNumber, counter); return new Runnable(){ @Override public void run() { counter.incrementAndGet(); } }; } }, Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS); for(AtomicInteger counter : counters.values()) { assertThat(counter.get()).isEqualTo(iterationCount); } assertThat(counters).hasSize(ConcurrencyTester.NR_OF_CONCURRENT_RUNNERS); } @Test public void testThatNoUniqueNumberIsUsedTwice() throws Throwable { final Set<Integer> assignedNumbers = Collections.synchronizedSet(Sets.<Integer>newHashSet()); ConcurrencyTester.verify(new RunnableFactory(){ @Override public int iterationCount() { return 2; } @Override public Runnable create(final int uniqueNumber) { assignedNumbers.add(uniqueNumber); return new Runnable(){ @Override public void run() { } }; } }, Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS); assertThat(assignedNumbers).hasSize(ConcurrencyTester.NR_OF_CONCURRENT_RUNNERS); } @Test public void testThatInterruptedStatusIsClearedWhenInterruptedAndThatRemainingThreadsGetInterrupted() throws Throwable { final Thread originThread = Thread.currentThread(); final AtomicReference<Throwable> failure = Atomics.newReference(); final Lock infinitelyLocked = new ReentrantLock(); infinitelyLocked.lock(); // +1 so that the starter thread also can await the start final CyclicBarrier startSignal = new CyclicBarrier(ConcurrencyTester.NR_OF_CONCURRENT_RUNNERS + 1); final CountDownLatch doneSignal = new CountDownLatch(1); Thread interruptableThread = new Thread(){ @Override public void run() { try { // Verify that all the Runnables gets an interrupt signal ConcurrencyTester.verify(new RunnableFactory(){ @Override public int iterationCount() { return 1; } @Override public Runnable create(final int uniqueNumber) { return new Runnable(){ @Override public void run() { try { startSignal.await(); // Wait for the interrupt signal infinitelyLocked.lockInterruptibly(); fail("Executor did not interrupt remaining threads during shutdown operation"); } catch(InterruptedException expected) { Thread.interrupted(); } catch(BrokenBarrierException e) { Throwables.propagate(e); } } }; } }, Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS); fail("verify completed without being interrupted"); } catch(InterruptedException expected) { } catch(Throwable error) { failure.compareAndSet(null, error); originThread.interrupt(); return; } try { // As the interrupt status of this thread should have been cleared, it // should be okay to reenter the verify method ConcurrencyTester.verify(new RunnableFactory(){ @Override public int iterationCount() { return 1; } @Override public Runnable create(int uniqueNumber) { return new Runnable(){ @Override public void run() { } }; } }, Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS); } catch(Throwable error) { failure.compareAndSet(null, error); originThread.interrupt(); return; } doneSignal.countDown(); } }; try { interruptableThread.start(); startSignal.await(); interruptableThread.interrupt(); if(!doneSignal.await(Constants.EXPECTED_TEST_TIME_FOR_THIS_SUITE, TimeUnit.MILLISECONDS)) { fail("Timeout while waiting for shutdown of remaining threads"); } } catch(InterruptedException error) { Thread.interrupted(); } if(failure.get() != null) throw failure.get(); } @Test public void testThatVerifyTimeoutsWhenTasksTakesToLongToExecute() throws Throwable { final Lock infinitelyLocked = new ReentrantLock(); infinitelyLocked.lock(); try { verify(new RunnableFactory(){ @Override public int iterationCount() { return 1; } @Override public Runnable create(final int uniqueNumber) { return new Runnable(){ @Override public void run() { try { infinitelyLocked.lockInterruptibly(); fail("Inifinite test was not interrupted"); } catch(InterruptedException expected) { } } }; } }, 0, TimeUnit.NANOSECONDS); fail("Infinitely running test did not throw AssertionError when timeout should have occured"); } catch(AssertionError expected) { assertThat(expected.getMessage()).isEqualTo(NR_OF_CONCURRENT_RUNNERS + " of " + NR_OF_CONCURRENT_RUNNERS + " did not finish within 0 NANOSECONDS"); } } @Test public void testThatNullContractsAreFollowed() throws Exception { new NullPointerTester().testStaticMethods(ConcurrencyTester.class, Visibility.PACKAGE); } }