package org.threadly.test.concurrent; import java.util.concurrent.TimeoutException; import org.threadly.util.Clock; /** * A simple class for verifying multi-threaded unit tests. If any thread has a failed a failed * {@code assert} or call to {@link #fail()} the main threads call to {@link #waitForTest()} will * throw an exception to indicate the test as a failure. * <p> * This class also provides a way to control the flow of a unit test by blocking main test thread * until {@link #signalComplete()} is called from the other thread. * * @since 1.0.0 */ public class AsyncVerifier { protected static final int DEFAULT_TIMEOUT = 1000 * 10; protected final Object notifyLock; private int signalCount; private RuntimeException failure; /** * Constructs a new {@link AsyncVerifier}. */ public AsyncVerifier() { notifyLock = new Object(); signalCount = 0; failure = null; } /** * Waits for a default of 10 seconds, or until signalComplete has been called once, or until a * failure occurs. If signalComplete has been called before this, this call will never block. * * @throws InterruptedException Thrown if thread is interrupted while waiting * @throws TimeoutException Thrown if timeout occurs without signalComplete being called */ public void waitForTest() throws InterruptedException, TimeoutException { waitForTest(DEFAULT_TIMEOUT, 1); } /** * Waits a specified amount of time, or until signalComplete has been called once, or until a * failure occurs. If signalComplete has been called before this, this call will never block. * * @param timeoutInMs Timeout to wait for signalComplete action to occur * @throws InterruptedException Thrown if thread is interrupted while waiting * @throws TimeoutException Thrown if timeout occurs without signalComplete being called */ public void waitForTest(long timeoutInMs) throws InterruptedException, TimeoutException { waitForTest(timeoutInMs, 1); } /** * Waits a specified amount of time, or until signalComplete has been called a specified amount * of times, or until a failure occurs. * <p> * If {@link #waitForTest()} is being called multiple times on the same instance, the * signalComplete count is NOT reset. So you must either create new instances, or pass in a * larger value for the expected signalComplete count. * * @param timeoutInMs Timeout to wait for signalComplete action to occur * @param signalCount Amount of signalComplete calls to expect before unblocking * @throws InterruptedException Thrown if thread is interrupted while waiting * @throws TimeoutException Thrown if timeout occurs without signalComplete being called */ public void waitForTest(long timeoutInMs, int signalCount) throws InterruptedException, TimeoutException { long startTime = Clock.accurateForwardProgressingMillis(); long remainingWaitTime = timeoutInMs; synchronized (notifyLock) { while (this.signalCount < signalCount && remainingWaitTime > 0 && failure == null) { notifyLock.wait(remainingWaitTime); remainingWaitTime = timeoutInMs - (Clock.accurateForwardProgressingMillis() - startTime); } } if (failure != null) { throw failure; } else if (remainingWaitTime <= 0) { throw new TimeoutException(); } // if neither are true we exited normally } /** * Call to indicate that this thread has finished, and should notify the waiting main test * thread that the test may be complete. */ public void signalComplete() { synchronized (notifyLock) { signalCount++; notifyLock.notifyAll(); } } /** * Verifies that the passed in condition is true. If it is not an exception is thrown in this * thread, as well as possibly any blocking thread waiting at {@link #waitForTest()}. * * @param condition condition to verify is {@code true} */ public void assertTrue(boolean condition) { if (! condition) { synchronized (notifyLock) { failure = new TestFailure("assertTrue failure"); notifyLock.notifyAll(); } throw failure; } } /** * Verifies that the passed in condition is false. If it is not an exception is thrown in this * thread, as well as possibly any blocking thread waiting at {@link #waitForTest()}. * * @param condition condition to verify is {@code false} */ public void assertFalse(boolean condition) { if (condition) { synchronized (notifyLock) { failure = new TestFailure("assertFalse failure"); notifyLock.notifyAll(); } throw failure; } } /** * Verifies that the passed in object is null. If it is not null an exception is thrown in this * thread, as well as possibly any blocking thread waiting at {@link #waitForTest()}. * * @param o Object to verify is {@code null} */ public void assertNull(Object o) { if (o != null) { synchronized (notifyLock) { failure = new TestFailure("Object is not null: " + o); notifyLock.notifyAll(); } throw failure; } } /** * Verifies that the passed in object is not null. If it is null an exception is thrown in this * thread, as well as possibly any blocking thread waiting at {@link #waitForTest()}. * * @param o Object to verify is not {@code null} */ public void assertNotNull(Object o) { if (o == null) { synchronized (notifyLock) { failure = new TestFailure("Object is null"); notifyLock.notifyAll(); } throw failure; } } /** * Verifies that the passed in values are equal using the o1.equals(o2) relationship. If this * check fails an exception is thrown in this thread, as well as any blocking thread waiting at * {@link #waitForTest()}. * * @param o1 First object to compare against * @param o2 Second object to compare against */ public void assertEquals(Object o1, Object o2) { boolean nullMissmatch = (o1 == null && o2 != null) || (o1 != null && o2 == null); if (nullMissmatch || ! ((o1 == null && o2 == null) || o1.equals(o2))) { synchronized (notifyLock) { failure = new TestFailure(o1 + " != " + o2); notifyLock.notifyAll(); } throw failure; } } /** * Marks a failure with no cause. This causes an exception to be thrown in the calling thread, * as well was any blocking thread waiting at {@link #waitForTest()}. */ public void fail() { fail(""); } /** * Marks a failure with a specified message. This causes an exception to be thrown in the * calling thread, as well was any blocking thread waiting at {@link #waitForTest()}. * * @param message Message to be provided in failure exception */ public void fail(String message) { synchronized (notifyLock) { failure = new TestFailure(message); notifyLock.notifyAll(); } throw failure; } /** * Marks a failure with a specified throwable cause. This causes an exception to be thrown in * the calling thread, as well was any blocking thread waiting at {@link #waitForTest()}. * * @param cause Throwable cause to be provided in failure exception */ public void fail(Throwable cause) { synchronized (notifyLock) { failure = new TestFailure(cause); notifyLock.notifyAll(); } throw failure; } /** * Small exception to represent when a test failure occurs. * * @since 1.0.0 */ protected static class TestFailure extends RuntimeException { private static final long serialVersionUID = -4683332806581392944L; private TestFailure(String failureMsg) { super(failureMsg); } private TestFailure(Throwable cause) { super(cause); } } }