package org.codefx.libfx.concurrent.when; import static org.junit.Assert.assertEquals; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; import org.junit.Before; import org.junit.Test; /** * Tests the class {@link ExecuteOnceWhen}. */ public class ExecuteOnceWhenTest { // #begin FIELDS & INITIALIZATION /** * The string which passes the {@link #ACTION_CONDITION}. */ private static final String ACTION_STRING = "action!"; /** * A string which does not pass the {@link #ACTION_CONDITION}. */ private static final String NO_ACTION_STRING = "no action..."; /** * The condition which has to be passed for the action to be executed. */ private static final Predicate<String> ACTION_CONDITION = string -> Objects.equals(string, ACTION_STRING); /** * The observable on which the execution depends. */ private Property<String> observable; /** * The action which is undertaken. Increases {@link #executedActionCount}. */ private Consumer<String> action; /** * Counts how many actions were executed. */ private AtomicInteger executedActionCount; /** * Initializes the instances used to test. */ @Before public void setUp() { observable = new SimpleStringProperty(NO_ACTION_STRING); executedActionCount = new AtomicInteger(0); action = string -> executedActionCount.incrementAndGet(); } // #end FIELDS & INITIALIZATION // #begin SINGLE-THREADED TESTS /** * Tests whether an {@link IllegalStateException} is thrown when {@link ExecuteOnceWhen#executeWhen() executeWhen()} * is called for the second time. */ @Test(expected = IllegalStateException.class) public void testThrowExceptionIfCallActTwice() { ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); execute.executeWhen(); } /** * Tests whether no action is executed if the initial value does not pass the {@link #ACTION_CONDITION}. */ @Test public void testDoNotActIfInitialValueWrong() { ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); assertEquals(0, executedActionCount.get()); } /** * Tests whether the action is executed when the initial value passes the {@link #ACTION_CONDITION}. */ @Test public void testExecuteWhenWhenInitialValueCorrect() { observable.setValue(ACTION_STRING); ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); assertEquals(1, executedActionCount.get()); } /** * Tests whether the action is executed only once after the initial value already passed the * {@link #ACTION_CONDITION} . */ @Test public void testExecuteWhenOnlyOnceWhenInitialValueWasCorrect() { observable.setValue(ACTION_STRING); ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); // this executes the action for the first time execute.executeWhen(); // change the value and set the action string again; if this executes the action again, there is a bug observable.setValue(NO_ACTION_STRING); observable.setValue(ACTION_STRING); assertEquals(1, executedActionCount.get()); } /** * Tests whether the action is executed when the value is changed to one which passes the {@link #ACTION_CONDITION} * after waiting began. */ @Test public void testExecuteWhenWhenCorrectValueIsObserved() { ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); observable.setValue(ACTION_STRING); assertEquals(1, executedActionCount.get()); } /** * Tests whether the action is executed only once after some value already passed the {@link #ACTION_CONDITION}. */ @Test public void testExecuteWhenOnlyOnceWhenCorrectValueWasObserved() { ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); // this executes the action for the first time observable.setValue(ACTION_STRING); // change the value and set the action string again; if this executes the action again, there is a bug observable.setValue(NO_ACTION_STRING); observable.setValue(ACTION_STRING); assertEquals(1, executedActionCount.get()); } /** * Tests whether {@link ExecuteOnceWhen#cancel()} correctly prevents the execution of the action. */ @Test public void testCancel() { ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); // cancel and then set the value, which would lead to action execution execute.cancel(); observable.setValue(ACTION_STRING); assertEquals(0, executedActionCount.get()); } // #end SINGLE-THREADED TESTS // #begin MULTI-THREADED TESTS /** * Creates a number of threads which repeatedly change the {@link #observable}'s value and a number of threads which * execute {@link #action} once when the correct value is set. The value setting threads behave randomly but will * definitely set the correct value at least once. This means that the action must be executed exactly as often as * acting threads exist. * <p> * This is tested. * * @throws InterruptedException * if waiting for the {@link CountDownLatch} fails */ @Test public void testWithMultipleThreads() throws InterruptedException { int nrOfActThreads = 4; int nrOfValueThreads = 16; int nrOfLoopsPerThread = (int) 1e5; CountDownLatch latch = new CountDownLatch(nrOfValueThreads); createThreadsWhichActAndSetValues( latch, nrOfActThreads, nrOfValueThreads, nrOfLoopsPerThread) .forEach(thread -> thread.start()); latch.await(); assertEquals(nrOfActThreads, executedActionCount.get()); } /** * Creates threads where some repeatedly set a value on {@link #observable} (setting {@link #ACTION_STRING} approx. * half of the time) and some execute {@link #action} when the correct value is set. * * @param latch * the latch used to signal that the value threads are done * @param nrOfActThreads * number of threads which execute {@link #action}. * @param nrOfValueThreads * the number of created threads * @param nrOfLoopsPerValueThread * the number of times each thread sets a new value on {@link #observable} * @return a {@link List} of {@link Thread}s which did not yet start */ private List<Thread> createThreadsWhichActAndSetValues( CountDownLatch latch, int nrOfActThreads, int nrOfValueThreads, int nrOfLoopsPerValueThread) { Random random = new Random(); List<Thread> threads = createThreadsWhichSetCorrectValueOften(latch, nrOfValueThreads, nrOfLoopsPerValueThread); for (int i = 0; i < nrOfActThreads; i++) { int randomIndex = random.nextInt(threads.size()); Thread threadWhichActs = createThreadWhichActs(); threadWhichActs.setName("ACT #" + i); threads.add(randomIndex, threadWhichActs); } return threads; } /** * Creates a thread which execute {@link #action} when the correct value is set to {@link #observable}. * * @return a {@link Thread} which did not yet start */ private Thread createThreadWhichActs() { Runnable runnable = () -> { ExecuteOnceWhen<String> execute = new ExecuteOnceWhen<>(observable, ACTION_CONDITION, action); execute.executeWhen(); }; return new Thread(runnable); } /** * Creates threads which set the correct value in approximately half of the specified number of loops. * * @param latch * the latch used to signal that the thread is done * @param nrOfThreads * the number of created threads * @param nrOfLoopsPerThread * the number of times each thread sets a new value on {@link #observable} * @return a {@link List} of {@link Thread}s which did not yet start */ private List<Thread> createThreadsWhichSetCorrectValueOften( CountDownLatch latch, int nrOfThreads, int nrOfLoopsPerThread) { List<Thread> threads = new ArrayList<>(nrOfThreads * 2); for (int i = 0; i < nrOfThreads; i++) { Thread thread = createThreadWhichSetsCorrectValueOften(latch, nrOfLoopsPerThread); thread.setName("CORRECT VALUE #" + i); threads.add(thread); } return threads; } /** * Creates a thread which sets the correct value in approximately half of the specified number of loops. * * @param latch * the latch used to signal that the thread is done * @param nrOfLoops * the number of times the thread sets a new value on {@link #observable} * @return a {@link Thread} which did not yet start */ private Thread createThreadWhichSetsCorrectValueOften(CountDownLatch latch, int nrOfLoops) { Random random = new Random(); Runnable runnable = () -> { // make the first n-1 loops random for (int i = 0; i < nrOfLoops - 1; i++) { boolean setCorrectValue = random.nextBoolean(); if (setCorrectValue) observable.setValue(ACTION_STRING); else { String randomValue = "" + random.nextDouble(); observable.setValue(randomValue); } } // set the correct value at the end to give executing threads which start afterwards a chance to execute observable.setValue(ACTION_STRING); latch.countDown(); }; return new Thread(runnable, "CorrectValue"); } // #end MULTI-THREADED TESTS }