[jconch commit] r127 - in trunk: src/jconch/testing test/jconch/testing

2 views
Skip to first unread message

codesite...@google.com

unread,
Jan 20, 2009, 6:02:48 PM1/20/09
to jco...@googlegroups.com
Author: HamletDRC
Date: Fri Jan 16 21:06:55 2009
New Revision: 127

Added:
trunk/src/jconch/testing/Assert.java
trunk/test/jconch/testing/AssertTest.java
Modified:
trunk/src/jconch/testing/SerialExecutorService.java

Log:
Added an assertSynchronized method along with unit tests.

Added: trunk/src/jconch/testing/Assert.java
==============================================================================
--- (empty file)
+++ trunk/src/jconch/testing/Assert.java Fri Jan 16 21:06:55 2009
@@ -0,0 +1,150 @@
+package jconch.testing;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A collection of helpful concurrency based assert methods.
+ *
+ * @author Hamlet D'Arcy
+ */
+public class Assert {
+ private static final int MAX_THREADS = 10;
+ private static final int NUM_ITERATIONS = 1000;
+
+ /**
+ * <p>The assertSynchronized method attempts to cause concurrency errors
by repeatedly invoking code on seperate threads. </p>
+ *
+ * <p>To use this assertion, give the method a {@link
java.util.concurrent.Callable} object that produces a Collection
+ * of code fragments which must be synchronized between themselves. This
master callable that produces the code
+ * fragments is called the "taskFactory", and it will be invoked
repeatedly, running all the resulting Callable
+ * objects on seperate threads. </p>
+ *
+ * <p>As an example, the {@link java.util.ArrayList#add(Object)} method
is not synchronized. So add invocations on
+ * multiple threads will eventually result in an
ArrayIndexOutOfBoundsException. To test this, pass
+ * assertSynchronized a factory for Callables that call add():</p>
+ *
+ * <pre>
+ *
+ * Assert.assertSynchronized(
+ * new Callable&lt;List&lt;Callable&lt;Void&gt;&gt;&gt;() {
+ * public List&lt;Callable&lt;Void&gt;&gt; call() throws Exception
{
+ *
+ * // ArrayList.add(T) is not synchronized
+ * final ArrayList&lt;Object&gt; unsafeObject = new
ArrayList&lt;Object&gt;();
+ * final Callable&lt;Void&gt; unsafeInvocation = new
Callable&lt;Void&gt;() {
+ * public Void call() throws Exception {
+ * for (int x = 0; x &lt; 1000; x++) {
+ * unsafeObject.add(new Object());
+ * }
+ * return null;
+ * }
+ * };
+ * return Arrays.asList(
+ * unsafeInvocation,
+ * unsafeInvocation
+ * );
+ * }
+ * }
+ * );</pre>
+ * <p>This will fail with an AssertionError as expected. The test will
pass if you switch the implementation
+ * to a {@link java.util.Vector} instead of an ArrayList</p>
+ *
+ * <p>By default, it creates a thread pool of 10 threads, and attempts to
run the code passed in 1000 times. Any errors
+ * that occur are collected and reported in an {@link
java.lang.AssertionError}.</p>
+ *
+ * @param taskFactory
+ * a Callable that produces a new Collection of Callable objects on
each iteration. Expect this Callable to
+ * be invoked x number of times, where x is the numIterations value.
May not be null.
+ * @throws Exception
+ * Any exception raised by the taskFactory will be passed up to the
caller
+ * @throws AssertionError
+ * If any of the callables raises an exception, then as AssertionError
is thrown. The error will report how
+ * many failures occurred and the message of one of the thrown
exceptions. The message will usually (but not always)
+ * be that of the most recently thrown exception.
+ */
+ public static void assertSynchronized(Callable<? extends
Collection<Callable<Void>>> taskFactory) throws Exception {
+ doAssertSynchronized(taskFactory, MAX_THREADS, NUM_ITERATIONS);
+ }
+
+ /**
+ * <p>This is the same as {@link Assert#assertSynchronized(Callable)}
expect that the thread pool size and the number
+ * or iterations can be specified.</p>
+ * @param taskFactory
+ * a Callable that produces a new Collection of Callable objects on
each iteration. Expect this Callable to
+ * be invoked x number of times, where x is the numIterations value.
May not be null.
+ * @param threadPoolSize
+ * size of the threadpool used during testing, must be positive
+ * @param numIterations
+ * the number of times to run the test. This number needs to be
sufficiently large emough to cause the error
+ * condition, whether it be a race condition, phantom data, or anything
else. Must be positive.
+ * @throws Exception
+ * Any exception raised by the taskFactory will be passed up to the
caller
+ * @throws AssertionError
+ * If any of the callables raises an exception, then as AssertionError
is thrown. The error will report how
+ * many failures occurred and the message of one of the thrown
exceptions. The message will usually (but not always)
+ * be that of the most recently thrown exception.
+ */
+ public static void assertSynchronized(Callable<? extends
Collection<Callable<Void>>> taskFactory, int threadPoolSize, int
numIterations) throws Exception {
+ doAssertSynchronized(taskFactory, threadPoolSize, numIterations);
+ }
+
+ private static void doAssertSynchronized(Callable<? extends
Collection<Callable<Void>>> taskFactory, int maxThreads, int numIterations)
throws Exception {
+ if (taskFactory == null) throw new NullPointerException("Null:
taskFactory");
+ if (maxThreads <= 0) throw new IllegalArgumentException("maxThreads must
be positive. Received: " + maxThreads);
+ if (numIterations <= 0) throw new
IllegalArgumentException("numIterations must be positive. Received: " +
maxThreads);
+
+ final ExecutorService executor =
Executors.newFixedThreadPool(maxThreads);
+ final AtomicInteger failureCount = new AtomicInteger(0);
+ final AtomicReference<Throwable> lastFailure = new
AtomicReference<Throwable>();
+
+ for (int x = 0; x < numIterations; x++) {
+ final Collection<Callable<Void>> tasks = taskFactory.call();
+ final int numTasks = tasks.size();
+ final CyclicBarrier startGate = new CyclicBarrier(numTasks);
+ final CountDownLatch endGate = new CountDownLatch(numTasks);
+
+ for (final Callable task : tasks) {
+
+ executor.submit(new Callable<Void>(){
+ public Void call() throws Exception {
+ try {
+ startGate.await();
+ task.call();
+ } catch (Throwable t) {
+ failureCount.incrementAndGet();
+ lastFailure.set(t); // race conditions here are no big deal
+ } finally {
+ endGate.countDown();
+ }
+ return null;
+ }
+ });
+ }
+
+ endGate.await();
+ }
+ executor.shutdown();
+ if (failureCount.get() > 0) {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ lastFailure.get().printStackTrace(new PrintStream(bytes));
+
+ throw new AssertionError(
+ String.format(
+ "An exception was raised running the synchronization test. " +
+ "The test failed %d out of %d times. Last known error:\n%s",
+ failureCount.get(),
+ numIterations,
+ bytes.toString()));
+ }
+ }
+}

Modified: trunk/src/jconch/testing/SerialExecutorService.java
==============================================================================
--- trunk/src/jconch/testing/SerialExecutorService.java (original)
+++ trunk/src/jconch/testing/SerialExecutorService.java Fri Jan 16 21:06:55
2009
@@ -3,7 +3,14 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-import java.util.concurrent.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

/**

Added: trunk/test/jconch/testing/AssertTest.java
==============================================================================
--- (empty file)
+++ trunk/test/jconch/testing/AssertTest.java Fri Jan 16 21:06:55 2009
@@ -0,0 +1,137 @@
+package jconch.testing;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Unit test for the {@link jconch.testing.Assert} class.
+ *
+ * @author Hamlet D'Arcy
+ */
+public class AssertTest {
+
+ /**
+ * Tests that each task in the taskFactory is called 1000 times and
+ * no to tasks within the same iteration executes on the same thread.
+ * @throws Exception
+ * on error
+ */
+ @Test
+ public void testAssertSynchronized_SuccessfulIteration() throws Exception
{
+
+ final AtomicInteger calledFactoryCount = new AtomicInteger(0);
+ final Map<Integer, Long> iterationToThreadId1 = new
ConcurrentHashMap<Integer, Long>();
+ final Map<Integer, Long> iterationToThreadId2 = new
ConcurrentHashMap<Integer, Long>();
+ final AtomicInteger calledClosure1Count = new AtomicInteger(0);
+ final AtomicInteger calledClosure2Count = new AtomicInteger(0);
+
+ Assert.assertSynchronized(
+ new Callable<List<Callable<Void>>>(){
+ public List<Callable<Void>> call() throws Exception {
+ calledFactoryCount.incrementAndGet(); //this should be invoked once
per iteration
+ return Arrays.asList(
+ new Callable<Void>(){
+ public Void call() throws Exception {
+ iterationToThreadId1.put(
+ calledClosure1Count.incrementAndGet(),
+ Thread.currentThread().getId()
+ );
+ return null;
+ }
+ },
+ new Callable<Void>(){
+ public Void call() throws Exception {
+ iterationToThreadId2.put(
+ calledClosure2Count.incrementAndGet(),
+ Thread.currentThread().getId()
+ );
+ return null;
+ }
+ }
+ );
+ }
+ }
+ );
+
+ assertEquals(1000, calledFactoryCount.get(), "Default should have been
1000 taskFactory invocations");
+ assertEquals(1000, calledFactoryCount.get(), "Default should have been
1000 task 1 invocations");
+ assertEquals(1000, calledFactoryCount.get(), "Default should have been
1000 task 2 invocations");
+ for(int x = 1; x <= 1000; x++) {
+ final Long firstThreadID = iterationToThreadId1.get(x);
+ final Long secondThreadID = iterationToThreadId2.get(x);
+ assertFalse(
+ firstThreadID.equals(secondThreadID),
+ String.format("Two closures in same run were executed on thread %d",
firstThreadID));
+ }
+ }
+
+ @Test
+ public void testAssertSynchronized_Failure() throws Exception {
+ try {
+ Assert.assertSynchronized(
+ new Callable<List<Callable<Void>>>() {
+ public List<Callable<Void>> call() throws Exception {
+
+ // ArrayList.add(T) is not synchronized
+ final ArrayList<Object> unsafeObject = new ArrayList<Object>();
+ final Callable<Void> unsafeInvocation = new Callable<Void>() {
+ public Void call() throws Exception {
+ for (int x = 0; x < 1000; x++) {
+ unsafeObject.add(new Object());
+ }
+ return null;
+ }
+ };
+ return Arrays.asList(
+ unsafeInvocation,
+ unsafeInvocation
+ );
+ }
+ }
+ );
+ fail("Unsafe ArrayList usage should have failed.");
+ } catch (AssertionError e) {
+ assertTrue(e.getMessage().contains("An exception was raised running the
synchronization test"));
+ assertTrue(e.getMessage().contains("ArrayIndexOutOfBoundsException"));
+ assertTrue(e.getMessage().contains("at java.util.ArrayList.add"));
+ }
+ }
+
+
+ @Test
+ public void testAssertSynchronized_Success() throws Exception {
+ Assert.assertSynchronized(
+ new Callable<List<Callable<Void>>>() {
+ public List<Callable<Void>> call() throws Exception {
+
+ // Vector.add(T) is synchronized
+ final Vector<Object> safeObject = new Vector<Object>();
+ final Callable<Void> safeInvocation = new Callable<Void>() {
+ public Void call() throws Exception {
+ for (int x = 0; x < 1000; x++) {
+ safeObject.add(new Object());
+ }
+ return null;
+ }
+ };
+ return Arrays.asList(
+ safeInvocation,
+ safeInvocation
+ );
+ }
+ }
+ );
+ }
+}

Reply all
Reply to author
Forward
0 new messages