/* * Copyright (c) 2015 Futurice GmbH. All rights reserved. * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * <p> * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.reactivecascade.systemtest; import android.util.Log; import com.reactivecascade.util.AltFutureFuture; import com.reactivecascade.functional.ImmutableValue; import com.reactivecascade.functional.SettableAltFuture; import com.reactivecascade.i.CallOrigin; import com.reactivecascade.i.IThreadType; import com.reactivecascade.i.action.IAction; import com.reactivecascade.i.action.IActionOne; import com.reactivecascade.i.action.IActionR; import com.reactivecascade.i.functional.IAltFuture; import com.reactivecascade.reactive.ReactiveValue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import static com.reactivecascade.Async.SHOW_ERROR_STACK_TRACES; import static com.reactivecascade.Async.UI; import static com.reactivecascade.Async.d; import static com.reactivecascade.Async.v; import static com.reactivecascade.Async.vv; import static org.assertj.core.api.Assertions.assertThat; /** * System exercise tests for an Aspect. By default this is Aspect.UI but overriding classes can * repeat these tests for another Aspect type or implementation */ @CallOrigin public class ThreadTypeTest { private static final long TEST_TIMEOUT = 1000; //ms protected String tag; protected IThreadType threadType; public ThreadTypeTest() { // Replace these in any overriding class for a different Aspect tag = this.getClass().getSimpleName(); threadType = UI; } private void logMethodStart() { v(tag, "Start " + Thread.currentThread().getStackTrace()[0].getMethodName()); } private <IN, OUT> OUT awaitDone(IAltFuture<IN, OUT> altFuture) throws Exception { return new AltFutureFuture<>(altFuture).get(TEST_TIMEOUT, TimeUnit.MILLISECONDS); } /** * TODO This works for simple cases, but not when additional steps are spawned which generate the error * * @param action * @throws Throwable */ private <IN> void hideIntentionalErrorStackTraces(IAction<IN> action) { SHOW_ERROR_STACK_TRACES = false; try { action.call(); } catch (Exception e) { vv(tag, "hideIntentionalErrorStackTraces() from expected test exception: " + e); } finally { // Turn error stack traces back on only after any pending async tasks already in the queue (usually this is good enough for masking intentional exception tests on a single-threaded test setup) threadType.then(() -> { SHOW_ERROR_STACK_TRACES = true; }); } } // Android Studio thinks this and other test methods is not called because they are called reflectively @Test public void thenIActionDoesSomething() throws Throwable { logMethodStart(); IAltFuture altFuture = threadType.then(() -> { // Doing nothing Log.d("TEST", "Does nothing"); }); assertThat(altFuture.isDone()).isFalse(); assertThat(altFuture.isCancelled()).isFalse(); assertThat(altFuture.isForked()).isFalse(); altFuture.fork(); assertThat(altFuture.isForked()).isTrue(); awaitDone(altFuture); assertThat(altFuture.isDone()).isTrue(); assertThat(altFuture.isCancelled()).isFalse(); assertThat(altFuture.isForked()).isTrue(); } @Test public void thenIActionOnError() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); final AtomicReference<String> ar = new AtomicReference<>("empty"); IAltFuture secondAltFuture = threadType.then(() -> d(tag, "Second catch exec")); IAltFuture altFuture = threadType.then(() -> { throw new Exception("Blah"); }) .onError(e -> { ar.set("yeah"); assertThat("blah").isEqualToIgnoringCase(e.getMessage()); secondAltFuture.fork(); return false; }) .fork(); awaitDone(secondAltFuture); assertThat("yeah").isEqualToIgnoringCase(ar.get()); }); } @Test public void thenIActionOnErrorAndConsume() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); final AtomicReference<String> ar = new AtomicReference<>("not set"); IAltFuture secondAltFuture = threadType.then(() -> d(tag, "Second catch exec")); IAltFuture altFuture = threadType.then(() -> { throw new Exception("Ba"); }) .onError(e -> { ar.set("ya"); assertThat("ba").isEqualToIgnoringCase(e.getMessage()); secondAltFuture.fork(); return true; }) .fork(); awaitDone(altFuture); awaitDone(secondAltFuture); assertThat(ar.get()).isEqualToIgnoringCase("ya"); }); } @Test public void thenIActionOnErrorWithConsume_noPropagationAfterOnError() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); final AtomicReference<String> ar = new AtomicReference<>("not set"); IAltFuture<Object, Object> secondAltFuture = threadType.then(() -> d(tag, "Second catch exec")); IAltFuture<Object, Object> altFuture = threadType.then(() -> { throw new Exception("Ba2"); }); altFuture.onError(e -> { ar.set("B: 1st onCatch"); assertThat("B: ba2").isEqualToIgnoringCase(e.getMessage()); secondAltFuture.fork(); return true; }) .then(() -> ar.set("B: Foo")) .onError(e -> { ar.set("B: 2nd onCatch"); return false; }) .fork(); awaitDone(altFuture); awaitDone(secondAltFuture); assertThat(ar.get()).isEqualToIgnoringCase("B: 1st onCatch"); assertThat(altFuture.isDone()).isTrue(); assertThat(altFuture.isCancelled()).isTrue(); assertThat(altFuture.isForked()).isTrue(); }); } // @Test // public void secondOnErrorIsCalledIfNoValueSent() throws Throwable { // logMethodStart(); // final AtomicReference<String> ar = new AtomicReference<>("not set"); // IAltFuture<Object, Object> secondCatch = threadType.subscribeTarget(() -> d(tag, "Second catch exec")); // IAltFuture<Long, Long> altFuture = new SettableAltFuture<>(threadType, Long.MAX_VALUE); // IAltFuture<String, String> altFuture2 = altFuture // .subscribeTarget(new IAction() { // Not folding to a lambda, want to be clear which case is being tested // @Override // public void call() throws Exception { // ar.set("first call"); // throw new Exception("Ba2"); // } // }) // .onError( // (e, l)-> { // ar.set("1st onCatch"); // assertThat("ba2").isEqualToIgnoringCase(e.getMessage()); // assertThat(Long.MAX_VALUE).isEqualTo(l); // }) // .subscribeTarget(() -> ar.set("Foo")) // .onError( // e -> { // ar.set("2nd onCatch"); // secondCatch.fork(); // }) // .fork(); // // awaitDone(altFuture2); // awaitDone(secondCatch); // assertThat(ar.get()).isEqualToIgnoringCase("2nd onCatch"); // assertThat(altFuture.isDone()).isTrue(); // assertThat(altFuture.isCancelled()).isTrue(); // assertThat(altFuture.isForked()).isTrue(); // } @Test public void valueNotPassedToSecondOnError() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); final AtomicReference<String> ar = new AtomicReference<>("not set"); IAltFuture<Object, Object> secondCatch = threadType.then(() -> d(tag, "Second catch exec")); IAltFuture<Object, Object> thirdCatch = threadType.then(() -> d(tag, "Third catch exec")); IAltFuture<?, Long> altFuture = new SettableAltFuture<>(threadType, Long.MAX_VALUE); IAltFuture<String, String> altFuture2 = altFuture .then(() -> { ar.set("C: first call"); throw new Exception("C: Ba2"); }) .onError(e -> { ar.set("C: 1st onCatch"); assertThat("C: ba2").isEqualToIgnoringCase(e.getMessage()); return true; }) .then(s -> { ar.set(s + "C: Foo"); return "It was set to " + ar.get(); }) .onError(e -> { //This will never be called because the up-chain onError(e,l) has absorbed the value and cancelled this, so there is not value to pass ar.set(ar + " -> C: 2nd onCatch"); secondCatch.fork(); return false; }) .then(s -> { ar.set(s + "C: Bar"); }) .onError(e -> { thirdCatch.fork(); return false; }) .fork(); awaitDone(altFuture); awaitDone(altFuture2); awaitDone(thirdCatch); assertThat(!secondCatch.isDone()); assertThat(ar.get()).isEqualToIgnoringCase("C: 1st onCatch"); assertThat(altFuture.isDone()).isTrue(); assertThat(altFuture.isCancelled()).isTrue(); assertThat(altFuture.isForked()).isTrue(); assertThat(altFuture.isConsumed()).isFalse(); }); } @Test public void valuePassedToThirdOnError() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); final AtomicReference<String> ar = new AtomicReference<>("not set"); IAltFuture<Object, Object> secondCatch = threadType.then(() -> d(tag, "Second catch exec")); IAltFuture<Object, Object> thirdCatch = threadType.then(() -> d(tag, "Third catch exec")); IAltFuture<Long, Long> altFuture = new SettableAltFuture<>(threadType, Long.MAX_VALUE) .then(() -> { ar.set("D: first call"); throw new Exception("Ba2"); }); altFuture .onError(e -> { ar.set("D: 1st onCatch"); assertThat("D: ba2").isEqualToIgnoringCase(e.getMessage()); return true; }) .then(() -> ar.set("D: Foo")) .onError(e -> { //This will never be called because the up-chain onError(e,l) has absorbed the value and cancelled this, so there is not value to pass ar.set("D: 2nd onCatch"); secondCatch.fork(); return false; }) .then(() -> ar.set("D: Bar")) .onError(e -> { ar.set("D: 3rd onCatch"); thirdCatch.fork(); return false; }) .fork(); awaitDone(altFuture); awaitDone(thirdCatch); assertThat(ar.get()).isEqualToIgnoringCase("D: 3rd onCatch"); assertThat(altFuture.isDone()).isTrue(); assertThat(altFuture.isCancelled()).isTrue(); assertThat(altFuture.isForked()).isTrue(); }); } @Test public void thenIActionOnCatch_propagationAfterOnError() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); final AtomicReference<String> ar = new AtomicReference<>("not set"); IAltFuture<Object, Object> e1Catch = threadType.then(() -> d(tag, "Error one exec")); IAltFuture<Object, Object> fooCatch = threadType.then(() -> d(tag, "Foo exec")); IAltFuture<Object, Object> secondCatch = threadType.then(() -> d(tag, "Second catch exec")); IAltFuture<Object, Object> altFuture = threadType.then(new IAction<Object>() { // Not folding to a lambda, want to be clear which case is being tested @Override public void call() throws Exception { throw new Exception("Ba2"); } }); altFuture.onError(e -> { ar.set("A: 1st onCatch"); e1Catch.fork(); assertThat("ba2").isEqualToIgnoringCase(e.getMessage()); return false; }) .then(() -> { fooCatch.fork(); ar.set("A: Foo"); }) .onError(e -> { ar.set("A: 2nd onCatch"); secondCatch.fork(); return false; }) .fork(); awaitDone(altFuture); awaitDone(e1Catch); awaitDone(fooCatch); awaitDone(secondCatch); assertThat("A: 2nd onCatch").isEqualToIgnoringCase(ar.get()); assertThat(altFuture.isDone()).isTrue(); assertThat(altFuture.isCancelled()).isTrue(); assertThat(altFuture.isForked()).isTrue(); }); } @Test public void thenIActionRDoesSomething() throws Throwable { logMethodStart(); int expected = 56; IAltFuture<?, Integer> test = threadType.then(new IActionR<Object, Integer>() { // Not folding to a lambda, want to be clear which case is being tested @Override public Integer call() throws Exception { return expected; } }) .fork(); assertThat(expected).isEqualTo(awaitDone(test)); } @Test public void thenIActionR_returnsValue() throws Throwable { logMethodStart(); Integer expected = 66; IAltFuture<?, Integer> test = threadType.then(() -> { v(tag, "Do 66"); return expected; }).fork(); v(tag, "Wait for 66"); awaitDone(test); v(tag, "Notified 66"); assertThat(expected).isEqualTo(test.get()); } @Test public void thenIActionR_doesNothingThenReturnsValue() throws Throwable { logMethodStart(); Integer expected = 66; IAltFuture<?, Integer> test = threadType.then(() -> { v(tag, "Do 66"); return expected; }) .then(() -> v(tag, "After 66")) .then(() -> v(tag, "1")) .then(() -> v(tag, "2")) .then(() -> v(tag, "3")) .then(() -> v(tag, "After notify 66")) .fork(); v(tag, "Wait for 66"); awaitDone(test); v(tag, "Notified 66"); assertThat(expected).isEqualTo(test.get()); } @Test public void thenIActionOneRDoesSomething() throws Throwable { logMethodStart(); String expected = "aabb"; IAltFuture<String, String> continueAltFuture = threadType.then(() -> v(tag, "Continuing")); IAltFuture<?, String> test = threadType.from("aa") .then((String s) -> { v(tag, "Merge strings"); return s + "bb"; }) .then(continueAltFuture) .fork(); awaitDone(continueAltFuture); assertThat("aabb").isEqualTo(test.get()); } @Test public void thenIActionOneRChainCombination() throws Throwable { logMethodStart(); String expected = "abcd"; IAltFuture<String, String> test = threadType.from("a") .then(s -> s + "b") .then(s -> s + "c") .then(s -> s + "d") .fork(); assertThat(awaitDone(test)).isEqualTo(expected); } @Test public void atomicValueBasicSetGet() throws Throwable { logMethodStart(); String expected = "abcd"; ReactiveValue<String> reactiveValue = new ReactiveValue<>("empty", expected); assertThat(reactiveValue.get()).isEqualTo(expected); } @Test public void settableAltFutureFork_Set() throws Throwable { logMethodStart(); String expected = "yes SettableAltFuture was set"; SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType); altFuture.fork(); // The fork execution will be delayed until and triggered by the .set() below altFuture.set(expected); assertThat(altFuture.get()).isEqualTo(expected); } @Test public void settableAltFutureSet_Fork_ThenAltFuture() throws Throwable { logMethodStart(); String initialValue = "yes SettableAltFuture was set"; String expected = "yes SettableAltFuture was set and augmented"; SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType); IAltFuture<String, String> downchainAltFuture = altFuture.then((s -> s + " and augmented")); altFuture.set(initialValue); altFuture.fork(); assertThat(awaitDone(downchainAltFuture)).isEqualTo(expected); } @Test public void settableAltFutureSet_Fork_ThenAltFuture_AltGenerics() throws Throwable { logMethodStart(); String initialValue = "yes SettableAltFuture was set"; String expected = "yes SettableAltFuture was set and added to"; SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType); IAltFuture<String, String> downchainAltFuture = altFuture.then(s -> s + " and added to"); altFuture.fork(); altFuture.set(initialValue); assertThat(awaitDone(altFuture)).isEqualTo(initialValue); assertThat(awaitDone(downchainAltFuture)).isEqualTo(expected); } @Test public void settableAltFutureFork_Set_ThenAltFuture() throws Throwable { logMethodStart(); String initialValue = "yes SettableAltFuture was set"; String expected = "yes SettableAltFuture was set and augmented"; SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType); IAltFuture<String, String> downchainAltFuture = altFuture.then((s -> s + " and augmented")); altFuture.set(initialValue); altFuture.fork(); assertThat(awaitDone(downchainAltFuture)).isEqualTo(expected); } @Test public void execute_and_simple_wait() throws Throwable { logMethodStart(); AtomicBoolean done = new AtomicBoolean(false); threadType.execute((IAction) () -> done.set(true)); long timeout = System.currentTimeMillis() + 10000; while (!done.get() && System.currentTimeMillis() < timeout) { Thread.sleep(100); } assertThat(done.get()).isTrue(); } @Test public void executeNext_and_simple_wait() throws Throwable { logMethodStart(); AtomicBoolean done = new AtomicBoolean(false); threadType.execute((IAction) () -> done.set(true)); long timeout = System.currentTimeMillis() + 10000; while (!done.get() && System.currentTimeMillis() < timeout) { Thread.sleep(100); } assertThat(done.get()).isTrue(); } @Test public <IN> void settableAltFutureFork_Set_ThenAltFuture_AltGenerics() throws Throwable { logMethodStart(); String initialValue = "yes SettableAltFuture was set"; String expected = "yes SettableAltFuture was set and added to"; SettableAltFuture<IN, String> altFuture = new SettableAltFuture<>(threadType); IAltFuture<String, String> downchainAltFuture = altFuture.then(s -> s + " and added to"); altFuture.fork(); altFuture.set(initialValue); assertThat(awaitDone(downchainAltFuture)).isEqualTo(expected); } @Test public void immutableValue_NotYetSet() throws Throwable { logMethodStart(); String expected = "yes ImmutableValue was set"; ImmutableValue<String> immutableValue = new ImmutableValue<>(); assertThat(immutableValue.isSet()).isFalse(); } @Test public void immutableValue_Set() throws Throwable { logMethodStart(); String expected = "yes ImmutableValue was set"; ImmutableValue<String> immutableValue = new ImmutableValue<>(); immutableValue.set(expected); assertThat(immutableValue.get()).isEqualTo(expected); } @Test public void immutableValue_SetThenActionPreviouslyAdded() throws Throwable { logMethodStart(); String expected = "yes ImmutableValue was set"; ImmutableValue<String> immutableValue = new ImmutableValue<>(); ImmutableValue<Integer> otherImmutableValue = new ImmutableValue<>(); ImmutableValue<Integer> yetAnotherImmutableValue = new ImmutableValue<>(); immutableValue.then(s -> otherImmutableValue.set(s.length())); immutableValue.then(s -> yetAnotherImmutableValue.set(s.length() + 1)); immutableValue.set(expected); assertThat(otherImmutableValue.get()).isEqualTo(26); assertThat(yetAnotherImmutableValue.get()).isEqualTo(27); } @Test public void immutableValue_SetThenActionAddedLater() throws Throwable { logMethodStart(); String expected = "ImmutableValue was set"; ImmutableValue<String> immutableValue = new ImmutableValue<>(); ImmutableValue<Integer> otherImmutableValue = new ImmutableValue<>(); ImmutableValue<Integer> yetAnotherImmutableValue = new ImmutableValue<>(); immutableValue.set(expected); immutableValue.then(s -> otherImmutableValue.set(s.length())); immutableValue.then(s -> yetAnotherImmutableValue.set(s.length() + 1)); assertThat(otherImmutableValue.get()).isEqualTo(22); assertThat(yetAnotherImmutableValue.get()).isEqualTo(23); } @Test public void immutableValue_BlowsExceptionOnEarlyGet() throws Throwable { hideIntentionalErrorStackTraces(() -> { logMethodStart(); ImmutableValue<String> immutableValue = new ImmutableValue<>(); try { immutableValue.get(); throw new AssertionError("We expected an Exception before this point"); } catch (IllegalStateException e) { // Expected } }); } @Test public void immutableValue_EarlySafeGet() throws Throwable { logMethodStart(); String expected = "yes ImmutableValue was set"; ImmutableValue<String> immutableValue = new ImmutableValue<>(); assertThat(immutableValue.safeGet()).isEqualTo(null); } //@Test TODO messed up test public void reactiveBind_Then_functionalFork_Then_Set() throws Throwable { logMethodStart(); String expected = "yes binding set"; ReactiveValue<String> reactiveValue = new ReactiveValue<>("first binding", "not set"); ReactiveValue<String> secondReactiveValue = new ReactiveValue<>("second binding", "not set either"); SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType, "AV set"); ImmutableValue<String> immutableValue = new ImmutableValue<String>().then(altFuture::fork); reactiveValue.subscribe(() -> { secondReactiveValue.set(expected); if (!immutableValue.isSet()) { immutableValue.set(expected); } }); reactiveValue.set(expected); awaitDone(altFuture); assertThat(secondReactiveValue.get()).isEqualTo(expected); } //@Test TODO messed up test public void functionalFork_Then_Set_Then_reactiveBind() throws Throwable { logMethodStart(); String expected = "yes binding set"; ReactiveValue<String> reactiveValue = new ReactiveValue<>("first binding", "not set"); ReactiveValue<String> secondReactiveValue = new ReactiveValue<>("second binding", "not set either"); SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType); altFuture.fork(); // The fork execution will be delayed until and triggered by the .set() below reactiveValue.set(expected); awaitDone(altFuture); reactiveValue.subscribe(threadType, () -> secondReactiveValue.set(expected)); reactiveValue.subscribe(threadType, (IActionOne<String>) altFuture::set); assertThat(secondReactiveValue.get()).isEqualTo(expected); assertThat(altFuture.get()).isEqualTo(expected); } //@Test TODO messed up test public void reactiveBind_Then_functionalSet_Then_Fork() throws Throwable { logMethodStart(); String expected = "yes binding set"; ReactiveValue<String> reactiveValue = new ReactiveValue<>("first binding", "not set"); ReactiveValue<String> secondReactiveValue = new ReactiveValue<>("second binding", "not set either"); SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType); reactiveValue.subscribe(threadType, () -> secondReactiveValue.set(expected)); reactiveValue.subscribe(threadType, (IActionOne<String>) altFuture::set); altFuture.set(expected); altFuture.fork(); awaitDone(altFuture); assertThat(secondReactiveValue.get()).isEqualTo(expected); assertThat(altFuture.get()).isEqualTo(expected); } //@Test TODO messed up test public void functionalSet_Then_Fork_Then_reactiveBind() throws Throwable { logMethodStart(); String expected = "yes binding set"; ReactiveValue<String> reactiveValue = new ReactiveValue<>("first binding", "not set"); ReactiveValue<String> secondReactiveValue = new ReactiveValue<>("second binding", "not set either"); SettableAltFuture<?, String> altFuture = new SettableAltFuture<>(threadType, expected); altFuture.fork(); awaitDone(altFuture); reactiveValue.subscribe(threadType, () -> secondReactiveValue.set(expected)); reactiveValue.subscribe(threadType, (String s) -> altFuture.set(s)); assertThat(secondReactiveValue.get()).isEqualTo(expected); assertThat(altFuture.get()).isEqualTo(expected); } }