package org.threadly.test.concurrent;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import org.threadly.concurrent.collections.ConcurrentArrayList;
import org.threadly.util.Clock;
/**
* Generic runnable implementation that can be used in unit tests for verifying execution occurred.
* <p>
* This structure is used extensively within threadly's own unit tests. Please use those as
* examples using this class.
*
* @since 1.0.0
*/
public class TestRunnable implements Runnable {
protected static final int DEFAULT_TIMEOUT_PER_RUN = 10 * 1000;
protected static final int RUN_CONDITION_POLL_INTERVAL = 20;
protected final long creationForwardProgressingMillis;
private final ConcurrentArrayList<Long> runTime;
private final AtomicInteger runCount;
private final AtomicInteger currentRunningCount;
private volatile int runDelayInMillis;
private volatile boolean ranConcurrent;
/**
* Constructs a new runnable for unit testing.
*/
public TestRunnable() {
this(0);
}
/**
* Constructs a new runnable for unit testing.
* <p>
* This constructor allows the parameter for the runnable to sleep after {@link #handleRunStart()}
* was called and before {@link #handleRunFinish()} is called.
*
* @param runTimeInMillis time for runnable to sleep in milliseconds
*/
public TestRunnable(int runTimeInMillis) {
this.creationForwardProgressingMillis = Clock.accurateForwardProgressingMillis();
setRunDelayInMillis(runTimeInMillis);
this.runTime = new ConcurrentArrayList<>(0, 1);
this.runCount = new AtomicInteger(0);
this.currentRunningCount = new AtomicInteger(0);
this.ranConcurrent = false;
}
/**
* Changes the amount of time the runnable will sleep/block when called. This change will only
* be for future run calls that are not already blocking.
*
* @param runDelayInMillis new amount to wait when run is called
*/
public void setRunDelayInMillis(int runDelayInMillis) {
this.runDelayInMillis = runDelayInMillis;
}
/**
* Getter for the currently set amount of time the {@link TestRunnable} will block after
* {@link #handleRunStart()} and before {@link #handleRunFinish()} is called.
*
* @return current set sleep time in milliseconds
*/
public int getRunDelayInMillis() {
return runDelayInMillis;
}
/**
* Returns the millis returned from {@link Clock#accurateForwardProgressingMillis()}, that was
* recorded when this runnable was constructed.
*
* @return Forward progressing time in milliseconds {@link TestRunnable} was constructed
*/
public long getCreationTime() {
return creationForwardProgressingMillis;
}
/**
* Check if this instance has ever been detected to run concurrently. Keep in mind that just
* because this call returns false does not guarantee the runnable is incapable of running
* parallel. It does it's best to detect a situation where it is started while another instance
* has not returned from the run function. Adding a delay in {@link #handleRunStart()},
* {@link #handleRunFinish()}, or in the constructor increases the chances of detecting
* concurrency.
*
* @return True if this instance ran in parallel at least once
*/
public boolean ranConcurrently() {
return ranConcurrent;
}
/**
* Getter to check if the runnable has run exactly once.
*
* @return {@code true} if the runnable has been called once
*/
public boolean ranOnce() {
return runCount.get() == 1;
}
/**
* Check to see if the run function has started but has not completed returned yet.
*
* @return True if the runnable's run function is being called currently.
*/
public boolean isRunning() {
return currentRunningCount.get() != 0;
}
/**
* Getter for the number of times the run function has completed.
*
* @return The number of times the run function has been called
*/
public int getRunCount() {
return runCount.get();
}
/**
* This function blocks till the first run completes then will return the time until the first
* run started it's call.
*
* @return The amount of time between construction and run being called
*/
public long getDelayTillFirstRun() {
return getDelayTillRun(1);
}
/**
* This function blocks till the run provided, and then gets the time between creation and a
* given run.
*
* @param runNumber the run count to get delay to
* @return The amount of time between construction and run being called
*/
public long getDelayTillRun(int runNumber) {
return getDelayTillRun(runNumber, DEFAULT_TIMEOUT_PER_RUN * runNumber);
}
/**
* This function blocks till the run provided, and
* then gets the time between creation and a given run.
*
* @param runNumber the run count to get delay to
* @param timeout timeout to wait for given run count to finish
* @return The amount of time between construction and run being called
*/
public long getDelayTillRun(int runNumber, int timeout) {
blockTillFinished(timeout, runNumber);
Collections.sort(runTime);
return runTime.get(runNumber - 1) - getCreationTime();
}
/**
* Blocks until run has completed at least once. Will throw an exception if runnable does not
* run within 10 seconds.
*/
public void blockTillFinished() {
blockTillFinished(DEFAULT_TIMEOUT_PER_RUN, 1);
}
/**
* Blocks until run has completed at least once.
*
* @param timeout time to wait for run to be called before throwing exception
*/
public void blockTillFinished(int timeout) {
blockTillFinished(timeout, 1);
}
/**
* Blocks until run completed been called the provided quantity of times.
*
* @param timeout time to wait for run to be called before throwing exception
* @param expectedRunCount run count to wait for
*/
public void blockTillFinished(int timeout, int expectedRunCount) {
final int blockRunCount = expectedRunCount;
new TestCondition() {
@Override
public boolean get() {
int finishCount = runCount.get();
if (finishCount < blockRunCount) {
return false;
} else if (finishCount > blockRunCount) {
return true;
} else { // they are equal
return currentRunningCount.get() == 0;
}
}
}.blockTillTrue(timeout, RUN_CONDITION_POLL_INTERVAL);
}
/**
* Blocks until run has been called at least once. Will throw an exception if run is not called
* within 10 seconds.
*/
public void blockTillStarted() {
blockTillStarted(DEFAULT_TIMEOUT_PER_RUN);
}
/**
* Blocks until run has been called at least once.
*
* @param timeout time to wait for run to be called before throwing exception
*/
public void blockTillStarted(int timeout) {
new TestCondition() {
@Override
public boolean get() {
return ! runTime.isEmpty();
}
}.blockTillTrue(timeout, RUN_CONDITION_POLL_INTERVAL);
}
@Override
public final void run() {
int startRunningCount = currentRunningCount.incrementAndGet();
runTime.addLast(Clock.accurateForwardProgressingMillis());
try {
handleRunStart();
} catch (InterruptedException e) {
// ignored, just reset status
Thread.currentThread().interrupt();
} finally {
if (runDelayInMillis > 0) {
TestUtils.sleep(runDelayInMillis);
}
runCount.incrementAndGet();
try {
handleRunFinish();
} finally {
ranConcurrent = currentRunningCount.decrementAndGet() != 0 || // must be first to ensure decrement is called
ranConcurrent || startRunningCount != 1;
}
}
}
/**
* Function to be overloaded by extending classes if more data or operations need to happen at
* the run point.
* <p>
* This is also the first call to be made in the runnable, but all necessary
* {@link TestRunnable} actions are in a finally block so it is safe to throw any exceptions
* necessary here.
*
* @throws InterruptedException only InterruptedExceptions will be swallowed
*/
public void handleRunStart() throws InterruptedException {
// only public to include in javadocs, otherwise could be protected
// nothing in default implementation
}
/**
* Function to be overloaded by extending classes if more data or operations need to happen at
* the run point.
* <p>
* This is the last call to be made in the runnable. If you want a {@link RuntimeException} to
* get thrown to the caller, it must be thrown from here.
*/
public void handleRunFinish() {
// only public to include in javadocs, otherwise could be protected
// nothing in default implementation
}
}