
edu.umd.cs.mtc.MultithreadedTestCase Maven / Gradle / Ivy
package edu.umd.cs.mtc;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import junit.framework.Assert;
/**
* This is the base class for each test in the MultithreadedTC framework.
* To create a multithreaded test case, simply extend this class. Any method
* with a name that starts with "thread", that has no parameters and a void
* return type is a thread method. Each thread method will be run in a seperate
* thread. This class also defines {@link #initialize()} and {@link #finish()} methods
* you can override.
*
*
* A single run of a multithreaded test case consists of:
*
* - Running the {@link #initialize()} method
*
- Running each thread method in a seperate thread
*
- Running the {@link #finish()} method when all threads are done.
*
*
*
* The method {@link TestFramework#runOnce(MultithreadedTestCase)} can be used
* to run a MultithreadedTestCase once. The method
* {@link TestFramework#runManyTimes(MultithreadedTestCase, int)} can be used to
* run a multithread test case multiple times (to see if different interleavings
* produce different behaviors).
*
*
* There are several additional methods you can use in designing test cases. The
* MultithreadedTestCase maintains a metronome or clock, and ticks off
* intervals. You can get the current tick with {@link #getTick()} and you can
* wait until a particular tick with {@link #waitForTick(int)}. The metronome
* isn't a free running clock; it only advances to the next tick when all
* threads are blocked or waiting. Also, when all threads are blocked, if at least one
* thread isn't waiting for the metronome to advance, the system declares a
* deadlock to have occurred and terminates the test case (unless one of the
* threads is in state TIMED_WAITING).
*
*
* You can set a command line parameter -Dtunit.trace=true to cause tracing
* messages to be printed by the metronome frame, or invoke
* {@link MultithreadedTestCase#setTrace(boolean)} to turn tracing on or off.
*
*
* You can set command line parameter -Dtunit.runLimit=10 to cause a test case
* to fail if at least one thread stays in a runnable state for more than 10
* seconds without becoming blocked or waiting for a metronome tick. Use
* different values for shorter or longer time limits.
*
* @see TestFramework
*
* @author William Pugh
* @author Nathaniel Ayewah
* @since 1.0
*/
abstract public class MultithreadedTestCase extends Assert {
/**
* The metronome used to coordinate between threads. This clock
* is advanced by the clock thread started by {@link TestFramework}.
* The clock will not advance if it is frozen.
*
* @see #waitForTick(int)
* @see #freezeClock()
* @see #unfreezeClock()
*/
int clock;
/**
* The primary lock to synchronize on in this test case before
* accessing fields in this class.
*/
Object lock = new Object();
/**
* If true, the debugging information is printed to standard out
* while the test runs
*/
private boolean trace = Boolean.getBoolean("tunit.trace");
/**
* This flag is set to true when a test fails due to deadlock or
* timeout.
*
* @see TestFramework
*/
boolean failed;
/**
* This method is invoked in a test run before any test threads have
* started.
*
*/
public void initialize() {
}
/**
* This method is invoked in a test after after all test threads have
* finished.
*
*/
public void finish() {
}
/**
* @param trace
* the trace to set
*/
public void setTrace(boolean trace) {
this.trace = trace;
}
/**
* @return the trace
*/
public boolean getTrace() {
return trace;
}
// =======================
// -- Thread Management --
// - - - - - - - - - - - -
/**
* Map each thread to the clock tick it is waiting for.
*/
IdentityHashMap threads = new IdentityHashMap();
/**
* ThreadLocal containing a reference to the current instance of
* this class for each thread. When a thread completes or dies, its reference
* to this class is removed.
*/
static ThreadLocal currentTestCase =
new ThreadLocal();
/**
* This method is called right after a new testcase thread is created by
* the {@link TestFramework}. It provides initial values for
* {@link #currentTestCase} and {@link #threads}.
*/
void hello() {
currentTestCase.set(this);
synchronized (lock) {
Thread currentThread = Thread.currentThread();
threads.put(currentThread, 0);
}
}
/**
* This method is called just before a testcase thread completes.
* It cleans out {@link #currentTestCase} and {@link #threads}.
*/
void goodbye() {
synchronized (lock) {
Thread currentThread = Thread.currentThread();
threads.remove(currentThread);
}
currentTestCase.set(null);
}
/**
* Map a thread name to all test case threads as they are created, primarily
* so that they can be accessed by each other.
*
* @see #getThreadByName(String)
* @see #getThread(int)
*/
HashMap methodThreads = new HashMap();
/**
* Get a thread given the method name that it corresponds to. E.g.
* to get the thread running the contents of the method
* thread1()
, call getThreadByName("thread1")
*
*
* NOTE: {@link #initialize()} is called before threads are created,
* so this method returns null if called from {@link #initialize()}
* (but not from {@link #finish()}).
*
* @see #getThread(int)
*
* @param methodName
* the name of the method corresponding to the thread requested
* @return
* the thread corresponding to methodName
*/
public Thread getThreadByName(String methodName) {
synchronized (lock) {
return methodThreads.get(methodName);
}
}
/**
* Get a thread corresponding to the method whose name is formed using
* the prefix "thread" followed by an integer (represented by
* index
. e.g. getThread(1) returns the thread
* that thread1()
is running in.
*
*
* NOTE: {@link #initialize()} is called before threads are created,
* so this method returns null if called from {@link #initialize()}
* (but not from {@link #finish()}).
*
* @see #getThreadByName(String)
*
* @param index
* an integer following "thread" in the name of the method
* @return
* the Thread corresponding to this method
*/
public Thread getThread(int index) {
return getThreadByName("thread" + index);
}
/**
* Associates a thread with given method name. If the method name is already
* associated with a Thread, the old thread is returned, otherwise null is returned
*/
public Thread putThread(String methodName, Thread t) {
synchronized (lock) {
return methodThreads.put(methodName, t);
}
}
// ===========================
// -- Clock tick management --
// - - - - - - - - - - - - - -
/**
* Force this thread to block until the thread metronome reaches the
* specified value, at which point the thread is unblocked.
*
* @param c
* the tick value to wait for
*/
public void waitForTick(int c) {
synchronized (lock) {
threads.put(Thread.currentThread(), c);
while (!failed && clock < c)
try {
if (getTrace())
System.out.println(Thread.currentThread().getName()
+ " is waiting for time " + c);
lock.wait();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
if (failed)
throw new IllegalStateException("Clock never reached " + c);
if (getTrace())
System.out.println("Releasing "
+ Thread.currentThread().getName() + " at time "
+ clock);
}
}
/**
* An Enum-based version of waitForTick. It simply looks up the ordinal and
* adds 1 to determine the clock tick to wait for.
*
* @see #waitForTick(int)
*
* @param e
* An Enum representing the tick to wait for. The first enumeration
* constant represents tick 1, the second is tick 2, etc.
*/
public void waitForTick(Enum e) {
waitForTick(e.ordinal()+1);
}
/**
* Gets the current value of the thread metronome. Primarily useful in
* assert statements.
*
* @see #assertTick(int)
*
* @return the current tick value
*/
public int getTick() {
synchronized (lock) {
return clock;
}
}
/**
* Assert that the clock is in tick tick
*
* @param tick
* a number >= 0
*/
public void assertTick(int tick) {
assertEquals(tick, getTick());
}
// =======================================
// -- Components for freezing the clock --
// - - - - - - - - - - - - - - - - - - - -
/**
* Read locks are acquired when clock is frozen and must be
* released before the clock can advance in a waitForTick().
*/
final ReentrantReadWriteLock clockLock = new ReentrantReadWriteLock();
/**
* When the clock is frozen, it will not advance even when all threads
* are blocked. Use this to block the current thread with a time limit,
* but prevent the clock from advancing due to a {@link #waitForTick(int)} in
* another thread. This statements that occur when clock is frozen should be
* followed by {@link #unfreezeClock()} in the same thread.
*/
public void freezeClock() {
clockLock.readLock().lock();
}
/**
* Unfreeze a clock that has been frozen by {@link #freezeClock()}. Both
* methods must be called from the same thread.
*/
public void unfreezeClock() {
clockLock.readLock().unlock();
}
/**
* Check if the clock has been frozen by any threads.
*/
public boolean isClockFrozen() {
return clockLock.getReadLockCount() > 0;
}
// ===============================
// -- Customized Wait Functions --
// - - - - - - - - - - - - - - - -
/**
* A boolean flag for each thread indicating whether the next call to
* {@link #waitOn(Object)} or {@link #awaitOn(Condition)} should
* return immediately.
*
* @see #skipNextWait()
*/
private static ThreadLocal skipNextWait = new ThreadLocal() {
@Override
public Boolean initialValue() {
return Boolean.FALSE;
}
};
/**
* When this method is called from a thread, the next call to
* {@link #waitOn(Object)} or {@link #awaitOn(Condition)} will return
* immediately without blocking. Use this to make tests more robust.
*/
static public void skipNextWait() {
skipNextWait.set(true);
}
/**
* This method is a replacement for {@link Object#wait()}. It suppresses
* the {@link InterruptedException} that you would otherwise have to
* deal with, and allows automated skipping of the next wait. The
* method {@link #skipNextWait()} will force that thread to immediately return
* from the next call to this method.
* Designing your tests so that they work even if {@link Object#wait()}
* occasionally returns immediately will make your code
* much more robust in face of several potential threading issues.
*
* @param o
* the object to wait on
*/
static public void waitOn(Object o) {
// System.out.println("About to wait on " + System.identityHashCode(o));
MultithreadedTestCase thisTestCase = currentTestCase.get();
if (thisTestCase != null && thisTestCase.failed)
throw new RuntimeException("Test case has failed");
if (skipNextWait.get()) {
skipNextWait.set(false);
return;
}
try {
o.wait(3000);
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (IllegalMonitorStateException e) {
System.out.println("Got illegal monitor state exception");
}
if (thisTestCase != null && thisTestCase.failed)
throw new RuntimeException("Test case has failed");
// System.out.println("waited on " + System.identityHashCode(o));
}
/**
* This method is a replacement for {@link Condition#await()}. It suppresses
* the {@link InterruptedException} that you would otherwise have to
* deal with, and allows automated skipping of the next wait. The
* method {@link #skipNextWait()} will force that thread to immediately return
* from the next call to this method.
* Designing your tests so that they work even if {@link Condition#await()}
* occasionally returns immediately will make your code
* much more robust in face of several potential threading issues.
*
* @param c
* the condition to await on
*/
static public void awaitOn(Condition c) {
MultithreadedTestCase thisTestCase = currentTestCase.get();
if (thisTestCase != null && thisTestCase.failed)
throw new RuntimeException("Test case has failed");
if (skipNextWait.get()) {
skipNextWait.set(false);
return;
}
try {
c.await(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
if (thisTestCase != null && thisTestCase.failed)
throw new RuntimeException("Test case has failed");
throw new AssertionError(e);
}
if (thisTestCase != null && thisTestCase.failed)
throw new RuntimeException("Test case has failed");
}
// ==================
// -- Experimental --
// -- - - - - - - - -
/**
* A ThreadLocal that contains a Random number generator. This is used
* in {@link #mayYield()}
*
* @see #mayYield()
*/
private static ThreadLocal mtcRandomizer = new ThreadLocal() {
@Override
public Random initialValue() {
return new Random();
}
};
/**
* Calling this method from one of the test threads may cause the
* thread to yield. Use this between statements to generate more
* interleavings.
*/
public void mayYield() {
mayYield(0.5);
}
/**
* Calling this method from one of the test threads may cause the
* thread to yield. Use this between statements to generate more
* interleavings.
*
* @param probability
* (a number between 0 and 1) the likelihood that Thread.yield() is called
*/
public void mayYield(double probability) {
if (mtcRandomizer.get().nextDouble() < probability) Thread.yield();
}
}