package org.threadly.test.concurrent; import java.util.concurrent.locks.LockSupport; import java.util.function.Supplier; import org.threadly.util.ArgumentVerifier; import org.threadly.util.Clock; /** * {@link TestCondition} in unit test, designed to check a condition for something that is * happening in a different thread. Allowing a test to efficiently block till the testable * action has finished. * <p> * This tool is used often within threadly's own unit tests. Please use those as examples using * this class. * * @since 1.0.0 */ public class TestCondition { protected static final int DEFAULT_POLL_INTERVAL = 10; protected static final int DEFAULT_TIMEOUT = 1000 * 10; protected static final int SPIN_THRESHOLD = 10; private final Supplier<Boolean> condition; /** * This constructor is expected to have {@link #get()} overridden, otherwise an exception will be * thrown when this condition is checked/used. */ public TestCondition() { this(() -> { throw new RuntimeException("Must override get() or provide condition supplier"); }); } /** * Construct a new test condition with a provided supplier for when the state changes. * Alternatively this can be constructed with {@link #TestCondition()} and then the condition can * be reported by overriding {@link #get()}. * * @since 5.0.0 * @param condition Condition to check */ public TestCondition(Supplier<Boolean> condition) { ArgumentVerifier.assertNotNull(condition, "condition"); this.condition = condition; } /** * Getter for the conditions current state. If constructed with {@link #TestCondition()} this * must be overridden. Otherwise this will return the result from the provided supplier at * construction. * * @return Test condition state, {@code true} if ready */ public boolean get() { return condition.get(); } /** * Blocks till condition is true, useful for asynchronism operations, waiting for them to * complete in other threads during unit tests. * <p> * This uses a default timeout of 10 seconds, and a poll interval of 10ms */ public void blockTillTrue() { blockTillTrue(DEFAULT_TIMEOUT, DEFAULT_POLL_INTERVAL); } /** * Blocks till condition is true, useful for asynchronism operations, waiting for them to * complete in other threads during unit tests. * <p> * This uses the default poll interval of 10ms * * @param timeoutInMillis time to wait for value to become true */ public void blockTillTrue(int timeoutInMillis) { blockTillTrue(timeoutInMillis, DEFAULT_POLL_INTERVAL); } /** * Blocks till condition is true, useful for asynchronism operations, waiting for them to * complete in other threads during unit tests. * * @param timeoutInMillis time to wait for value to become true * @param pollIntervalInMillis time to sleep between checks */ public void blockTillTrue(int timeoutInMillis, int pollIntervalInMillis) { long startTime = Clock.accurateForwardProgressingMillis(); long now = startTime; boolean lastResult; while (! (lastResult = get()) && ! Thread.currentThread().isInterrupted() && (now = Clock.accurateForwardProgressingMillis()) - startTime < timeoutInMillis) { if (pollIntervalInMillis > SPIN_THRESHOLD) { LockSupport.parkNanos(Clock.NANOS_IN_MILLISECOND * pollIntervalInMillis); } } if (! lastResult) { throw new ConditionTimeoutException("Still false after " + (now - startTime) + "ms, interrupted: " + Thread.currentThread().isInterrupted()); } } /** * Thrown if condition is still false after a given timeout. * <p> * The reason this exception is public is to allow test implementors to catch this exception * specifically and handle it if necessary. */ public static class ConditionTimeoutException extends RuntimeException { private static final long serialVersionUID = 7445447193772617274L; /** * Constructor for new TimeoutException. */ public ConditionTimeoutException() { super(); } /** * Constructor for new TimeoutException. * * @param msg Exception message */ public ConditionTimeoutException(String msg) { super(msg); } } }