net.sf.javagimmicks.testing.MultiThreadedTestHelper Maven / Gradle / Ivy
Show all versions of gimmicks-testing Show documentation
package net.sf.javagimmicks.testing;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import net.sf.javagimmicks.lang.CallableRunnableAdapter;
import net.sf.javagimmicks.lang.Factory;
/**
* This class helps to execute multi-threaded unit tests in a manner of executing a number of workers
* in parallel. Results (positive or negative) are collected into a {@link TestResult} object which
* can be used to examine what happend within the workers. Depending on the value of {@link #isAutoFail()}
* automatically a respective {@link AssertionError} will be thrown if any of the workers ran into any problem
*
* Additionally a "main" worker can be submitted when starting a test run which will then be executed
* within the main test thread after all other workers have been started.
* @param the type of result objects the workers can produce
*/
public class MultiThreadedTestHelper
{
private static String LINE_SEP = System.getProperty("line.separator");
private List> _workers = new LinkedList>();
private boolean _autoFail;
/**
* Creates a new instance with the given auto-fail mode
* @param autoFail the active-state of the auto-fail mode
* @see #setAutoFail(boolean)
*/
public MultiThreadedTestHelper(boolean autoFail)
{
_autoFail = autoFail;
}
/**
* Create a new instance with auto-fail set to true
* @see #setAutoFail(boolean)
*/
public MultiThreadedTestHelper()
{
this(true);
}
/**
* Returns if auto-fail is active
* @return if auto-fail is active
* @see #setAutoFail(boolean)
*/
public boolean isAutoFail()
{
return _autoFail;
}
/**
* Activates or deactivates the auto-fail mode.
* If active a resepctive {@link AssertionError} will automatically thrown after test runs
* if any worker failed
* @param autoFail the desired active-state of the auto-fail mode
*/
public void setAutoFail(boolean autoFail)
{
_autoFail = autoFail;
}
/**
* Adds a bunch of workers to be executed within test runs.
* Workers will be started in the same order as they were added.
* @param workers an {@link Iterable} of workers represented as {@link Callable} objects
* (see {@link CallableRunnableAdapter} for adding {@link Runnable} objects instead)
* @see CallableRunnableAdapter
*/
public void addWorkers(Iterable extends Callable> workers)
{
for(Callable worker : workers)
{
if(worker != null)
{
_workers.add(worker);
}
}
}
/**
* Convenience method for massively adding workers.
* Worker are not specified directly, instead a number {@link Factory} instances
* can be provided together with a count that determines, how many instances
* each {@link Factory} should add here.
*
* This means, the number of workers added is the multiplication of the given
* count
parameter and the number of given {@link Factory} instances
* @param count the number of instances each provided {@link Factory} should create
* @param factories any number of {@link Factory} instances that should create workers
*/
public void addWorkers(int count, Iterable>> factories)
{
for(int i = 0; i < count; ++i)
{
for(Factory extends Callable> factory : factories)
{
if(factory != null)
{
final Callable worker = factory.create();
if(worker != null)
{
_workers.add(worker);
}
}
}
}
}
/**
* Convenience method for {@link #addWorkers(int, Iterable)} that allows specifying
* {@link Factory} instances as var-args list
* @param count the number of instances each provided {@link Factory} should create
* @param factories any number of {@link Factory} instances that should create workers
* @see #addWorkers(int, Factory...)
*/
public void addWorkers(int count, Factory extends Callable>... factories)
{
addWorkers(count, Arrays.asList(factories));
}
/**
* Starts a test run with all registered workers and a "main" worker.
*
* ATTENTION: The calling thread will be blocked until all worker have finished.
* @param mainWorker the "main" worker which will be started within the main test thread
* after all "sub" workers have been started
* @return the {@link TestResult} containing all test result details (including any thrown errors)
* @throws AssertionError if anything in the test run went wrong and {@link #isAutoFail()} is true
* @see #isAutoFail()
*/
public TestResult executeWorkers(Callable mainWorker) throws AssertionError
{
return executeWorkers(mainWorker, new LatchWaitStrategy()
{
@Override
public boolean await(CountDownLatch latch) throws InterruptedException
{
latch.await();
return true;
}
});
}
/**
* Starts a test run with all registered workers and a "main" worker aborting the test after a given time.
* @param mainWorker the "main" worker which will be started within the main test thread
* after all "sub" workers have been started
* @param timeout the amount of time to wait with the given {@link TimeUnit} until the test run is aborted
* @param unit the unit of time to wait until the test run is aborted
* @return the {@link TestResult} containing all test result details (including any thrown errors)
* @throws AssertionError if anything in the test run went wrong and {@link #isAutoFail()} is true
* @see #isAutoFail()
*/
public TestResult executeWorkers(Callable mainWorker, final long timeout, final TimeUnit unit) throws AssertionError
{
return executeWorkers(mainWorker, new LatchWaitStrategy()
{
@Override
public boolean await(CountDownLatch latch) throws InterruptedException, AssertionError
{
return latch.await(timeout, unit);
}
});
}
/**
* Convenience method for {@link #executeWorkers(Callable)} but taking the main worker as a {@link Runnable}
* @see #executeWorkers(Callable)
*/
public TestResult executeWorkers(Runnable mainWorker) throws AssertionError
{
return executeWorkers(mainWorker != null ? new CallableRunnableAdapter(mainWorker) : (Callable)null);
}
/**
* Convenience method for {@link #executeWorkers(Callable, long, TimeUnit)} but taking the main worker as a {@link Runnable}
* @see #executeWorkers(Callable, long, TimeUnit)
*/
public TestResult executeWorkers(Runnable mainWorker, final long timeout, final TimeUnit unit) throws AssertionError
{
return executeWorkers(mainWorker != null ? new CallableRunnableAdapter(mainWorker) : (Callable)null, timeout, unit);
}
/**
* Convenience method for {@link #executeWorkers(Callable)} that will run the test without a "main" worker
* @see #executeWorkers(Callable)
*/
public TestResult executeWorkers() throws AssertionError
{
return executeWorkers((Callable)null);
}
/**
* Convenience method for {@link #executeWorkers(Callable, long, TimeUnit)} that will run the test without a "main" worker
* @see #executeWorkers(Callable, long, TimeUnit)
*/
public TestResult executeWorkers(final long timeout, final TimeUnit unit) throws AssertionError
{
return executeWorkers((Callable)null, timeout, unit);
}
/**
* Testers can override this method to provide their own implementation of {@link ExecutorService} to run the
* "sub" workers. By default the {@link ExecutorService} is created by calling {@link Executors#newFixedThreadPool(int)}
* @param size the number of workers that have to be executed
* @return an instance of {@link ExecutorService} that should execute the workers
*/
protected ExecutorService getExecutorService(int size)
{
return Executors.newFixedThreadPool(size);
}
private List> setupWorkers(CountDownLatch latch)
{
final List> workers = new ArrayList>(_workers.size());
// Wrap the worker Callables into a respective internal Worker (which will take care about collecting results)
for(Callable workerCallable : _workers)
{
workers.add(new Worker(workerCallable, latch));
}
return workers;
}
private List>> runAll(ExecutorService executor, List> workers)
{
final List>> result = new ArrayList>>(workers.size());
// Submit all the workers into the ExecutorService collecting their Futures
for(Worker worker : workers)
{
result.add(executor.submit(worker));
}
return result;
}
private TestResult executeWorkers(Callable mainWorker, LatchWaitStrategy latchWaitStrategy) throws AssertionError
{
final int workerCount = _workers.size();
// Let us get an ExecutorService for the current size of workers
final ExecutorService executor = getExecutorService(workerCount);
// Create a CountDownlatch with the same size to be later able to join after their termination
final CountDownLatch latch = new CountDownLatch(workerCount);
// Create the workers and hand them over our latch (they will report to it after termination)
final List> workers = setupWorkers(latch);
// Start the workers with the ExecutorService
final List>> workerResults = runAll(executor, workers);
// Prepare the result object
final TestResult result = new TestResult();
try
{
// If we have a "Main Worker", we call it now directly
if(mainWorker != null)
{
// Execute it with our normal worker helper object
final WorkerResult mainWorkerResult = new Worker(mainWorker, null).call();
// Adapt the "Main Worker's" result to our result
result.setMainWorkerResult(mainWorkerResult);
}
// Now it's time to join - we wait for the Latch with the given strategy
// But we can skip this if the main worker already failed
if(result.isSuccess())
{
try
{
if(!latchWaitStrategy.await(latch))
{
result._mainWorkerResult._assertionError = new AssertionError("Workers did not terminate within given time");
}
}
catch(InterruptedException e)
{
result._mainWorkerResult._interruptedException = e;
}
}
}
// Ensure the Executor to be shutdown (it might not have been shutdown
// e.g. because workers are still running or main worker failed)
finally
{
if(!executor.isShutdown())
{
executor.shutdownNow();
}
}
// Collect and register the results of all already finished workers
addWorkerResults(result, workerResults);
// If auto-fail is activated and one or more of the workers failed, throw an according AssertionError
if(_autoFail)
{
result.assertSuccessful();
}
return result;
}
private void addWorkerResults(TestResult result, final List>> results) throws AssertionError
{
// Walk through the Futures, collect their results and add them to the main result object
for(ListIterator>> iter = results.listIterator(); iter.hasNext();)
{
final Future> workerFuture = iter.next();
// There is only a result if the execution ended
if(workerFuture.isDone())
{
try
{
result.addWorkerResult(iter.previousIndex(), workerFuture.get());
}
// Theoretically we ensured that we won't get here
catch(Exception e)
{
throw new AssertionError("Unexpected exception while getting worker results: " + e.toString());
}
}
}
}
/**
* Represents the results of one single worker.
*
* There can be one of four results
*
* - The resulting object produced by the worker if it did not fail - get via {@link #getResult()}
* - An {@link AssertionError} if an assertion within the worker failed - get via {@link #getAssertionError()}
* - An {@link InterruptedException} if the worker was interrupted - get via {@link #getInterruptedException()}
* - Any other {@link Throwable} if it was thrown by the worker - get via {@link #getOtherError()}
*
*
* @param the type of result objects the worker could produce
*/
public static class WorkerResult
{
protected AssertionError _assertionError;
protected InterruptedException _interruptedException;
protected Throwable _otherError;
protected R _result;
protected WorkerResult()
{
}
/**
* Returns an according fail message if the worker did not finish successfully
* @return the according fail message or null
if the worker finished successfully
* @see #isSuccess()
*/
public String buildFailMessage()
{
if(_assertionError != null)
{
return _assertionError.toString();
}
else if(_interruptedException != null)
{
return _interruptedException.toString();
}
else if(_otherError != null)
{
return _otherError.toString();
}
else
{
return null;
}
}
/**
* Return the {@link AssertionError} that might have been thrown by the worker
* @return the resulting {@link AssertionError} or null
if none occurred
* @see #isSuccess()
*/
public AssertionError getAssertionError()
{
return _assertionError;
}
/**
* Return the {@link InterruptedException} that might have been thrown by the worker
* @return the resulting {@link InterruptedException} or null
if none occurred
* @see #isSuccess()
*/
public InterruptedException getInterruptedException()
{
return _interruptedException;
}
/**
* Return the non-{@link AssertionError} {@link Throwable} that might have been thrown by the worker
* @return the resulting {@link Throwable} or null
if none occurred
* @see #isSuccess()
*/
public Throwable getOtherError()
{
return _otherError;
}
/**
* Return the result object that the worker might have been produced
* @return the result object or null
if the worker failed
* @see #isSuccess()
*/
public R getResult()
{
return _result;
}
/**
* Checks if the associated worker was successfully finished
* @return if the associated worker was successfully finished
*/
public boolean isSuccess()
{
return _assertionError == null && _interruptedException == null && _otherError == null;
}
}
/**
* Acts as a container for the results of a test run which consists in particular of the results
* of the "main" worker and the results of all "sub" workers.
* @param the result type of the "main" worker
* @param the result type of the "sub" workers
*/
public static class TestResult
{
protected WorkerResult _mainWorkerResult = new WorkerResult();
protected final SortedMap> _workerResults = new TreeMap>();
protected final SortedMap> _failedWorkerResults = new TreeMap>();
protected TestResult() {}
/**
* Checks if the whole test run was successful i.e. all workers (including the "main" one) were successful
* @return if the whole test run was successful
*/
public boolean isSuccess()
{
return _mainWorkerResult.isSuccess() && _failedWorkerResults.isEmpty();
}
/**
* Builds a composite fail message from all the wokers' results
* @return the composed fail message
*/
public String buildFailMessage()
{
final StringBuilder result = new StringBuilder();
if(!_mainWorkerResult.isSuccess())
{
result.append("Main worker failed with reason: ").append(_mainWorkerResult.buildFailMessage()).append(LINE_SEP);
}
for(Entry> entry : _failedWorkerResults.entrySet())
{
result.append("Worker thread ").append(entry.getKey()).append(" failed with reason: ").append(entry.getValue().buildFailMessage()).append(LINE_SEP);
}
return result.toString();
}
/**
* Throws an {@link AssertionError} if the test was not successful.
*
* Calls {@link #buildFailMessage()} for getting the error message
* @throws AssertionError the resulting
* @see #buildFailMessage()
* @see #isSuccess()
*/
public void assertSuccessful() throws AssertionError
{
if(!isSuccess())
{
throw new AssertionError(buildFailMessage());
}
}
/**
* Returns the result of the main worker's call as {@link TestResult} object
* @return the result of the main worker's call
* @see WorkerResult
*/
public WorkerResult getMainWorkerResult()
{
return _mainWorkerResult;
}
/**
* Return the results of the sub workers as a {@link SortedMap} which identifies
* each worker by a unique numbers (matching the order in which the workers were
* started)
* @return a {@link SortedMap} containing all {@link WorkerResult}s of all workers
* @see WorkerResult
*/
public SortedMap> getWorkerResults()
{
return Collections.unmodifiableSortedMap(_workerResults);
}
/**
* Return the results of the all sub workers that failed as a {@link SortedMap}
* which identifies each worker by a unique numbers
* (matching the order in which the workers were started)
* @return a {@link SortedMap} containing all {@link WorkerResult}s of all failed workers
* @see WorkerResult
*/
public SortedMap> getFailedWorkerResults()
{
return Collections.unmodifiableSortedMap(_failedWorkerResults);
}
protected void setMainWorkerResult(WorkerResult mainWorkerResult)
{
_mainWorkerResult = mainWorkerResult;
}
protected void addWorkerResult(int id, WorkerResult result)
{
_workerResults.put(id, result);
if(!result.isSuccess())
{
_failedWorkerResults.put(id, result);
}
}
}
private interface LatchWaitStrategy
{
boolean await(CountDownLatch latch) throws InterruptedException;
}
private static class Worker implements Callable>
{
private final Callable _delegate;
private final CountDownLatch _latch;
public Worker(Callable delegate, CountDownLatch latch)
{
_delegate = delegate;
_latch = latch;
}
@Override
public WorkerResult call()
{
final WorkerResult result = new WorkerResult();
try
{
result._result = _delegate.call();
}
catch(AssertionError e)
{
result._assertionError = e;
}
catch(Throwable t)
{
result._otherError = t;
}
finally
{
if(_latch != null)
{
_latch.countDown();
}
}
return result;
}
}
}