All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.umd.cs.mtc.TestFramework Maven / Gradle / Ivy

package edu.umd.cs.mtc;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 * This class provides static methods to perform a {@link MultithreadedTestCase}.
 * The method {@link #runOnce(MultithreadedTestCase)} can be used
 * to run a MultithreadedTestCase once. The method
 * {@link TestFramework#runManyTimes(MultithreadedTestCase, int)} can be used to
 * run a MultithreadedTestCase multiple times (to see if different interleavings
 * produce different behaviors).
 * 
 * 

* Each test case starts by running the initialize method, followed by all the thread * methods in different threads, and finally the finish method when all threads have * finished. The thread methods are run in a new thread group, and are regulated by a * separate clock thread. The clock thread checks periodically to see if all threads are * blocked. If all threads are blocked and at least one is waiting for a tick, the clock * thread advances the clock to the next desired tick. (A slight delay -- about a * clock period -- is applied before advancing the clock to ensure that this is not done * prematurely and any threads trying to unblock are given a chance to do so.) * *

* The clock thread also detects * deadlock (when all threads are blocked, none are waiting for a tick, and none are in * state TIMED_WAITING), and can stop a test that is going on too long (a thread is in * state RUNNABLE for too long.) * *

* Since the test case threads are placed in a new thread group, any other threads * created by these test cases will be placed in this thread group by default. All * threads in the thread group will be considered by the clock thread when deciding * whether to advance the clock, declare a deadlock, or stop a long-running test. * *

* The framework catches exceptions thrown in the threads and propagates them to * the JUnit test (It also throws AssertionErrors) * *

* This class also defines a number of parameters to be used to control the tests. * Set command line parameter -Dtunit.runLimit=n to cause a test case * to fail if at least one thread stays in a runnable state for more than n * seconds without becoming blocked or waiting for a metronome tick. * Set command line parameter -Dtunit.clockPeriod=p to cause the clock thread * to check the status of all the threads every p milliseconds. * * @see MultithreadedTestCase * @see #runOnce(MultithreadedTestCase) * @see #runManyTimes(MultithreadedTestCase, int) * * @author William Pugh * @author Nathaniel Ayewah * @since 1.0 */ public class TestFramework { /** * If code is translated to JDK 1.4, some functionality may be unavailable * (specifically the ability to check the state of a Thread) */ private static final boolean isJDK14; static { isJDK14 = System.getProperty("java.version").indexOf("1.4.") != -1; } /** * Command line key for indicating the regularity (in milliseconds) * with which the clock thread regulates the thread methods. */ public static final String CLOCKPERIOD_KEY = "tunit.clockPeriod"; /** * Command line key for indicating the time limit (in seconds) for * runnable threads. */ public static final String RUNLIMIT_KEY = "tunit.runLimit"; /** * The default clock period in milliseconds */ public static final Integer DEFAULT_CLOCKPERIOD = 10; /** * The default run limit in seconds */ public static final Integer DEFAULT_RUNLIMIT = 5; /** * Change/set the system property for the clock period * * @param v * the new value for the clock period */ public static void setGlobalClockPeriod(Integer v) { if (v != null) System.setProperty(CLOCKPERIOD_KEY, v.toString()); } /** * Change/set the system property for the run limit * * @param v * the new value for the run limit */ public static void setGlobalRunLimit(Integer v) { if (v != null) System.setProperty(RUNLIMIT_KEY, v.toString()); } /** * Run multithreaded test case multiple times using the default or global settings * for clock period and run limit. This method adds instrumentation to count the * number of times failures occur (an exception is thrown). If the array * failureCount is initialized to be of at least size 1, it returns * this count in failureCount[0]. If failures do occur, it saves the * first failure, and then throws it after running the test count times. * * @param test * The multithreaded test case to run * @param count * the number of times to run the test case * @param failureCount * if this array is initialzed to at least size 1, the number of failures * is returned in failureCount[0] * @throws Throwable * if there is at least one failure -- the first failure is thrown */ public static void runInstrumentedManyTimes(final MultithreadedTestCase test, int count, int [] failureCount) throws Throwable { int failures = 0; Throwable t = null; boolean failed = false; System.out.println("Testing " + test.getClass()); for (int i = 0; i < count; i++) { try { runOnce(test); } catch (Throwable e) { failed = true; failures++; if (t == null) t = e; } if (i%10 == 9) { if (failed) { System.out.print("f"); failed=false; } else System.out.print("."); if (i%100 == 99) System.out.println(" " + (i+1)); } } if (failureCount != null && failureCount.length > 0) failureCount[0] = failures; if (t!=null) throw t; } /** * Run multithreaded test case multiple times using the default or global settings * for clock period and run limit. The value of this is limited, * since even running a test case a thousand or a million times may not * expose any bugs dependent upon particular thread interleavings. * * @param test * The multithreaded test case to run * @param count * the number of times to run the test case * @throws Throwable * -- if any of the test runs fails, the exception is thrown immediately * without completing the rest of the test runs. */ public static void runManyTimes(final MultithreadedTestCase test, int count) throws Throwable { runManyTimes(test, count, null, null); } /** * Run multithreaded test case multiple times. The value of this is limited, * since even running a test case a thousand or a million times may not * expose any bugs dependent upon particular thread interleavings. * * @param test * The multithreaded test case to run * @param count * the number of times to run the test case * @param clockPeriod * The period (in ms) between checks for the clock (or null for * default or global setting) * @param runLimit * The limit to run the test in seconds (or null for default or * global setting) * @throws Throwable * -- if any of the test runs fails, the exception is thrown immediately * without completing the rest of the test runs. */ public static void runManyTimes(final MultithreadedTestCase test, int count, Integer clockPeriod, Integer runLimit) throws Throwable { for (int i = 0; i < count; i++) runOnce(test, clockPeriod, runLimit); } /** * Run a multithreaded test case once, using the default or global settings * for clock period and run limit * * @param test * The multithreaded test case to run * @throws Throwable * if the test runs fails or causes an exception */ public static void runOnce(final MultithreadedTestCase test) throws Throwable { runOnce(test, null, null); } /** * Run multithreaded test case once. * * @param test * The multithreaded test case to run * @param clockPeriod * The period (in ms) between checks for the clock (or null for * default or global setting) * @param runLimit * The limit to run the test in seconds (or null for default or * global setting) * @throws Throwable * if the test runs fails or causes an exception */ public static void runOnce(final MultithreadedTestCase test, Integer clockPeriod, Integer runLimit) throws Throwable { // choose global setting if parameter is null, or default value if there // is no global setting if (clockPeriod == null) clockPeriod = Integer.getInteger(CLOCKPERIOD_KEY, DEFAULT_CLOCKPERIOD); if (runLimit == null) runLimit = Integer.getInteger(RUNLIMIT_KEY, DEFAULT_RUNLIMIT); // prepare run data structures Collection methods = getAllThreads(test); LinkedList threads = new LinkedList(); final Throwable[] error = new Throwable[1]; // invoke initialize method before each run test.initialize(); test.clock = 0; // invoke each thread method in a seperate thread and place all threads in a // new thread group ThreadGroup threadGroup = startMethodThreads(test, methods, threads, error); // start and add clock thread threads.add(startClock(test, threadGroup, error, clockPeriod, runLimit)); // wait until all threads have ended waitForMethodThreads(threads, error); // invoke finish at the end of each run test.finish(); } /** * Use reflection to get the thread methods in this test. Thread methods * start with the name "thread", have no parameters and return void * * @param test * the test case from which to extract methods * @return * a collection of Method objects, one for each thread method */ private static Collection getAllThreads(MultithreadedTestCase test) { Class c = test.getClass(); TreeMap result = new TreeMap(); for (Method m : c.getDeclaredMethods()) { if (m.getName().startsWith("thread") && m.getParameterTypes().length == 0 && m.getReturnType().equals(Void.TYPE)) result.put(m.getName(), m); } return result.values(); } /** * Start and return a clock thread which periodically checks all the test case * threads and regulates them. * *

* If all the threads are blocked and at least one is waiting for a tick, the clock * advances to the next tick and the waiting thread is notified. If none of the * threads are waiting for a tick or in timed waiting, a deadlock is detected. The * clock thread times out if a thread is in runnable or all are blocked and one is * in timed waiting for longer than the runLimit. * * @param test * the test case the clock thread is regulating * @param threadGroup * the thread group containing the running thread methods * @param error * an array containing any Errors/Exceptions that occur in thread methods * or that are thrown by the clock thread * @param clockPeriod * The period (in ms) between checks for the clock (or null for * default or global setting) * @param runLimit * The limit to run the test in seconds (or null for default or * global setting) * @return * The (already started) clock thread */ private static Thread startClock( final MultithreadedTestCase test, final ThreadGroup threadGroup, final Throwable[] error, final int clockPeriod, final int runLimit) { // hold a reference to the current thread. This thread // will be waiting for all the test threads to finish. It // should be interrupted if there is an deadlock or timeout // in the clock thread final Thread mainThread = Thread.currentThread(); Thread t = new Thread("Tick thread") { public void run() { try { long lastProgress = System.currentTimeMillis(); int deadlocksDetected = 0; int readyToTick = 0; while (true) { Thread.sleep(clockPeriod); // Attempt to get a write lock; this succeeds // if clock is not frozen if (!test.clockLock.writeLock().tryLock( 1000L * runLimit, TimeUnit.MILLISECONDS)) { synchronized (test.lock) { test.failed = true; test.lock.notifyAll(); if (error[0] == null) error[0] = new IllegalStateException( "No progress"); mainThread.interrupt(); return; } } synchronized (test.lock) { try { // Get the contents of the thread group int tgCount = threadGroup.activeCount() + 10; Thread [] ths = new Thread [tgCount]; tgCount = threadGroup.enumerate(ths, false); if (tgCount == 0) return; // all threads are done // will set to true to force a check for timeout conditions // and restart the loop boolean checkProgress = false; // will set true if any thread is in state TIMED_WAITING boolean timedWaiting = false; int nextTick = Integer.MAX_VALUE; // examine the threads in the thread group; look for // next tick for (int ii = 0; ii < tgCount; ii++) { Thread t = ths[ii]; if (!isJDK14) { try { if (test.getTrace()) System.out.println(t.getName() + " is in state " + t.getState()); if (t.getState() == Thread.State.RUNNABLE) checkProgress = true; if (t.getState() == Thread.State.TIMED_WAITING) timedWaiting = true; } catch (Throwable e) { // JVM may not support Thread.State checkProgress = false; timedWaiting = true; } } else { // JVM does not support Thread.State checkProgress = false; timedWaiting = true; } Integer waitingFor = test.threads.get(t); if (waitingFor != null && waitingFor > test.clock) nextTick = Math.min(nextTick, waitingFor); } // If not waiting for anything, but a thread is in // TIMED_WAITING, then check progress and loop again if (nextTick == Integer.MAX_VALUE && timedWaiting) checkProgress = true; // Check for timeout conditions and restart the loop if (checkProgress) { if (readyToTick > 0) { if (test.getTrace()) System.out.println("Was Ready to tick too early"); readyToTick = 0; } long now = System.currentTimeMillis(); if (now - lastProgress > 1000L * runLimit) { test.failed = true; test.lock.notifyAll(); if (error[0] == null) error[0] = new IllegalStateException( "No progress"); mainThread.interrupt(); return; } deadlocksDetected = 0; continue; } // Detect deadlock if (nextTick == Integer.MAX_VALUE) { if (readyToTick > 0) { if (test.getTrace()) System.out.println("Was Ready to tick too early"); readyToTick = 0; } if (++deadlocksDetected < 50) { if (deadlocksDetected % 10 == 0 && test.getTrace()) System.out.println("[Detecting deadlock... " + deadlocksDetected + " trys]"); continue; } if (test.getTrace()) System.out.println("Deadlock!"); StringWriter sw = new StringWriter(); PrintWriter out = new PrintWriter(sw); for (Map.Entry e : test.threads .entrySet()) { Thread t = e.getKey(); out.println(t.getName() + " " + t.getState()); for (StackTraceElement st : t .getStackTrace()) { out.println(" " + st); } } test.failed = true; if (error[0] == null) error[0] = new IllegalStateException( "Apparent deadlock\n" + sw.toString()); mainThread.interrupt(); return; } deadlocksDetected = 0; if (++readyToTick < 2) { continue; } readyToTick = 0; // Advance to next tick test.clock = nextTick; lastProgress = System.currentTimeMillis(); // notify any threads that are waiting for this tick test.lock.notifyAll(); if (test.getTrace()) System.out.println("Time is now " + test.clock); } finally { test.clockLock.writeLock().unlock(); } } } } catch (Throwable e) { // killed if (test.getTrace()) System.out.println("Tick thread killed"); } } }; t.setDaemon(true); t.start(); return t; } /** * Wait for all of the test case threads to complete, or for one * of the threads to throw an exception, or for the clock thread to * interrupt this (main) thread of execution. When the clock thread * or other threads fail, the error is placed in the shared error array * and thrown by this method. * * @param threads * List of all the test case threads and the clock thread * @param error * an array containing any Errors/Exceptions that occur in thread methods * or that are thrown by the clock thread * @throws Throwable * The first error or exception that is thrown by one of the threads */ @SuppressWarnings("deprecation") private static void waitForMethodThreads(LinkedList threads, final Throwable[] error) throws Throwable { for (Thread t : threads) try { if (t.isAlive() && error[0] != null) t.stop(); else t.join(); } catch (InterruptedException e1) { if (error[0] != null) throw error[0]; throw new AssertionError(e1); } if (error[0] != null) throw error[0]; } /** * Invoke each of the thread methods in a seperate thread and * place them all in a common (new) thread group. As a side-effect * all the threads are placed in the 'threads' LinkedList parameter, * and any errors detected are placed in the 'error' array parameter. * * @param test * The test case containing the thread methods * @param methods * Collection of the methods to be invoked * @param threads * By the time this method returns, this parameter will * contain all the test case threads * @param error * By the time this method returns, this parameter will * contains the first error thrown by one of the threads. * @return * The thread group for all the newly created test case threads */ private static ThreadGroup startMethodThreads(final MultithreadedTestCase test, Collection methods, final LinkedList threads, final Throwable[] error) { ThreadGroup threadGroup = new ThreadGroup("MTC-Threads"); final CountDownLatch latch = new CountDownLatch(methods.size()); final Semaphore waitForRegistration = new Semaphore(0); for (final Method m : methods) { Runnable r = new Runnable() { public void run() { try { waitForRegistration.release(); latch.countDown(); latch.await(); // At this point all threads are created and released // (in random order?) together to run in parallel test.hello(); makeAccessible(m); m.invoke(test); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof ThreadDeath) return; if (error[0] == null) { error[0] = cause; } signalError(threads); } catch (ThreadDeath e) { // ignore it } catch (Throwable e) { System.out.println(Thread.currentThread().getName() + " caught " + e.getMessage()); if (error[0] == null) error[0] = e; signalError(threads); } finally { test.goodbye(); } } }; String threadName = "thread " + m.getName().substring(6); Thread t = new Thread(threadGroup, r, threadName); threads.add(t); // add thread to map of method threads, mapped by name test.putThread(m.getName(), t); t.start(); waitForRegistration.acquireUninterruptibly(); } return threadGroup; } /** * Stop all test case threads and clock thread, except the thread from * which this method is called. This method is used when a thread is * ready to end in failure and it wants to make sure all the other * threads have ended before throwing an exception. * * @param threads * LinkedList of all the test case threads and the clock thread */ @SuppressWarnings("deprecation") private static void signalError(final LinkedList threads) { Thread currentThread = Thread.currentThread(); for (Thread t : threads) if (t != currentThread) { AssertionError assertionError = new AssertionError(t.getName() + " killed by " + currentThread.getName()); assertionError.setStackTrace(t.getStackTrace()); t.stop(assertionError); } } /** * Change security access on an accessible object (e.g. Method * or Constructor) so it can be invoked * * @param obj * the object to make accessible */ private static void makeAccessible(final AccessibleObject obj) { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { obj.setAccessible(true); return null; } }); } /** * Scan through a given class c to find any inner classes * that implement {@link junit.framework.Test}. If the classes have * a no-arg constructor, they are instantiated added them to a TestSuite. * If the inner classes are not declared static then an instance of the * class represented by c (created with a no-arg constructor) * is used to construct the inner class. If no relevant inner classes are * found, then an empty TestSuite is returned. * * @param c * the class to scan for relevant inner classes * @return * A TestSuite containing one test for each relevant inner class */ public static TestSuite buildTestSuite(Class c) { return buildTestSuite(c, c.getName()); } /** * Scan through a given class c to find any inner classes * that implement {@link junit.framework.Test}. If the classes have * a no-arg constructor, they are instantiated added them to a TestSuite. * If the inner classes are not declared static then an instance of the * class represented by c (created with a no-arg constructor) * is used to construct the inner class. If no relevant inner classes are * found, then an empty TestSuite is returned. * *

* If the class is a TestCase, then an instance of it is passed to any * non-static innerclass and the appropriate setUp and tearDown methods * are called. * * @param c * the class to scan for relevant inner classes * @param suiteName * A name for the TestSuite * @return * A TestSuite containing one test for each relevant inner class */ public static TestSuite buildTestSuite(Class c, String suiteName) { TestSuite suite = new TestSuite(suiteName); final Class [] CNULL = null; final Object [] ONULL = null; // A no-arg constructor for c will be created if necessary Constructor mainCons = null; Class [] innerClasses = c.getDeclaredClasses(); for (Class innerClass : innerClasses) { // only consider subclasses of junit.framework.Test if (!Test.class.isAssignableFrom(innerClass)) continue; // check for static class with no-arg constructor try { Constructor cons = innerClass.getDeclaredConstructor(CNULL); if (!cons.isAccessible()) makeAccessible(cons); suite.addTest( (Test) cons.newInstance(ONULL) ); continue; } catch (Exception e) { } // check for non-static class with no-arg constructor try { Constructor cons = innerClass.getDeclaredConstructor(new Class[]{c}); if (!cons.isAccessible()) makeAccessible(cons); // try to create instance of outer class if (mainCons == null) { mainCons = c.getDeclaredConstructor(CNULL); if (!mainCons.isAccessible()) makeAccessible(mainCons); } // Successful! Create test from non-static class Object outerInstance = mainCons.newInstance(ONULL); Test test = (Test) cons.newInstance( new Object[]{outerInstance} ); if (outerInstance instanceof TestCase && test instanceof MultithreadedTest) addSetUpAndTearDown( (MultithreadedTest) test, (TestCase) outerInstance ); suite.addTest(test); continue; } catch (Exception e) { e.printStackTrace(); } // if we get to this point, then inner-class is ignored } return suite; } /** * Update a given test to call "setUp" and "tearDown" before and after * running the test respectively. Update is done by creating a new test. * This assumes that the provided TestCase instance was used to create * the Test, otherwise it does little good to call the "setUp" and "tearDown" * method in the TestCase. * * @param mtc * the test to update * @param tc * the TestCase that contains the setUp and tearDown methods called * @throws * Any exceptions that occur along the process. In this case, just * use the old uninstrumented Test. */ public static void addSetUpAndTearDown(MultithreadedTest mtc, TestCase tc) throws SecurityException, NoSuchMethodException { Method setUp = null, tearDown = null; setUp = TestCase.class.getDeclaredMethod("setUp", (Class []) null); if (!setUp.isAccessible()) makeAccessible(setUp); tearDown = TestCase.class.getDeclaredMethod("tearDown", (Class []) null); if (!tearDown.isAccessible()) makeAccessible(tearDown); mtc.addSetUpAndTearDown(tc, setUp, tearDown); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy