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

net.sf.javagimmicks.testing.MultiThreadedTestHelper Maven / Gradle / Ivy

There is a newer version: 0.99-alpha1
Show newest version
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
 * happened 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(final 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(final 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(final Iterable> workers) { for (final 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(final int count, final Iterable>> factories) { for (int i = 0; i < count; ++i) { for (final Factory> 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(final int count, final Factory>... 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(final Callable mainWorker) throws AssertionError { return executeWorkers(mainWorker, new LatchWaitStrategy() { @Override public boolean await(final 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(final Callable mainWorker, final long timeout, final TimeUnit unit) throws AssertionError { return executeWorkers(mainWorker, new LatchWaitStrategy() { @Override public boolean await(final 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(final 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(final 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(final int size) { return Executors.newFixedThreadPool(size); } private List> setupWorkers(final 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 (final Callable workerCallable : _workers) { workers.add(new Worker(workerCallable, latch)); } return workers; } private List>> runAll(final ExecutorService executor, final List> workers) { final List>> result = new ArrayList>>(workers.size()); // Submit all the workers into the ExecutorService collecting their // Futures for (final Worker worker : workers) { result.add(executor.submit(worker)); } return result; } private TestResult executeWorkers(final Callable mainWorker, final 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 (final 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(final TestResult result, final List>> results) throws AssertionError { // Walk through the Futures, collect their results and add them to the // main result object for (final 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 (final 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 (final 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(final WorkerResult mainWorkerResult) { _mainWorkerResult = mainWorkerResult; } protected void addWorkerResult(final int id, final 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(final Callable delegate, final CountDownLatch latch) { _delegate = delegate; _latch = latch; } @Override public WorkerResult call() { final WorkerResult result = new WorkerResult(); try { result._result = _delegate.call(); } catch (final AssertionError e) { result._assertionError = e; } catch (final Throwable t) { result._otherError = t; } finally { if (_latch != null) { _latch.countDown(); } } return result; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy