From 443ec14ee778e9b26a31b5864d1cb324de7a8244 Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Tue, 8 Mar 2016 16:48:11 +0100 Subject: [PATCH 01/11] Unit tests, first round. WIP --- .../java/se/code77/jq/ExampleUnitTest.java | 15 - jq/src/test/java/se/code77/jq/JQTests.java | 154 +++++++++ .../test/java/se/code77/jq/PromiseTests.java | 316 ++++++++++++++++++ .../test/java/se/code77/jq/util/Assert.java | 52 +++ .../java/se/code77/jq/util/AsyncTests.java | 7 + .../java/se/code77/jq/util/TestConfig.java | 139 ++++++++ 6 files changed, 668 insertions(+), 15 deletions(-) delete mode 100644 jq/src/test/java/se/code77/jq/ExampleUnitTest.java create mode 100644 jq/src/test/java/se/code77/jq/JQTests.java create mode 100644 jq/src/test/java/se/code77/jq/PromiseTests.java create mode 100644 jq/src/test/java/se/code77/jq/util/Assert.java create mode 100644 jq/src/test/java/se/code77/jq/util/AsyncTests.java create mode 100644 jq/src/test/java/se/code77/jq/util/TestConfig.java diff --git a/jq/src/test/java/se/code77/jq/ExampleUnitTest.java b/jq/src/test/java/se/code77/jq/ExampleUnitTest.java deleted file mode 100644 index d0d2034..0000000 --- a/jq/src/test/java/se/code77/jq/ExampleUnitTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package se.code77.jq; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * To work on unit tests, switch the Test Artifact in the Build Variants view. - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/jq/src/test/java/se/code77/jq/JQTests.java b/jq/src/test/java/se/code77/jq/JQTests.java new file mode 100644 index 0000000..7e97464 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/JQTests.java @@ -0,0 +1,154 @@ +package se.code77.jq; + +import org.junit.Test; + +import static se.code77.jq.util.Assert.*; + +import se.code77.jq.JQ.Deferred; +import se.code77.jq.Promise.OnFulfilledCallback; +import se.code77.jq.Promise.OnRejectedCallback; +import se.code77.jq.util.AsyncTests; + +public class JQTests extends AsyncTests { + @Test + public void resolve_isResolved() { + String value = "Hello"; + Promise p = JQ.resolve(value); + assertResolved(p, value); + } + + @Test + public void resolveVoid_isResolved() { + Promise p = JQ.resolve(); + assertResolved(p, null); + } + + @Test + public void reject_isRejected() { + IllegalArgumentException reason = new IllegalArgumentException("foobar"); + Promise p = JQ.reject(reason); + assertRejected(p, reason); + } + + @Test + public void all_isResolvedList() { + + } + + @Test + public void all_isResolvedVarArg() { + + } + + @Test + public void all_isRejected() { + + } + + @Test + public void all_isPending() { + // at least one promise is not done -> resulting is pending forever + } + + @Test + public void any_isResolvedByFirst() { + + } + + @Test + public void any_isResolvedByLast() { + + } + + @Test + public void any_isRejected() { + + } + + @Test + public void any_isPending() { + + } + + @Test + public void race_isResolvedByFirst() { + + } + + @Test + public void race_isResolvedByLast() { + + } + + @Test + public void race_isRejectedByFirst() { + + } + + @Test + public void race_isRejectedByLast() { + + } + + @Test + public void race_isPending() { + + } + + @Test + public void allSettled_isResolvedAllResolved() { + + } + + @Test + public void allSettled_isResolvedAllResolvedOrRejected() { + + } + + @Test + public void allSettled_isResolvedAllRejected() { + + } + + @Test + public void allSettled_isPending() { + + } + + @Test + public void wrap_isReturnedForPromise() { + + } + + @Test + public void wrap_isResolvedForValue() { + + } + + @Test + public void wrap_isResolvedForFutureTaskCompleted() { + + } + + @Test + public void wrap_isRejectedForFutureTaskFailedWithException() { + + } + + @Test + public void wrap_isRejectedForFutureTaskFailedWithError() { + // Hmm consider if this should be rejected with dummy Exception, use ExecutionException(Throwable) or actually throw the Error + } + + @Test + public void wrap_isRejectedForFutureTaskInterrupted() { + + } + + @Test + public void wrap_isRejectedForFutureTaskCancelled() { + + } + + // when, fail, done, timeout, delay are just convenience wrappers. That's not evident from a black box pov, but are they worth writing tests for? +} diff --git a/jq/src/test/java/se/code77/jq/PromiseTests.java b/jq/src/test/java/se/code77/jq/PromiseTests.java new file mode 100644 index 0000000..12d59a8 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/PromiseTests.java @@ -0,0 +1,316 @@ +package se.code77.jq; + +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static se.code77.jq.util.Assert.*; + +import se.code77.jq.JQ.Deferred; +import se.code77.jq.Promise.OnFulfilledCallback; +import se.code77.jq.Promise.OnRejectedCallback; +import se.code77.jq.util.AsyncTests; + +public class PromiseTests extends AsyncTests { + + @Test + public void pending_isPending() throws Exception { + Deferred deferred = JQ.defer(); + Promise p = deferred.promise; + + assertPending(p); + final Semaphore sem = new Semaphore(0); + + p.then(new OnFulfilledCallback() { + @Override + public Future onFulfilled(String value) throws Exception { + sem.release(); + return null; + } + }); + + assertFalse(sem.tryAcquire(2, TimeUnit.SECONDS)); + deferred.resolve("Hello"); + } + + @Test + public void resolved_isResolved() throws Exception { + Deferred deferred = JQ.defer(); + Promise p = deferred.promise; + + final Semaphore sem = new Semaphore(0); + + p.then(new OnFulfilledCallback() { + @Override + public Future onFulfilled(String value) throws Exception { + sem.release(); + return null; + } + }); + + String value = "Hello"; + deferred.resolve(value); + + assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertResolved(p, value); + } + + + @Test + public void rejected_isRejected() throws Exception { + Deferred deferred = JQ.defer(); + Promise p = deferred.promise; + + final Semaphore sem = new Semaphore(0); + + p.fail(new OnRejectedCallback() { + @Override + public Future onRejected(Exception reason) throws Exception { + sem.release(); + return null; + } + }); + + IllegalArgumentException reason = new IllegalArgumentException("Foobar"); + deferred.reject(reason); + + assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertRejected(p, reason); + } + + @Test + public void preResolved_isResolved() throws Exception { + String value = "Hello"; + Promise p = JQ.resolve(value); + + final Semaphore sem = new Semaphore(0); + + p.then(new OnFulfilledCallback() { + @Override + public Future onFulfilled(String value) throws Exception { + sem.release(); + return null; + } + }); + + assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertResolved(p, value); + } + + @Test + public void preRejected_isRejected() throws Exception { + IllegalArgumentException reason = new IllegalArgumentException("Foobar"); + Promise p = JQ.reject(reason); + + final Semaphore sem = new Semaphore(0); + + p.fail(new OnRejectedCallback() { + @Override + public Future onRejected(Exception reason) throws Exception { + sem.release(); + return null; + } + }); + + assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertRejected(p, reason); + } + + @Test + public void resolved_isNotRejected() { + + } + + @Test + public void rejected_isNotResolved() { + + } + + @Test + public void resolved_isImmutable() { + final Deferred deferred = JQ.defer(); + Promise p = deferred.promise; + + deferred.resolve("resolve1"); + + assertThrows(new Callable() { + @Override + public Void call() throws Exception { + deferred.resolve("resolve2"); + return null; + } + }, IllegalStateException.class); + + assertThrows(new Callable() { + @Override + public Void call() throws Exception { + deferred.reject(new Exception("reject1")); + return null; + } + }, IllegalStateException.class); + } + + @Test + public void rejected_isImmutable() { + final Deferred deferred = JQ.defer(); + Promise p = deferred.promise; + + deferred.reject(new Exception("reject1")); + + assertThrows(new Callable() { + @Override + public Void call() throws Exception { + deferred.reject(new Exception("reject2")); + return null; + } + }, IllegalStateException.class); + + assertThrows(new Callable() { + @Override + public Void call() throws Exception { + deferred.resolve("resolve1"); + return null; + } + }, IllegalStateException.class); + } + + @Test + public void cancel_isNotSupported() { + final Deferred deferred = JQ.defer(); + Promise p = deferred.promise; + + assertFalse(p.cancel(true)); + assertFalse(p.isCancelled()); + } + + @Test + public void resolved_isResolvedSync() { + // Promise.get + } + + @Test + public void rejected_isRejectedSync() { + // Promise.get + } + + @Test + public void pending_hasTimeoutSync() { + // Promise.get(timeout, TimeUnit) + } + + @Test + public void chained_isResolvedWithValue() { + // OnFulfilled returns Value, chained fulfillment handlers are invoked + } + + @Test + public void chained_isResolvedWithPromise() { + // OnFulfilled returns new Promise, chained fulfillment handlers are invoked + } + + @Test + public void chained_isResolvedWithFutureTask() { + // OnFulfilled returns java.util.concurrent.FutureTask, chained fulfillment handlers are invoked + } + + @Test + public void chained_isRejectedWithException() { + // OnFulfilled throws exception, chained fulfillment handlers are not invoked, trailing rejection handler is + + } + + @Test + public void chained_isResolvedWithoutHandler() { + // resolved promise without fulfillmenthandler -> next promise is resolved with value + } + + @Test + public void chained_isRejectedWithoutHandler() { + // rejected promise without rejection handler -> next promise is rejected with reason + // (this is standard case and implicitly tested elsewhere, but for clarity has its own test) + } + + @Test + public void terminated_isRejectedWithoutHandler() { + // This should throw UnhandledRejectionException + } + + @Test + public void timeout_isResolved() { + // new promise is resolved + } + + @Test + public void timeout_isRejected() { + // new promise is rejected + } + + @Test + public void timeout_isResolvedAfterTimeout() { + // Promise is resolved but too late, TimeoutException is thrown + } + + @Test + public void timeout_isRejectedAfterTimeout() { + // Promise is rejected but too late, TimeoutException is thrown + } + + @Test + public void delay_isResolved() { + // new promise is resolved after delay + } + + @Test + public void delay_isRejected() { + // new promise is rejected immediately + } + + @Test + public void delay_isPending() { + // New promise is also pending after delay has passed + } + + @Test + public void join_isResolvedThisThatEqual() { + // v1 equals v2 -> new promise should be resolved with v1 + } + + @Test + public void join_isResolvedThisThatNotEqual() { + // v1 not equals v2 -> new promise should be rejected + } + + @Test + public void join_isResolvedThisNull() { + // v1 == null, v2 != null -> new promise should be rejected + } + + @Test + public void join_isResolvedThatNull() { + // v1 != null, v2 == null -> new promise should be rejected + } + + @Test + public void join_isResolvedThisThatNull() { + // v1 == null, v2 == null -> new promise should be resolved with null + } + + @Test + public void join_isThisRejected() { + // this rejected, that resolved -> new promise should be rejected + } + + @Test + public void join_isThatRejected() { + // this resolved, that rejected -> new promise should be rejected + } + + @Test + public void join_isPending() { + // this or that is pending -> new promise should be pending + } + +} diff --git a/jq/src/test/java/se/code77/jq/util/Assert.java b/jq/src/test/java/se/code77/jq/util/Assert.java new file mode 100644 index 0000000..90662b5 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/Assert.java @@ -0,0 +1,52 @@ +package se.code77.jq.util; + +import java.util.concurrent.Callable; + +import se.code77.jq.Promise; + +public class Assert extends org.junit.Assert { + public static void assertPending(Promise p) { + assertTrue(p.isPending()); + assertFalse(p.isFulfilled()); + assertFalse(p.isRejected()); + assertFalse(p.isCancelled()); + assertFalse(p.isDone()); + Promise.StateSnapshot snapshot = p.inspect(); + assertSame(snapshot.state, Promise.State.PENDING); + assertNull(snapshot.value); + assertNull(snapshot.reason); + } + + public static void assertResolved(Promise p, T expected) { + assertFalse(p.isPending()); + assertTrue(p.isFulfilled()); + assertFalse(p.isRejected()); + assertFalse(p.isCancelled()); + assertTrue(p.isDone()); + Promise.StateSnapshot snapshot = p.inspect(); + assertSame(snapshot.state, Promise.State.FULFILLED); + assertEquals(snapshot.value, expected); + assertNull(snapshot.reason); + } + + public static void assertRejected(Promise p, Exception reason) { + assertFalse(p.isPending()); + assertFalse(p.isFulfilled()); + assertTrue(p.isRejected()); + assertFalse(p.isCancelled()); + assertTrue(p.isDone()); + Promise.StateSnapshot snapshot = p.inspect(); + assertSame(snapshot.state, Promise.State.REJECTED); + assertNull(snapshot.value); + assertSame(snapshot.reason, reason); + } + + public static void assertThrows(Callable task, Class expectedExceptionClass) { + try { + task.call(); + assertTrue(false); + } catch (Exception e) { + assertSame(expectedExceptionClass, e.getClass()); + } + } +} diff --git a/jq/src/test/java/se/code77/jq/util/AsyncTests.java b/jq/src/test/java/se/code77/jq/util/AsyncTests.java new file mode 100644 index 0000000..735e232 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/AsyncTests.java @@ -0,0 +1,7 @@ +package se.code77.jq.util; + +public class AsyncTests { + public AsyncTests() { + TestConfig.init(); + } +} diff --git a/jq/src/test/java/se/code77/jq/util/TestConfig.java b/jq/src/test/java/se/code77/jq/util/TestConfig.java new file mode 100644 index 0000000..aa92764 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/TestConfig.java @@ -0,0 +1,139 @@ +package se.code77.jq.util; + +import java.util.PriorityQueue; + +import se.code77.jq.config.Config; + +public class TestConfig { + public static void init() { + final TestThread tt = new TestThread(); + tt.start(); + + Config.setConfig(new Config(true) { + @Override + public Dispatcher createDispatcher() { + return new TestDispatcher(tt); + } + + @Override + public Logger getLogger() { + return new Logger() { + private void log(String level, String s) { + System.out.println(System.currentTimeMillis() + " [PROMISE." + level + "] " + + s); + } + + @Override + public void debug(String s) { + log("debug", s); + } + + @Override + public void info(String s) { + log("info", s); + } + + @Override + public void warn(String s) { + log("warn", s); + } + + @Override + public void error(String s) { + log("error", s); + } + + }; + } + }); + + } + + private static class TestDispatcher implements Config.Dispatcher { + + private TestThread mThread; + + public TestDispatcher(TestThread t) { + mThread = t; + } + + public TestDispatcher() { + mThread = (TestThread) Thread.currentThread(); + } + + @Override + public void dispatch(Runnable r) { + dispatch(r, 0); + } + + @Override + public void dispatch(Runnable r, long ms) { + mThread.addEvent(r, ms); + } + + } + + private static class TestThread extends Thread { + private static class Event implements Comparable { + public final Runnable r; + public final long due; + + public Event(Runnable r, long due) { + this.r = r; + this.due = due; + } + + @Override + public int compareTo(Event o) { + return (int) (due - o.due); + } + } + + private final PriorityQueue mEvents = new PriorityQueue(); + private boolean mStopped; + + private synchronized Event getEvent() throws InterruptedException { + while (!Thread.interrupted()) { + Event e = mEvents.peek(); + + if (e == null) { + wait(); + } else { + long timeLeft = e.due - System.currentTimeMillis(); + + if (timeLeft > 0) { + wait(timeLeft); + } else { + return mEvents.poll(); + } + } + } + + throw new InterruptedException(); + } + + @Override + public void run() { + System.out.println("Starting event thread"); + while (!mStopped) { + try { + Event e = getEvent(); + + e.r.run(); + } catch (InterruptedException ex) { + } + } + } + + public void exit() { + System.out.println("exit"); + mStopped = true; + interrupt(); + } + + public synchronized void addEvent(Runnable r, long ms) { + mEvents.add(new Event(r, System.currentTimeMillis() + ms)); + notify(); + } + } +} From f1586f8fa0a9f280795ad1416241b29e8267bb1a Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Tue, 8 Mar 2016 17:42:47 +0100 Subject: [PATCH 02/11] More tests --- .../test/java/se/code77/jq/PromiseTests.java | 116 ++++++++++++++---- .../test/java/se/code77/jq/util/Assert.java | 4 +- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/jq/src/test/java/se/code77/jq/PromiseTests.java b/jq/src/test/java/se/code77/jq/PromiseTests.java index 12d59a8..0c7dc30 100644 --- a/jq/src/test/java/se/code77/jq/PromiseTests.java +++ b/jq/src/test/java/se/code77/jq/PromiseTests.java @@ -3,9 +3,13 @@ import org.junit.Test; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import static se.code77.jq.util.Assert.*; @@ -15,6 +19,24 @@ import se.code77.jq.util.AsyncTests; public class PromiseTests extends AsyncTests { + private static final String TEST_VALUE1 = "Hello"; + private static final String TEST_VALUE2 = "World"; + private static final Exception TEST_REASON1 = new IllegalArgumentException("foo"); + private static final Exception TEST_REASON2 = new IllegalArgumentException("bar"); + + private static class SlowTask implements Callable { + public final T value; + + public SlowTask(T value) { + this.value = value; + } + + @Override + public T call() throws Exception { + Thread.sleep(1000); + return value; + } + } @Test public void pending_isPending() throws Exception { @@ -33,7 +55,7 @@ public Future onFulfilled(String value) throws Exception { }); assertFalse(sem.tryAcquire(2, TimeUnit.SECONDS)); - deferred.resolve("Hello"); + deferred.resolve(TEST_VALUE1); } @Test @@ -51,11 +73,10 @@ public Future onFulfilled(String value) throws Exception { } }); - String value = "Hello"; - deferred.resolve(value); + deferred.resolve(TEST_VALUE1); assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); - assertResolved(p, value); + assertResolved(p, TEST_VALUE1); } @@ -83,8 +104,7 @@ public Future onRejected(Exception reason) throws Exception { @Test public void preResolved_isResolved() throws Exception { - String value = "Hello"; - Promise p = JQ.resolve(value); + Promise p = JQ.resolve(TEST_VALUE1); final Semaphore sem = new Semaphore(0); @@ -97,7 +117,7 @@ public Future onFulfilled(String value) throws Exception { }); assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); - assertResolved(p, value); + assertResolved(p, TEST_VALUE1); } @Test @@ -134,12 +154,12 @@ public void resolved_isImmutable() { final Deferred deferred = JQ.defer(); Promise p = deferred.promise; - deferred.resolve("resolve1"); + deferred.resolve(TEST_VALUE1); assertThrows(new Callable() { @Override public Void call() throws Exception { - deferred.resolve("resolve2"); + deferred.resolve(TEST_VALUE2); return null; } }, IllegalStateException.class); @@ -147,7 +167,7 @@ public Void call() throws Exception { assertThrows(new Callable() { @Override public Void call() throws Exception { - deferred.reject(new Exception("reject1")); + deferred.reject(TEST_REASON1); return null; } }, IllegalStateException.class); @@ -158,12 +178,12 @@ public void rejected_isImmutable() { final Deferred deferred = JQ.defer(); Promise p = deferred.promise; - deferred.reject(new Exception("reject1")); + deferred.reject(TEST_REASON1); assertThrows(new Callable() { @Override public Void call() throws Exception { - deferred.reject(new Exception("reject2")); + deferred.reject(TEST_REASON2); return null; } }, IllegalStateException.class); @@ -171,7 +191,7 @@ public Void call() throws Exception { assertThrows(new Callable() { @Override public Void call() throws Exception { - deferred.resolve("resolve1"); + deferred.resolve(TEST_VALUE1); return null; } }, IllegalStateException.class); @@ -187,33 +207,85 @@ public void cancel_isNotSupported() { } @Test - public void resolved_isResolvedSync() { - // Promise.get + public void resolved_isResolvedSync() throws Exception { + Promise p = JQ.resolve(TEST_VALUE1); + + assertEquals(TEST_VALUE1, p.get()); } @Test - public void rejected_isRejectedSync() { - // Promise.get + public void rejected_isRejectedSync() throws Exception { + final Promise p = JQ.reject(TEST_REASON1); + + Exception e = assertThrows(new Callable() { + @Override + public Void call() throws Exception { + return p.get(); + } + }, ExecutionException.class); + + assertSame(TEST_REASON1, e.getCause()); } @Test public void pending_hasTimeoutSync() { - // Promise.get(timeout, TimeUnit) + Deferred deferred = JQ.defer(); + final Promise p = deferred.promise; + + assertThrows(new Callable() { + @Override + public String call() throws Exception { + return p.get(100, TimeUnit.MILLISECONDS); + } + }, TimeoutException.class); + } + + private void resolveChain(final Future future, final T expected) throws InterruptedException { + final Semaphore resolved = new Semaphore(0); + final Semaphore rejected = new Semaphore(0); + + JQ.resolve(TEST_VALUE1).then(new OnFulfilledCallback() { + @Override + public Future onFulfilled(String value) throws Exception { + return future; + } + }).then(new OnFulfilledCallback() { + @Override + public Future onFulfilled(T value) throws Exception { + assertEquals(expected, value); + resolved.release(); + return null; + } + }).fail(new OnRejectedCallback() { + @Override + public Future onRejected(Exception reason) throws Exception { + assertTrue(false); + rejected.release(); + return null; + } + }); + + assertTrue(resolved.tryAcquire(2, TimeUnit.SECONDS)); + assertFalse(rejected.tryAcquire(2, TimeUnit.SECONDS)); } @Test - public void chained_isResolvedWithValue() { + public void chained_isResolvedWithValue() throws InterruptedException { // OnFulfilled returns Value, chained fulfillment handlers are invoked + resolveChain(Value.wrap(42), 42); } @Test - public void chained_isResolvedWithPromise() { + public void chained_isResolvedWithPromise() throws InterruptedException { // OnFulfilled returns new Promise, chained fulfillment handlers are invoked + + resolveChain(JQ.defer(new SlowTask<>(42)), 42); } @Test - public void chained_isResolvedWithFutureTask() { - // OnFulfilled returns java.util.concurrent.FutureTask, chained fulfillment handlers are invoked + public void chained_isResolvedWithFuture() throws InterruptedException { + // OnFulfilled returns java.util.concurrent.Future, chained fulfillment handlers are invoked + resolveChain(new FutureTask<>(new SlowTask<>(42)), 42); } @Test diff --git a/jq/src/test/java/se/code77/jq/util/Assert.java b/jq/src/test/java/se/code77/jq/util/Assert.java index 90662b5..f235477 100644 --- a/jq/src/test/java/se/code77/jq/util/Assert.java +++ b/jq/src/test/java/se/code77/jq/util/Assert.java @@ -41,12 +41,14 @@ public static void assertRejected(Promise p, Exception reason) { assertSame(snapshot.reason, reason); } - public static void assertThrows(Callable task, Class expectedExceptionClass) { + public static Exception assertThrows(Callable task, Class expectedExceptionClass) { try { task.call(); assertTrue(false); + return null; } catch (Exception e) { assertSame(expectedExceptionClass, e.getClass()); + return e; } } } From 5ba0fe0e1c487e3c18e1e4c638d65d156eafb926 Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Wed, 9 Mar 2016 14:40:43 +0100 Subject: [PATCH 03/11] More tests impl --- .../test/java/se/code77/jq/PromiseTests.java | 353 ++++++++++++------ .../test/java/se/code77/jq/util/Assert.java | 49 ++- .../se/code77/jq/util/BlockingDataHolder.java | 28 ++ .../java/se/code77/jq/util/DataCallback.java | 17 + .../code77/jq/util/DataFulfilledCallback.java | 22 ++ .../code77/jq/util/DataRejectedCallback.java | 21 ++ .../test/java/se/code77/jq/util/SlowTask.java | 37 ++ .../java/se/code77/jq/util/TestConfig.java | 108 ++++-- 8 files changed, 480 insertions(+), 155 deletions(-) create mode 100644 jq/src/test/java/se/code77/jq/util/BlockingDataHolder.java create mode 100644 jq/src/test/java/se/code77/jq/util/DataCallback.java create mode 100644 jq/src/test/java/se/code77/jq/util/DataFulfilledCallback.java create mode 100644 jq/src/test/java/se/code77/jq/util/DataRejectedCallback.java create mode 100644 jq/src/test/java/se/code77/jq/util/SlowTask.java diff --git a/jq/src/test/java/se/code77/jq/PromiseTests.java b/jq/src/test/java/se/code77/jq/PromiseTests.java index 0c7dc30..91e4667 100644 --- a/jq/src/test/java/se/code77/jq/PromiseTests.java +++ b/jq/src/test/java/se/code77/jq/PromiseTests.java @@ -4,19 +4,24 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; -import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import static se.code77.jq.util.Assert.*; import se.code77.jq.JQ.Deferred; import se.code77.jq.Promise.OnFulfilledCallback; import se.code77.jq.Promise.OnRejectedCallback; +import se.code77.jq.Promise.UnhandledRejectionException; import se.code77.jq.util.AsyncTests; +import se.code77.jq.util.BlockingDataHolder; +import se.code77.jq.util.DataFulfilledCallback; +import se.code77.jq.util.DataRejectedCallback; +import se.code77.jq.util.SlowTask; +import se.code77.jq.util.TestConfig; public class PromiseTests extends AsyncTests { private static final String TEST_VALUE1 = "Hello"; @@ -24,37 +29,17 @@ public class PromiseTests extends AsyncTests { private static final Exception TEST_REASON1 = new IllegalArgumentException("foo"); private static final Exception TEST_REASON2 = new IllegalArgumentException("bar"); - private static class SlowTask implements Callable { - public final T value; - - public SlowTask(T value) { - this.value = value; - } - - @Override - public T call() throws Exception { - Thread.sleep(1000); - return value; - } - } - @Test public void pending_isPending() throws Exception { Deferred deferred = JQ.defer(); Promise p = deferred.promise; assertPending(p); - final Semaphore sem = new Semaphore(0); - p.then(new OnFulfilledCallback() { - @Override - public Future onFulfilled(String value) throws Exception { - sem.release(); - return null; - } - }); + BlockingDataHolder then1 = new BlockingDataHolder<>(); + p.then(new DataFulfilledCallback(then1)); - assertFalse(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertNoData(then1, 2000); deferred.resolve(TEST_VALUE1); } @@ -63,19 +48,18 @@ public void resolved_isResolved() throws Exception { Deferred deferred = JQ.defer(); Promise p = deferred.promise; - final Semaphore sem = new Semaphore(0); + // Test one-arg then + BlockingDataHolder then1 = new BlockingDataHolder<>(); + p.then(new DataFulfilledCallback(then1)); - p.then(new OnFulfilledCallback() { - @Override - public Future onFulfilled(String value) throws Exception { - sem.release(); - return null; - } - }); + // Test two-arg then + BlockingDataHolder then2 = new BlockingDataHolder<>(); + p.then(new DataFulfilledCallback(then2), null); deferred.resolve(TEST_VALUE1); - assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertData(then1, 2000, TEST_VALUE1); + assertData(then2, 2000, TEST_VALUE1); assertResolved(p, TEST_VALUE1); } @@ -85,68 +69,61 @@ public void rejected_isRejected() throws Exception { Deferred deferred = JQ.defer(); Promise p = deferred.promise; - final Semaphore sem = new Semaphore(0); + // Test one-arg fail + BlockingDataHolder fail1 = new BlockingDataHolder<>(); + p.fail(new DataRejectedCallback<>(fail1)); - p.fail(new OnRejectedCallback() { - @Override - public Future onRejected(Exception reason) throws Exception { - sem.release(); - return null; - } - }); + // Test two-arg then + BlockingDataHolder fail2 = new BlockingDataHolder<>(); + p.then(null, new DataRejectedCallback<>(fail2)); - IllegalArgumentException reason = new IllegalArgumentException("Foobar"); - deferred.reject(reason); + deferred.reject(TEST_REASON1); - assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); - assertRejected(p, reason); + assertData(fail1, 2000, TEST_REASON1); + assertData(fail2, 2000, TEST_REASON1); + assertRejected(p, TEST_REASON1); } @Test public void preResolved_isResolved() throws Exception { Promise p = JQ.resolve(TEST_VALUE1); - final Semaphore sem = new Semaphore(0); - - p.then(new OnFulfilledCallback() { - @Override - public Future onFulfilled(String value) throws Exception { - sem.release(); - return null; - } - }); + BlockingDataHolder then1 = new BlockingDataHolder<>(); + p.then(new DataFulfilledCallback(then1)); - assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); + assertData(then1, 2000, TEST_VALUE1); assertResolved(p, TEST_VALUE1); } @Test public void preRejected_isRejected() throws Exception { - IllegalArgumentException reason = new IllegalArgumentException("Foobar"); - Promise p = JQ.reject(reason); + Promise p = JQ.reject(TEST_REASON1); - final Semaphore sem = new Semaphore(0); + BlockingDataHolder fail1 = new BlockingDataHolder<>(); + p.fail(new DataRejectedCallback<>(fail1)); - p.fail(new OnRejectedCallback() { - @Override - public Future onRejected(Exception reason) throws Exception { - sem.release(); - return null; - } - }); - - assertTrue(sem.tryAcquire(2, TimeUnit.SECONDS)); - assertRejected(p, reason); + assertData(fail1, 2000, TEST_REASON1); + assertRejected(p, TEST_REASON1); } @Test public void resolved_isNotRejected() { + Promise p = JQ.resolve(TEST_VALUE1); + BlockingDataHolder fail1 = new BlockingDataHolder<>(); + p.fail(new DataRejectedCallback<>(fail1)); + + assertNoData(fail1, 2000); } @Test public void rejected_isNotResolved() { + Promise p = JQ.reject(TEST_REASON1); + + BlockingDataHolder then1 = new BlockingDataHolder<>(); + p.then(new DataFulfilledCallback(then1)); + assertNoData(then1, 2000); } @Test @@ -241,32 +218,18 @@ public String call() throws Exception { } private void resolveChain(final Future future, final T expected) throws InterruptedException { - final Semaphore resolved = new Semaphore(0); - final Semaphore rejected = new Semaphore(0); + final BlockingDataHolder then1 = new BlockingDataHolder<>(); + final BlockingDataHolder then2 = new BlockingDataHolder<>(); + final BlockingDataHolder fail1 = new BlockingDataHolder<>(); - JQ.resolve(TEST_VALUE1).then(new OnFulfilledCallback() { - @Override - public Future onFulfilled(String value) throws Exception { - return future; - } - }).then(new OnFulfilledCallback() { - @Override - public Future onFulfilled(T value) throws Exception { - assertEquals(expected, value); - resolved.release(); - return null; - } - }).fail(new OnRejectedCallback() { - @Override - public Future onRejected(Exception reason) throws Exception { - assertTrue(false); - rejected.release(); - return null; - } - }); + JQ.resolve(TEST_VALUE1).then( + new DataFulfilledCallback<>(then1, future)).then( + new DataFulfilledCallback<>(then2)).fail( + new DataRejectedCallback<>(fail1)); - assertTrue(resolved.tryAcquire(2, TimeUnit.SECONDS)); - assertFalse(rejected.tryAcquire(2, TimeUnit.SECONDS)); + assertData(then1, 2000, TEST_VALUE1); + assertData(then2, 2000, expected); + assertNoData(fail1, 2000); } @Test @@ -283,106 +246,274 @@ public void chained_isResolvedWithPromise() throws InterruptedException { } @Test - public void chained_isResolvedWithFuture() throws InterruptedException { + public void chained_isResolvedWithFutureTask() throws InterruptedException { // OnFulfilled returns java.util.concurrent.Future, chained fulfillment handlers are invoked - resolveChain(new FutureTask<>(new SlowTask<>(42)), 42); + FutureTask future = new FutureTask<>(new SlowTask<>(42)); + Executors.newSingleThreadExecutor().execute(future); + + resolveChain(future, 42); } @Test public void chained_isRejectedWithException() { // OnFulfilled throws exception, chained fulfillment handlers are not invoked, trailing rejection handler is + final BlockingDataHolder then1 = new BlockingDataHolder<>(); + final BlockingDataHolder then2 = new BlockingDataHolder<>(); + final BlockingDataHolder fail1 = new BlockingDataHolder<>(); + + JQ.resolve(TEST_VALUE1).then( + new DataFulfilledCallback(then1) { + @Override + public Future onFulfilled(String value) throws Exception { + super.onFulfilled(value); + throw TEST_REASON1; + } + }).then( + new DataFulfilledCallback<>(then2), new DataRejectedCallback<>(fail1)); + + assertData(then1, 2000, TEST_VALUE1); + assertNoData(then2, 2000); + assertData(fail1, 2000, TEST_REASON1); } @Test public void chained_isResolvedWithoutHandler() { - // resolved promise without fulfillmenthandler -> next promise is resolved with value + // resolved promise without fulfillment handler -> next promise is resolved with value + + final BlockingDataHolder then1 = new BlockingDataHolder<>(); + final BlockingDataHolder fail1 = new BlockingDataHolder<>(); + + JQ.resolve(TEST_VALUE1).fail( + new DataRejectedCallback<>(fail1)).then( + new DataFulfilledCallback<>(then1)); + + assertData(then1, 2000, TEST_VALUE1); + assertNoData(fail1, 2000); } @Test public void chained_isRejectedWithoutHandler() { // rejected promise without rejection handler -> next promise is rejected with reason // (this is standard case and implicitly tested elsewhere, but for clarity has its own test) + + final BlockingDataHolder then1 = new BlockingDataHolder<>(); + final BlockingDataHolder then2 = new BlockingDataHolder<>(); + final BlockingDataHolder fail1 = new BlockingDataHolder<>(); + + JQ.resolve(TEST_VALUE1).then( + new DataFulfilledCallback(then1) { + @Override + public Future onFulfilled(String value) throws Exception { + super.onFulfilled(value); + throw TEST_REASON1; + } + }).then( + new DataFulfilledCallback<>(then2)).fail( + new DataRejectedCallback<>(fail1)); + + assertData(then1, 2000, TEST_VALUE1); + assertNoData(then2, 2000); + assertData(fail1, 2000, TEST_REASON1); } @Test - public void terminated_isRejectedWithoutHandler() { + public void terminated_isRejectedWithoutHandler() throws InterruptedException { // This should throw UnhandledRejectionException + + JQ.resolve(TEST_VALUE1).then( + new OnFulfilledCallback() { + @Override + public Future onFulfilled(String value) throws Exception { + throw TEST_REASON1; + } + }).done(); + + Thread.sleep(1000); + UnhandledRejectionException unhandledException = TestConfig.getTestThread().getUnhandledException(); + assertNotNull(unhandledException); + assertTrue(UnhandledRejectionException.class.isAssignableFrom(unhandledException.getClass())); } @Test public void timeout_isResolved() { // new promise is resolved + BlockingDataHolder then1 = new BlockingDataHolder<>(); + + JQ.resolve(TEST_VALUE1).timeout(1000).then(new DataFulfilledCallback(then1)); + + assertData(then1, 500, TEST_VALUE1); } @Test public void timeout_isRejected() { // new promise is rejected + BlockingDataHolder fail1 = new BlockingDataHolder<>(); + + JQ.reject(TEST_REASON1).timeout(1000).fail(new DataRejectedCallback<>(fail1)); + + assertData(fail1, 500, TEST_REASON1); + } @Test - public void timeout_isResolvedAfterTimeout() { + public void timeout_isResolvedAfterTimeout() throws InterruptedException { // Promise is resolved but too late, TimeoutException is thrown + Promise p = JQ.defer(new SlowTask<>(TEST_VALUE1, 2000)).timeout(1000); + + BlockingDataHolder then1 = new BlockingDataHolder<>(); + p.then(new DataFulfilledCallback<>(then1)); + + Thread.sleep(500); + assertPending(p); + + Thread.sleep(1000); + assertRejected(p, TimeoutException.class); + + assertNoData(then1, 1000); } @Test - public void timeout_isRejectedAfterTimeout() { + public void timeout_isRejectedAfterTimeout() throws InterruptedException { // Promise is rejected but too late, TimeoutException is thrown + Promise p = JQ.defer(new SlowTask(TEST_REASON1, 2000)).timeout(1000); + + BlockingDataHolder fail1 = new BlockingDataHolder<>(); + p.fail(new DataRejectedCallback<>(fail1)); + + Thread.sleep(500); + assertPending(p); + + Thread.sleep(1000); + assertRejected(p, TimeoutException.class); + + assertNoData(fail1, 1000); } @Test - public void delay_isResolved() { + public void delay_isResolved() throws InterruptedException { // new promise is resolved after delay + Promise p = JQ.resolve(TEST_VALUE1).delay(1000); + + Thread.sleep(500); + assertPending(p); + + Thread.sleep(1000); + assertResolved(p, TEST_VALUE1); } @Test - public void delay_isRejected() { + public void delay_isRejected() throws InterruptedException { // new promise is rejected immediately + Promise p = JQ.reject(TEST_REASON1).delay(1000); + + Thread.sleep(500); + assertRejected(p, TEST_REASON1); } @Test - public void delay_isPending() { - // New promise is also pending after delay has passed + public void delay_isPending() throws InterruptedException { + // New promise is also pending after delay has passed if original promise is not resolved yet + Promise p = JQ.defer(new SlowTask(TEST_REASON1, 2000)).delay(1000); + + Thread.sleep(1500); + assertPending(p); + + Thread.sleep(1000); + assertPending(p); + + Thread.sleep(1000); + assertResolved(p, TEST_VALUE1); } @Test - public void join_isResolvedThisThatEqual() { + public void join_isResolvedThisThatEqual() throws InterruptedException { // v1 equals v2 -> new promise should be resolved with v1 + Promise p1 = JQ.resolve(TEST_VALUE1); + Promise p2 = JQ.resolve(TEST_VALUE1); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertResolved(p3, TEST_VALUE1); } @Test - public void join_isResolvedThisThatNotEqual() { + public void join_isResolvedThisThatNotEqual() throws InterruptedException { // v1 not equals v2 -> new promise should be rejected + Promise p1 = JQ.resolve(TEST_VALUE1); + Promise p2 = JQ.resolve(TEST_VALUE2); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertRejected(p3); } @Test - public void join_isResolvedThisNull() { + public void join_isResolvedThisNull() throws InterruptedException { // v1 == null, v2 != null -> new promise should be rejected + Promise p1 = JQ.resolve(null); + Promise p2 = JQ.resolve(TEST_VALUE1); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertRejected(p3); } @Test - public void join_isResolvedThatNull() { + public void join_isResolvedThatNull() throws InterruptedException { // v1 != null, v2 == null -> new promise should be rejected + Promise p1 = JQ.resolve(TEST_VALUE1); + Promise p2 = JQ.resolve(null); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertRejected(p3); } @Test - public void join_isResolvedThisThatNull() { + public void join_isResolvedThisThatNull() throws InterruptedException { // v1 == null, v2 == null -> new promise should be resolved with null + Promise p1 = JQ.resolve(null); + Promise p2 = JQ.resolve(null); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertResolved(p3, null); } @Test - public void join_isThisRejected() { + public void join_isThisRejected() throws InterruptedException { // this rejected, that resolved -> new promise should be rejected + Promise p1 = JQ.defer(new SlowTask(TEST_REASON1, 0)); + Promise p2 = JQ.resolve(TEST_VALUE1); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertRejected(p3); } @Test - public void join_isThatRejected() { + public void join_isThatRejected() throws InterruptedException { // this resolved, that rejected -> new promise should be rejected + Promise p1 = JQ.resolve(TEST_VALUE1); + Promise p2 = JQ.defer(new SlowTask(TEST_REASON1, 0)); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertRejected(p3); } @Test - public void join_isPending() { + public void join_isPending() throws InterruptedException { // this or that is pending -> new promise should be pending + Promise p1 = JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)); + Promise p2 = JQ.resolve(TEST_VALUE1); + Promise p3 = p1.join(p2); + + Thread.sleep(500); + assertPending(p3); + Thread.sleep(1000); + assertResolved(p3, TEST_VALUE1); } } diff --git a/jq/src/test/java/se/code77/jq/util/Assert.java b/jq/src/test/java/se/code77/jq/util/Assert.java index f235477..c20a1d8 100644 --- a/jq/src/test/java/se/code77/jq/util/Assert.java +++ b/jq/src/test/java/se/code77/jq/util/Assert.java @@ -1,6 +1,9 @@ package se.code77.jq.util; +import org.junit.rules.Timeout; + import java.util.concurrent.Callable; +import java.util.concurrent.TimeoutException; import se.code77.jq.Promise; @@ -29,7 +32,7 @@ public static void assertResolved(Promise p, T expected) { assertNull(snapshot.reason); } - public static void assertRejected(Promise p, Exception reason) { + public static void assertRejected(Promise p) { assertFalse(p.isPending()); assertFalse(p.isFulfilled()); assertTrue(p.isRejected()); @@ -38,7 +41,15 @@ public static void assertRejected(Promise p, Exception reason) { Promise.StateSnapshot snapshot = p.inspect(); assertSame(snapshot.state, Promise.State.REJECTED); assertNull(snapshot.value); - assertSame(snapshot.reason, reason); + } + public static void assertRejected(Promise p, Exception reason) { + assertRejected(p); + assertSame(p.inspect().reason, reason); + } + + public static void assertRejected(Promise p, Class reasonClass) { + assertRejected(p); + assertTrue(reasonClass.isAssignableFrom(p.inspect().reason.getClass())); } public static Exception assertThrows(Callable task, Class expectedExceptionClass) { @@ -47,8 +58,40 @@ public static Exception assertThrows(Callable task, Class void assertData(BlockingDataHolder holder, long timeoutMillis, T expected) { + try { + assertEquals(expected, holder.get(timeoutMillis)); + } catch (TimeoutException e) { + fail("Data not set"); + } catch (InterruptedException e) { + fail("Test interrupted"); + } + + } + + public static void assertData(BlockingDataHolder holder, long timeoutMillis) { + try { + holder.get(timeoutMillis); + } catch (TimeoutException e) { + fail("Data was not set"); + } catch (InterruptedException e) { + fail("Test interrupted"); + } + } + + public static void assertNoData(BlockingDataHolder holder, long timeoutMillis) { + try { + holder.get(timeoutMillis); + fail("Data was set"); + } catch (TimeoutException e) { + } catch (InterruptedException e) { + fail("Test interrupted"); + } + } + } diff --git a/jq/src/test/java/se/code77/jq/util/BlockingDataHolder.java b/jq/src/test/java/se/code77/jq/util/BlockingDataHolder.java new file mode 100644 index 0000000..8b10d49 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/BlockingDataHolder.java @@ -0,0 +1,28 @@ +package se.code77.jq.util; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class BlockingDataHolder { + private Semaphore mSem = new Semaphore(0); + private T mData; + + public void set(T data) { + mData = data; + mSem.release(); + } + + public void set() { + set(null); + } + + public T get(long timeoutMillis) throws InterruptedException, TimeoutException { + if (mSem.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)) { + return mData; + } else { + throw new TimeoutException("Data not set"); + } + } + +} diff --git a/jq/src/test/java/se/code77/jq/util/DataCallback.java b/jq/src/test/java/se/code77/jq/util/DataCallback.java new file mode 100644 index 0000000..ee705b0 --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/DataCallback.java @@ -0,0 +1,17 @@ +package se.code77.jq.util; + +import java.util.concurrent.Future; + +public class DataCallback { + protected final BlockingDataHolder mHolder; + protected Future mNextValue; + + public DataCallback(BlockingDataHolder holder) { + mHolder = holder; + } + + public DataCallback(BlockingDataHolder holder, Future nextValue) { + mHolder = holder; + mNextValue = nextValue; + } +} diff --git a/jq/src/test/java/se/code77/jq/util/DataFulfilledCallback.java b/jq/src/test/java/se/code77/jq/util/DataFulfilledCallback.java new file mode 100644 index 0000000..31cbc3a --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/DataFulfilledCallback.java @@ -0,0 +1,22 @@ +package se.code77.jq.util; + +import java.util.concurrent.Future; + +import se.code77.jq.Promise.OnFulfilledCallback; + +public class DataFulfilledCallback extends DataCallback implements OnFulfilledCallback { + + public DataFulfilledCallback(BlockingDataHolder holder) { + super(holder); + } + + public DataFulfilledCallback(BlockingDataHolder holder, Future nextValue) { + super(holder, nextValue); + } + + @Override + public Future onFulfilled(V value) throws Exception { + mHolder.set(value); + return mNextValue; + } +} diff --git a/jq/src/test/java/se/code77/jq/util/DataRejectedCallback.java b/jq/src/test/java/se/code77/jq/util/DataRejectedCallback.java new file mode 100644 index 0000000..ef641ea --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/DataRejectedCallback.java @@ -0,0 +1,21 @@ +package se.code77.jq.util; + +import java.util.concurrent.Future; + +import se.code77.jq.Promise.OnRejectedCallback; + +public class DataRejectedCallback extends DataCallback implements OnRejectedCallback { + public DataRejectedCallback(BlockingDataHolder holder) { + super(holder); + } + + public DataRejectedCallback(BlockingDataHolder holder, Future nextValue) { + super(holder, nextValue); + } + + @Override + public Future onRejected(Exception reason) throws Exception { + mHolder.set(reason); + return mNextValue; + } +} diff --git a/jq/src/test/java/se/code77/jq/util/SlowTask.java b/jq/src/test/java/se/code77/jq/util/SlowTask.java new file mode 100644 index 0000000..30d621a --- /dev/null +++ b/jq/src/test/java/se/code77/jq/util/SlowTask.java @@ -0,0 +1,37 @@ +package se.code77.jq.util; + +import java.util.concurrent.Callable; + +public class SlowTask implements Callable { + private T mValue; + private Exception mReason; + private long mDelayMillis; + + public SlowTask(T value) { + this(value, 1000); + } + + public SlowTask(T value, long delayMillis) { + mValue = value; + mDelayMillis = delayMillis; + } + + public SlowTask(Exception reason) { + this(reason, 1000); + } + + public SlowTask(Exception reason, long delayMillis) { + mReason = reason; + mDelayMillis = delayMillis; + } + + @Override + public T call() throws Exception { + Thread.sleep(mDelayMillis); + if (mReason != null) { + throw mReason; + } else { + return mValue; + } + } +} diff --git a/jq/src/test/java/se/code77/jq/util/TestConfig.java b/jq/src/test/java/se/code77/jq/util/TestConfig.java index aa92764..3b21b48 100644 --- a/jq/src/test/java/se/code77/jq/util/TestConfig.java +++ b/jq/src/test/java/se/code77/jq/util/TestConfig.java @@ -2,50 +2,60 @@ import java.util.PriorityQueue; +import se.code77.jq.Promise; +import se.code77.jq.Promise.UnhandledRejectionException; import se.code77.jq.config.Config; public class TestConfig { - public static void init() { - final TestThread tt = new TestThread(); - tt.start(); + private static TestThread sTestThread; - Config.setConfig(new Config(true) { - @Override - public Dispatcher createDispatcher() { - return new TestDispatcher(tt); - } - - @Override - public Logger getLogger() { - return new Logger() { - private void log(String level, String s) { - System.out.println(System.currentTimeMillis() + " [PROMISE." + level + "] " - + s); - } - - @Override - public void debug(String s) { - log("debug", s); - } - - @Override - public void info(String s) { - log("info", s); - } + public static TestThread getTestThread() { + return sTestThread; + } - @Override - public void warn(String s) { - log("warn", s); - } + public static synchronized void init() { + if (sTestThread == null) { + sTestThread = new TestThread("Async event thread"); + sTestThread.start(); - @Override - public void error(String s) { - log("error", s); - } + Config.setConfig(new Config(true) { + @Override + public Dispatcher createDispatcher() { + return new TestDispatcher(sTestThread); + } - }; - } - }); + @Override + public Logger getLogger() { + return new Logger() { + private void log(String level, String s) { + System.out.println(System.currentTimeMillis() + " [PROMISE." + level + "] " + + s); + } + + @Override + public void debug(String s) { + log("debug", s); + } + + @Override + public void info(String s) { + log("info", s); + } + + @Override + public void warn(String s) { + log("warn", s); + } + + @Override + public void error(String s) { + log("error", s); + } + + }; + } + }); + } } @@ -73,7 +83,11 @@ public void dispatch(Runnable r, long ms) { } - private static class TestThread extends Thread { + public static class TestThread extends Thread { + public TestThread(String name) { + super(name); + } + private static class Event implements Comparable { public final Runnable r; public final long due; @@ -91,6 +105,7 @@ public int compareTo(Event o) { private final PriorityQueue mEvents = new PriorityQueue(); private boolean mStopped; + private UnhandledRejectionException mUnhandledException; private synchronized Event getEvent() throws InterruptedException { while (!Thread.interrupted()) { @@ -117,12 +132,19 @@ public void run() { System.out.println("Starting event thread"); while (!mStopped) { try { - Event e = getEvent(); - - e.r.run(); + Event event = getEvent(); + + try { + event.r.run(); + } catch (UnhandledRejectionException e) { + System.out.println("Exception in dispatched event " + e); + mStopped = true; + mUnhandledException = e; + } } catch (InterruptedException ex) { } } + System.out.println("Exiting event thread"); } public void exit() { @@ -135,5 +157,9 @@ public synchronized void addEvent(Runnable r, long ms) { mEvents.add(new Event(r, System.currentTimeMillis() + ms)); notify(); } + + public UnhandledRejectionException getUnhandledException() { + return mUnhandledException; + } } } From 04cfd8296169a34e541fc9a2de92dc867146975e Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Wed, 9 Mar 2016 15:40:28 +0100 Subject: [PATCH 04/11] Completing PromiseTests --- jq/src/test/java/se/code77/jq/PromiseTests.java | 16 ++++++++++------ .../test/java/se/code77/jq/util/AsyncTests.java | 13 +++++++++++-- .../test/java/se/code77/jq/util/TestConfig.java | 8 +++++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/jq/src/test/java/se/code77/jq/PromiseTests.java b/jq/src/test/java/se/code77/jq/PromiseTests.java index 91e4667..17f3d63 100644 --- a/jq/src/test/java/se/code77/jq/PromiseTests.java +++ b/jq/src/test/java/se/code77/jq/PromiseTests.java @@ -378,16 +378,11 @@ public void timeout_isRejectedAfterTimeout() throws InterruptedException { // Promise is rejected but too late, TimeoutException is thrown Promise p = JQ.defer(new SlowTask(TEST_REASON1, 2000)).timeout(1000); - BlockingDataHolder fail1 = new BlockingDataHolder<>(); - p.fail(new DataRejectedCallback<>(fail1)); - Thread.sleep(500); assertPending(p); Thread.sleep(1000); assertRejected(p, TimeoutException.class); - - assertNoData(fail1, 1000); } @Test @@ -414,7 +409,7 @@ public void delay_isRejected() throws InterruptedException { @Test public void delay_isPending() throws InterruptedException { // New promise is also pending after delay has passed if original promise is not resolved yet - Promise p = JQ.defer(new SlowTask(TEST_REASON1, 2000)).delay(1000); + Promise p = JQ.defer(new SlowTask<>(TEST_VALUE1, 2000)).delay(1000); Thread.sleep(1500); assertPending(p); @@ -514,6 +509,15 @@ public void join_isPending() throws InterruptedException { assertPending(p3); Thread.sleep(1000); assertResolved(p3, TEST_VALUE1); + + p1 = JQ.resolve(TEST_VALUE1); + p2 = JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)); + p3 = p1.join(p2); + + Thread.sleep(500); + assertPending(p3); + Thread.sleep(1000); + assertResolved(p3, TEST_VALUE1); } } diff --git a/jq/src/test/java/se/code77/jq/util/AsyncTests.java b/jq/src/test/java/se/code77/jq/util/AsyncTests.java index 735e232..ac311f1 100644 --- a/jq/src/test/java/se/code77/jq/util/AsyncTests.java +++ b/jq/src/test/java/se/code77/jq/util/AsyncTests.java @@ -1,7 +1,16 @@ package se.code77.jq.util; +import org.junit.After; +import org.junit.Before; + public class AsyncTests { - public AsyncTests() { - TestConfig.init(); + @Before + public void setup() { + TestConfig.start(); + } + + @After + public void tearDown() { + TestConfig.stop(); } } diff --git a/jq/src/test/java/se/code77/jq/util/TestConfig.java b/jq/src/test/java/se/code77/jq/util/TestConfig.java index 3b21b48..dcb43f4 100644 --- a/jq/src/test/java/se/code77/jq/util/TestConfig.java +++ b/jq/src/test/java/se/code77/jq/util/TestConfig.java @@ -13,7 +13,7 @@ public static TestThread getTestThread() { return sTestThread; } - public static synchronized void init() { + public static synchronized void start() { if (sTestThread == null) { sTestThread = new TestThread("Async event thread"); sTestThread.start(); @@ -56,7 +56,13 @@ public void error(String s) { } }); } + } + public static synchronized void stop() { + if (sTestThread != null) { + sTestThread.exit(); + sTestThread = null; + } } private static class TestDispatcher implements Config.Dispatcher { From 7a3c0fdd7c5dd4003b5ad1dc63bdf5bc632665ed Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Wed, 9 Mar 2016 15:42:22 +0100 Subject: [PATCH 05/11] Fix race condition in JQ.all This could happen if JQ.all is invoked on another thread than the dispatcher thread. Depending on the Config's implementation of Dispatcher this could occur. --- jq/src/main/java/se/code77/jq/JQ.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jq/src/main/java/se/code77/jq/JQ.java b/jq/src/main/java/se/code77/jq/JQ.java index 185f483..04d38ae 100644 --- a/jq/src/main/java/se/code77/jq/JQ.java +++ b/jq/src/main/java/se/code77/jq/JQ.java @@ -588,10 +588,13 @@ public final void checkStates(final List> promises) { mFulfilledCount = 0; mRejectedCount = 0; + for (Promise p : promises) { + mStates.add(p.inspect()); + } + for (int i = 0; i < promises.size(); i++) { final int pos = i; final Promise p = promises.get(pos); - mStates.add(p.inspect()); p.then(new Promise.OnFulfilledCallback() { @Override From e89061bca798547f03e4965ce1fb4a11533879eb Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Wed, 9 Mar 2016 15:49:32 +0100 Subject: [PATCH 06/11] JQTests. WIP. --- jq/src/test/java/se/code77/jq/JQTests.java | 31 ++++++++++++++++--- .../test/java/se/code77/jq/PromiseTests.java | 5 --- .../java/se/code77/jq/util/AsyncTests.java | 5 +++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/jq/src/test/java/se/code77/jq/JQTests.java b/jq/src/test/java/se/code77/jq/JQTests.java index 7e97464..b4a355d 100644 --- a/jq/src/test/java/se/code77/jq/JQTests.java +++ b/jq/src/test/java/se/code77/jq/JQTests.java @@ -2,12 +2,16 @@ import org.junit.Test; +import java.util.Arrays; +import java.util.List; + import static se.code77.jq.util.Assert.*; import se.code77.jq.JQ.Deferred; import se.code77.jq.Promise.OnFulfilledCallback; import se.code77.jq.Promise.OnRejectedCallback; import se.code77.jq.util.AsyncTests; +import se.code77.jq.util.SlowTask; public class JQTests extends AsyncTests { @Test @@ -31,23 +35,42 @@ public void reject_isRejected() { } @Test - public void all_isResolvedList() { + public void all_isResolvedList() throws InterruptedException { + Promise> p = JQ.all(Arrays.asList(JQ.resolve(TEST_VALUE1), JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); + Thread.sleep(500); + assertPending(p); + Thread.sleep(1500); + assertResolved(p, Arrays.asList(TEST_VALUE1, TEST_VALUE2)); } @Test - public void all_isResolvedVarArg() { + public void all_isResolvedVarArg() throws InterruptedException { + Promise> p = JQ.all(JQ.resolve(TEST_VALUE1), JQ.resolve(TEST_VALUE2)); + Thread.sleep(100); + assertResolved(p, Arrays.asList(TEST_VALUE1, TEST_VALUE2)); } @Test - public void all_isRejected() { + public void all_isRejected() throws InterruptedException { + Promise> p = JQ.all(JQ.resolve(TEST_VALUE1), JQ.defer(new SlowTask(TEST_REASON1, 1000))); + Thread.sleep(500); + assertPending(p); + Thread.sleep(1500); + assertRejected(p); } @Test - public void all_isPending() { + public void all_isPending() throws InterruptedException { // at least one promise is not done -> resulting is pending forever + Promise> p = JQ.all(JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), JQ.defer(new SlowTask(TEST_REASON1, 2000))); + + Thread.sleep(500); + assertPending(p); + Thread.sleep(1000); + assertPending(p); } @Test diff --git a/jq/src/test/java/se/code77/jq/PromiseTests.java b/jq/src/test/java/se/code77/jq/PromiseTests.java index 17f3d63..05e3d37 100644 --- a/jq/src/test/java/se/code77/jq/PromiseTests.java +++ b/jq/src/test/java/se/code77/jq/PromiseTests.java @@ -14,7 +14,6 @@ import se.code77.jq.JQ.Deferred; import se.code77.jq.Promise.OnFulfilledCallback; -import se.code77.jq.Promise.OnRejectedCallback; import se.code77.jq.Promise.UnhandledRejectionException; import se.code77.jq.util.AsyncTests; import se.code77.jq.util.BlockingDataHolder; @@ -24,10 +23,6 @@ import se.code77.jq.util.TestConfig; public class PromiseTests extends AsyncTests { - private static final String TEST_VALUE1 = "Hello"; - private static final String TEST_VALUE2 = "World"; - private static final Exception TEST_REASON1 = new IllegalArgumentException("foo"); - private static final Exception TEST_REASON2 = new IllegalArgumentException("bar"); @Test public void pending_isPending() throws Exception { diff --git a/jq/src/test/java/se/code77/jq/util/AsyncTests.java b/jq/src/test/java/se/code77/jq/util/AsyncTests.java index ac311f1..ec98ee1 100644 --- a/jq/src/test/java/se/code77/jq/util/AsyncTests.java +++ b/jq/src/test/java/se/code77/jq/util/AsyncTests.java @@ -4,6 +4,11 @@ import org.junit.Before; public class AsyncTests { + protected static final String TEST_VALUE1 = "Hello"; + protected static final String TEST_VALUE2 = "World"; + protected static final Exception TEST_REASON1 = new IllegalArgumentException("foo"); + protected static final Exception TEST_REASON2 = new IllegalArgumentException("bar"); + @Before public void setup() { TestConfig.start(); From edc5de4d0fddce74056767aa263a2a7964ddc84b Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Thu, 10 Mar 2016 12:16:32 +0100 Subject: [PATCH 07/11] Implement remaining unit tests --- jq/src/test/java/se/code77/jq/JQTests.java | 256 +++++++++++++++--- .../test/java/se/code77/jq/util/Assert.java | 10 +- .../test/java/se/code77/jq/util/SlowTask.java | 12 +- .../java/se/code77/jq/util/TestConfig.java | 122 ++++++--- 4 files changed, 322 insertions(+), 78 deletions(-) diff --git a/jq/src/test/java/se/code77/jq/JQTests.java b/jq/src/test/java/se/code77/jq/JQTests.java index b4a355d..cacec2b 100644 --- a/jq/src/test/java/se/code77/jq/JQTests.java +++ b/jq/src/test/java/se/code77/jq/JQTests.java @@ -4,21 +4,26 @@ import java.util.Arrays; import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import static se.code77.jq.util.Assert.*; -import se.code77.jq.JQ.Deferred; -import se.code77.jq.Promise.OnFulfilledCallback; -import se.code77.jq.Promise.OnRejectedCallback; +import se.code77.jq.Promise.State; +import se.code77.jq.Promise.StateSnapshot; import se.code77.jq.util.AsyncTests; import se.code77.jq.util.SlowTask; +import se.code77.jq.util.TestConfig; public class JQTests extends AsyncTests { @Test public void resolve_isResolved() { - String value = "Hello"; - Promise p = JQ.resolve(value); - assertResolved(p, value); + Promise p = JQ.resolve(TEST_VALUE1); + assertResolved(p, TEST_VALUE1); } @Test @@ -28,44 +33,48 @@ public void resolveVoid_isResolved() { } @Test - public void reject_isRejected() { - IllegalArgumentException reason = new IllegalArgumentException("foobar"); - Promise p = JQ.reject(reason); - assertRejected(p, reason); + public void reject_isRejected() throws InterruptedException { + Promise p = JQ.reject(TEST_REASON1); + TestConfig.waitForIdle(); + assertRejected(p, TEST_REASON1); } @Test public void all_isResolvedList() throws InterruptedException { - Promise> p = JQ.all(Arrays.asList(JQ.resolve(TEST_VALUE1), JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); + Promise> p = JQ.all( + Arrays.asList(JQ.resolve(TEST_VALUE1), JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); Thread.sleep(500); assertPending(p); - Thread.sleep(1500); + Thread.sleep(1000); assertResolved(p, Arrays.asList(TEST_VALUE1, TEST_VALUE2)); } @Test public void all_isResolvedVarArg() throws InterruptedException { - Promise> p = JQ.all(JQ.resolve(TEST_VALUE1), JQ.resolve(TEST_VALUE2)); + Promise> p = JQ.all( + JQ.resolve(TEST_VALUE1), JQ.resolve(TEST_VALUE2)); - Thread.sleep(100); + TestConfig.waitForIdle(); assertResolved(p, Arrays.asList(TEST_VALUE1, TEST_VALUE2)); } @Test public void all_isRejected() throws InterruptedException { - Promise> p = JQ.all(JQ.resolve(TEST_VALUE1), JQ.defer(new SlowTask(TEST_REASON1, 1000))); + Promise> p = JQ.all( + JQ.resolve(TEST_VALUE1), JQ.defer(new SlowTask(TEST_REASON1, 1000))); Thread.sleep(500); assertPending(p); Thread.sleep(1500); - assertRejected(p); + assertRejected(p, TEST_REASON1); } @Test public void all_isPending() throws InterruptedException { // at least one promise is not done -> resulting is pending forever - Promise> p = JQ.all(JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), JQ.defer(new SlowTask(TEST_REASON1, 2000))); + Promise> p = JQ.all( + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), JQ.defer(new SlowTask(TEST_REASON1, 2000))); Thread.sleep(500); assertPending(p); @@ -74,103 +83,276 @@ public void all_isPending() throws InterruptedException { } @Test - public void any_isResolvedByFirst() { + public void any_isResolvedByFirst() throws InterruptedException { + Promise p = JQ.any( + Arrays.asList(JQ.defer(new SlowTask<>(TEST_VALUE1, 100)), JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); + Thread.sleep(500); + assertResolved(p, TEST_VALUE1); } @Test - public void any_isResolvedByLast() { + public void any_isResolvedByFirstAfterOtherRejected() throws InterruptedException { + Promise p = JQ.any( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 200)), + JQ.defer(new SlowTask(TEST_REASON1, 100)))); + Thread.sleep(500); + assertResolved(p, TEST_VALUE1); } @Test - public void any_isRejected() { + public void any_isResolvedByLast() throws InterruptedException { + Promise p = JQ.any( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 100)))); + Thread.sleep(500); + assertResolved(p, TEST_VALUE2); + } + + @Test + public void any_isResolvedByLastAfterOtherRejected() throws InterruptedException { + Promise p = JQ.any( + Arrays.asList( + JQ.defer(new SlowTask(TEST_REASON1, 100)), + JQ.defer(new SlowTask<>(TEST_VALUE1, 200)))); + + Thread.sleep(500); + assertResolved(p, TEST_VALUE1); + } + + @Test + public void any_isRejected() throws InterruptedException { + Promise p = JQ.any( + Arrays.asList( + JQ.defer(new SlowTask(TEST_REASON1, 100)), + JQ.defer(new SlowTask(TEST_REASON2, 200)))); + + Thread.sleep(500); + assertRejected(p); } @Test - public void any_isPending() { + public void any_isPending() throws InterruptedException { + Promise p = JQ.any( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 2000)))); + Thread.sleep(500); + assertPending(p); } @Test - public void race_isResolvedByFirst() { + public void race_isResolvedByFirst() throws InterruptedException { + Promise p = JQ.race( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 100)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); + Thread.sleep(500); + assertResolved(p, TEST_VALUE1); } @Test - public void race_isResolvedByLast() { + public void race_isResolvedByLast() throws InterruptedException { + Promise p = JQ.race( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 100)))); + Thread.sleep(500); + assertResolved(p, TEST_VALUE2); } @Test - public void race_isRejectedByFirst() { + public void race_isRejectedByFirst() throws InterruptedException { + Promise p = JQ.race( + Arrays.asList( + JQ.defer(new SlowTask(TEST_REASON1, 100)), + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)))); + Thread.sleep(500); + assertRejected(p, TEST_REASON1); } @Test - public void race_isRejectedByLast() { + public void race_isRejectedByLast() throws InterruptedException { + Promise p = JQ.race( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), + JQ.defer(new SlowTask(TEST_REASON1, 100)))); + Thread.sleep(500); + assertRejected(p, TEST_REASON1); } @Test - public void race_isPending() { + public void race_isPending() throws InterruptedException { + Promise p = JQ.race( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 1000)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 2000)))); + Thread.sleep(500); + assertPending(p); } @Test - public void allSettled_isResolvedAllResolved() { + public void allSettled_isResolvedAllResolved() throws InterruptedException { + Promise>> p = JQ.allSettled( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 100)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); + Thread.sleep(500); + assertPending(p); + + Thread.sleep(1000); + assertResolved(p, Arrays.asList( + new StateSnapshot<>(State.FULFILLED, TEST_VALUE1, null), + new StateSnapshot<>(State.FULFILLED, TEST_VALUE2, null))); } @Test - public void allSettled_isResolvedAllResolvedOrRejected() { + public void allSettled_isResolvedAllResolvedOrRejected() throws InterruptedException { + Promise>> p = JQ.allSettled( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 100)), + JQ.defer(new SlowTask(TEST_REASON1, 200)), + JQ.defer(new SlowTask(TEST_REASON2, 1000)))); + + Thread.sleep(500); + assertPending(p); + Thread.sleep(1000); + assertResolved(p, Arrays.asList( + new StateSnapshot<>(State.FULFILLED, TEST_VALUE1, null), + new StateSnapshot(State.REJECTED, null, TEST_REASON1), + new StateSnapshot(State.REJECTED, null, TEST_REASON2))); } @Test - public void allSettled_isResolvedAllRejected() { + public void allSettled_isResolvedAllRejected() throws InterruptedException { + Promise>> p = JQ.allSettled( + Arrays.asList( + JQ.defer(new SlowTask(TEST_REASON1, 100)), + JQ.defer(new SlowTask(TEST_REASON2, 1000)))); + + Thread.sleep(500); + assertPending(p); + Thread.sleep(1000); + assertResolved(p, Arrays.asList( + new StateSnapshot(State.REJECTED, null, TEST_REASON1), + new StateSnapshot(State.REJECTED, null, TEST_REASON2))); } @Test - public void allSettled_isPending() { + public void allSettled_isPending() throws InterruptedException { + Promise>> p = JQ.allSettled( + Arrays.asList( + JQ.defer(new SlowTask<>(TEST_VALUE1, 100)), + JQ.defer(new SlowTask<>(TEST_VALUE2, 1000)))); + Thread.sleep(500); + assertPending(p); } @Test public void wrap_isReturnedForPromise() { + Future future = JQ.resolve(TEST_VALUE1); + assertEquals(future, JQ.wrap(future)); } @Test - public void wrap_isResolvedForValue() { + public void wrap_isResolvedForValue() throws InterruptedException { + Future future = Value.wrap(TEST_VALUE1); + Promise p = JQ.wrap(future); + + TestConfig.waitForIdle(); + assertResolved(p, TEST_VALUE1); } @Test - public void wrap_isResolvedForFutureTaskCompleted() { + public void wrap_isResolvedForFutureTaskCompleted() throws InterruptedException { + FutureTask future = new FutureTask<>(new SlowTask<>(TEST_VALUE1, 1000)); + Executors.newSingleThreadExecutor().execute(future); + + Promise p = JQ.wrap(future); + Thread.sleep(500); + assertPending(p); + + Thread.sleep(1000); + assertResolved(p, TEST_VALUE1); } @Test - public void wrap_isRejectedForFutureTaskFailedWithException() { + public void wrap_isRejectedForFutureTaskFailedWithException() throws InterruptedException { + FutureTask future = new FutureTask<>(new SlowTask(TEST_REASON1, 1000)); + Executors.newSingleThreadExecutor().execute(future); + + Promise p = JQ.wrap(future); + + Thread.sleep(500); + assertPending(p); + Thread.sleep(1000); + assertRejected(p, TEST_REASON1); } @Test - public void wrap_isRejectedForFutureTaskFailedWithError() { - // Hmm consider if this should be rejected with dummy Exception, use ExecutionException(Throwable) or actually throw the Error + public void wrap_isRejectedForFutureTaskFailedWithError() throws InterruptedException { + FutureTask future = new FutureTask<>(new SlowTask(new Error("Foobar!"), 1000)); + Executors.newSingleThreadExecutor().execute(future); + + Promise p = JQ.wrap(future); + + Thread.sleep(500); + assertPending(p); + + Thread.sleep(1000); + assertRejected(p, ExecutionException.class); } @Test - public void wrap_isRejectedForFutureTaskInterrupted() { + public void wrap_isRejectedForFutureTaskInterrupted() throws InterruptedException { + FutureTask future = new FutureTask<>(new SlowTask<>(TEST_VALUE1, 1000)); + ExecutorService es = Executors.newSingleThreadExecutor(); + es.execute(future); + + Promise p = JQ.wrap(future); + + Thread.sleep(500); + assertPending(p); + es.shutdownNow(); + + Thread.sleep(500); + assertRejected(p, InterruptedException.class); } @Test - public void wrap_isRejectedForFutureTaskCancelled() { + public void wrap_isRejectedForFutureTaskCancelled() throws InterruptedException { + FutureTask future = new FutureTask<>(new SlowTask<>(TEST_VALUE1, 1000)); + ExecutorService es = Executors.newSingleThreadExecutor(); + es.execute(future); + + Promise p = JQ.wrap(future); + + Thread.sleep(500); + assertPending(p); + future.cancel(true); + + Thread.sleep(500); + assertRejected(p, CancellationException.class); } // when, fail, done, timeout, delay are just convenience wrappers. That's not evident from a black box pov, but are they worth writing tests for? diff --git a/jq/src/test/java/se/code77/jq/util/Assert.java b/jq/src/test/java/se/code77/jq/util/Assert.java index c20a1d8..0fcaa86 100644 --- a/jq/src/test/java/se/code77/jq/util/Assert.java +++ b/jq/src/test/java/se/code77/jq/util/Assert.java @@ -15,7 +15,7 @@ public static void assertPending(Promise p) { assertFalse(p.isCancelled()); assertFalse(p.isDone()); Promise.StateSnapshot snapshot = p.inspect(); - assertSame(snapshot.state, Promise.State.PENDING); + assertSame(Promise.State.PENDING, snapshot.state); assertNull(snapshot.value); assertNull(snapshot.reason); } @@ -27,8 +27,8 @@ public static void assertResolved(Promise p, T expected) { assertFalse(p.isCancelled()); assertTrue(p.isDone()); Promise.StateSnapshot snapshot = p.inspect(); - assertSame(snapshot.state, Promise.State.FULFILLED); - assertEquals(snapshot.value, expected); + assertSame(Promise.State.FULFILLED, snapshot.state); + assertEquals(expected, snapshot.value); assertNull(snapshot.reason); } @@ -39,12 +39,12 @@ public static void assertRejected(Promise p) { assertFalse(p.isCancelled()); assertTrue(p.isDone()); Promise.StateSnapshot snapshot = p.inspect(); - assertSame(snapshot.state, Promise.State.REJECTED); + assertSame(Promise.State.REJECTED, snapshot.state); assertNull(snapshot.value); } public static void assertRejected(Promise p, Exception reason) { assertRejected(p); - assertSame(p.inspect().reason, reason); + assertSame(reason, p.inspect().reason); } public static void assertRejected(Promise p, Class reasonClass) { diff --git a/jq/src/test/java/se/code77/jq/util/SlowTask.java b/jq/src/test/java/se/code77/jq/util/SlowTask.java index 30d621a..b12c3a1 100644 --- a/jq/src/test/java/se/code77/jq/util/SlowTask.java +++ b/jq/src/test/java/se/code77/jq/util/SlowTask.java @@ -4,7 +4,7 @@ public class SlowTask implements Callable { private T mValue; - private Exception mReason; + private Throwable mReason; private long mDelayMillis; public SlowTask(T value) { @@ -16,11 +16,11 @@ public SlowTask(T value, long delayMillis) { mDelayMillis = delayMillis; } - public SlowTask(Exception reason) { + public SlowTask(Throwable reason) { this(reason, 1000); } - public SlowTask(Exception reason, long delayMillis) { + public SlowTask(Throwable reason, long delayMillis) { mReason = reason; mDelayMillis = delayMillis; } @@ -28,8 +28,10 @@ public SlowTask(Exception reason, long delayMillis) { @Override public T call() throws Exception { Thread.sleep(mDelayMillis); - if (mReason != null) { - throw mReason; + if (mReason instanceof Exception) { + throw (Exception)mReason; + } else if (mReason instanceof Error) { + throw (Error)mReason; } else { return mValue; } diff --git a/jq/src/test/java/se/code77/jq/util/TestConfig.java b/jq/src/test/java/se/code77/jq/util/TestConfig.java index dcb43f4..fcba639 100644 --- a/jq/src/test/java/se/code77/jq/util/TestConfig.java +++ b/jq/src/test/java/se/code77/jq/util/TestConfig.java @@ -2,9 +2,9 @@ import java.util.PriorityQueue; -import se.code77.jq.Promise; import se.code77.jq.Promise.UnhandledRejectionException; import se.code77.jq.config.Config; +import se.code77.jq.config.Config.LogLevel; public class TestConfig { private static TestThread sTestThread; @@ -18,7 +18,7 @@ public static synchronized void start() { sTestThread = new TestThread("Async event thread"); sTestThread.start(); - Config.setConfig(new Config(true) { + Config.setConfig(new Config(false) { @Override public Dispatcher createDispatcher() { return new TestDispatcher(sTestThread); @@ -26,33 +26,7 @@ public Dispatcher createDispatcher() { @Override public Logger getLogger() { - return new Logger() { - private void log(String level, String s) { - System.out.println(System.currentTimeMillis() + " [PROMISE." + level + "] " - + s); - } - - @Override - public void debug(String s) { - log("debug", s); - } - - @Override - public void info(String s) { - log("info", s); - } - - @Override - public void warn(String s) { - log("warn", s); - } - - @Override - public void error(String s) { - log("error", s); - } - - }; + return new TestLogger(LogLevel.DEBUG); } }); } @@ -60,11 +34,23 @@ public void error(String s) { public static synchronized void stop() { if (sTestThread != null) { - sTestThread.exit(); + try { + sTestThread.waitForIdle(); + sTestThread.exit(); + sTestThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } sTestThread = null; } } + public static synchronized void waitForIdle() throws InterruptedException { + if (sTestThread != null) { + sTestThread.waitForIdle(); + } + } + private static class TestDispatcher implements Config.Dispatcher { private TestThread mThread; @@ -111,12 +97,16 @@ public int compareTo(Event o) { private final PriorityQueue mEvents = new PriorityQueue(); private boolean mStopped; + private boolean mIdle = true; + private final Object mIdleLock = new Object(); private UnhandledRejectionException mUnhandledException; private synchronized Event getEvent() throws InterruptedException { while (!Thread.interrupted()) { Event e = mEvents.peek(); + setIdle(e == null); + if (e == null) { wait(); } else { @@ -161,11 +151,81 @@ public void exit() { public synchronized void addEvent(Runnable r, long ms) { mEvents.add(new Event(r, System.currentTimeMillis() + ms)); - notify(); + notifyAll(); + } + + private void setIdle(boolean idle) { + synchronized (mIdleLock) { + mIdle = idle; + mIdleLock.notifyAll(); + } + } + + public void waitForIdle() throws InterruptedException { + waitForIdle(100); + } + + public void waitForIdle(long margin) throws InterruptedException { + if (mStopped) { + return; + } + synchronized (mIdleLock) { + do { + while (!mIdle) { + mIdleLock.wait(); + } + Thread.sleep(margin); + } while (!mIdle); + } } public UnhandledRejectionException getUnhandledException() { return mUnhandledException; } } + + private static class TestLogger implements Config.Logger { + private final LogLevel mLogLevel; + + public TestLogger(LogLevel logLevel) { + mLogLevel = logLevel; + } + + private boolean hasLevel(LogLevel level) { + return mLogLevel.ordinal() <= level.ordinal(); + } + + private void log(LogLevel level, String s) { + if (hasLevel(level)) { + System.out.println(System.currentTimeMillis() + " [PROMISE." + level + "] " + + s); + } + } + + @Override + public void verbose(String s) { + log(LogLevel.VERBOSE, s); + } + + @Override + public void debug(String s) { + log(LogLevel.DEBUG, s); + } + + @Override + public void info(String s) { + log(LogLevel.INFO, s); + } + + @Override + public void warn(String s) { + log(LogLevel.WARN, s); + } + + @Override + public void error(String s) { + log(LogLevel.ERROR, s); + } + + } } From 6f4ae9d3372a47059ba1ac93d0b7f0a6a8058a50 Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Thu, 10 Mar 2016 12:17:15 +0100 Subject: [PATCH 08/11] JQ.wrap() will throw original ExecutionException if Future throws Throwable --- jq/src/main/java/se/code77/jq/JQ.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jq/src/main/java/se/code77/jq/JQ.java b/jq/src/main/java/se/code77/jq/JQ.java index 04d38ae..e5c9ed2 100644 --- a/jq/src/main/java/se/code77/jq/JQ.java +++ b/jq/src/main/java/se/code77/jq/JQ.java @@ -298,10 +298,9 @@ public V call() throws Exception { throw (Exception) cause; } else { // Promises can only rejected with - // Exceptions, not - // any Throwable. Simply wrap it. - throw new Exception("Future threw a Throwable", - cause); + // Exceptions, not any Throwable. + // Simply throw the ExecutionException. + throw e; } } From e43118add94d84e7f6a58e7091d75cfe99d19660 Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Thu, 10 Mar 2016 12:17:50 +0100 Subject: [PATCH 09/11] Implement StateSnapshot.equals() and hashCode() --- jq/src/main/java/se/code77/jq/Promise.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/jq/src/main/java/se/code77/jq/Promise.java b/jq/src/main/java/se/code77/jq/Promise.java index 356269c..beeaf28 100644 --- a/jq/src/main/java/se/code77/jq/Promise.java +++ b/jq/src/main/java/se/code77/jq/Promise.java @@ -124,6 +124,24 @@ public static final class StateSnapshot { this.reason = reason; } + @Override + public int hashCode() { + return state.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof StateSnapshot) { + StateSnapshot that = (StateSnapshot)o; + + return state.equals(that.state) && + (value == null ? that.value == null : value.equals(that.value)) && + (reason == null ? that.reason == null : reason.equals(that.reason)); + } + + return false; + } + @Override public String toString() { switch (state) { From b0ce47bbf3989123a52a591e4a2a608f370bda59 Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Thu, 10 Mar 2016 12:18:24 +0100 Subject: [PATCH 10/11] Add verbose log level, improve logs --- .../main/java/se/code77/jq/PromiseImpl.java | 22 ++++++++++++++++--- .../main/java/se/code77/jq/config/Config.java | 7 ++++++ .../jq/config/android/AndroidConfig.java | 7 ++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/jq/src/main/java/se/code77/jq/PromiseImpl.java b/jq/src/main/java/se/code77/jq/PromiseImpl.java index 3bb0819..71b8123 100644 --- a/jq/src/main/java/se/code77/jq/PromiseImpl.java +++ b/jq/src/main/java/se/code77/jq/PromiseImpl.java @@ -246,7 +246,7 @@ synchronized void _resolve(V value) { ensurePending(); mState = new StateSnapshot<>(State.FULFILLED, value, null); - debug("fulfilled with value '" + value + "'"); + info("fulfilled with value '" + value + "'"); notify(); handleCompletion(); } @@ -255,7 +255,7 @@ synchronized void _reject(Exception reason) { ensurePending(); mState = new StateSnapshot<>(State.REJECTED, null, reason); - debug("rejected with reason '" + reason + "'"); + info("rejected with reason '" + reason + "'"); notify(); handleCompletion(); } @@ -316,23 +316,30 @@ public void run() { try { Future nextValue; + debug("[" + mState + "]: Handling link " + link); + if (isRejected()) { if (link.onRejectedCallback != null) { nextValue = link.onRejectedCallback.onRejected(mState.reason); + verbose("Link.onRejected returned " + nextValue); } else { + verbose("Link has no onRejected, forward to next promise"); link.nextPromise._reject(mState.reason); return; } } else { if (link.onFulfilledCallback != null) { nextValue = link.onFulfilledCallback.onFulfilled(mState.value); + verbose("Link.onFulfilled returned " + nextValue); } else { + verbose("Link has no onFulfilled, forward to next promise"); link.nextPromise._resolve(mState.value); return; } } if (nextValue != null) { + verbose("Link returned future, next promise will inherit"); JQ.wrap(nextValue).then(new OnFulfilledCallback() { @Override public Future onFulfilled(Object value) throws Exception { @@ -347,6 +354,7 @@ public Future onRejected(Exception reason) throws Exception { } }).done(); } else { + verbose("Link returned null, next promise will resolve directly"); link.nextPromise._resolve(null); } } catch (UnhandledRejectionException e) { @@ -354,7 +362,7 @@ public Future onRejected(Exception reason) throws Exception { } catch (Exception reason) { StringWriter sw = new StringWriter(); reason.printStackTrace(new PrintWriter(sw)); - debug("Promise rejected from callback: " + sw.toString()); + info("Promise rejected from callback: " + sw.toString()); link.nextPromise._reject(reason); } @@ -374,10 +382,18 @@ private String getLogPrefix() { return "Promise@" + Integer.toHexString(hashCode()) + ": "; } + private void verbose(String s) { + getLogger().verbose(getLogPrefix() + s); + } + private void debug(String s) { getLogger().debug(getLogPrefix() + s); } + private void info(String s) { + getLogger().info(getLogPrefix() + s); + } + private void warn(String s) { getLogger().warn(getLogPrefix() + s); } diff --git a/jq/src/main/java/se/code77/jq/config/Config.java b/jq/src/main/java/se/code77/jq/config/Config.java index 0613ab4..5aad395 100644 --- a/jq/src/main/java/se/code77/jq/config/Config.java +++ b/jq/src/main/java/se/code77/jq/config/Config.java @@ -38,6 +38,12 @@ public interface Dispatcher { * A logger creates log prints. */ public interface Logger { + /** + * Print a debug log + * @param s log text + */ + void verbose(String s); + /** * Print a debug log * @param s log text @@ -64,6 +70,7 @@ public interface Logger { } public enum LogLevel { + VERBOSE, DEBUG, INFO, WARN, diff --git a/jq/src/main/java/se/code77/jq/config/android/AndroidConfig.java b/jq/src/main/java/se/code77/jq/config/android/AndroidConfig.java index 4fb8ee0..69af66a 100644 --- a/jq/src/main/java/se/code77/jq/config/android/AndroidConfig.java +++ b/jq/src/main/java/se/code77/jq/config/android/AndroidConfig.java @@ -52,6 +52,13 @@ private boolean hasLevel(LogLevel level) { return mLogLevel.ordinal() <= level.ordinal(); } + @Override + public void verbose(String s) { + if (hasLevel(LogLevel.VERBOSE)) { + Log.v(LOG_TAG, s); + } + } + @Override public void debug(String s) { if (hasLevel(LogLevel.DEBUG)) { From 93743cb906201cddc7e11dc770c7819d3d6a75b5 Mon Sep 17 00:00:00 2001 From: Henrik Hall Date: Thu, 10 Mar 2016 16:30:17 +0100 Subject: [PATCH 11/11] Add ValueTests --- jq/src/test/java/se/code77/jq/ValueTests.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 jq/src/test/java/se/code77/jq/ValueTests.java diff --git a/jq/src/test/java/se/code77/jq/ValueTests.java b/jq/src/test/java/se/code77/jq/ValueTests.java new file mode 100644 index 0000000..d315a4f --- /dev/null +++ b/jq/src/test/java/se/code77/jq/ValueTests.java @@ -0,0 +1,76 @@ +package se.code77.jq; + +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import se.code77.jq.JQ.Deferred; +import se.code77.jq.Promise.OnFulfilledCallback; +import se.code77.jq.Promise.UnhandledRejectionException; +import se.code77.jq.util.AsyncTests; +import se.code77.jq.util.BlockingDataHolder; +import se.code77.jq.util.DataFulfilledCallback; +import se.code77.jq.util.DataRejectedCallback; +import se.code77.jq.util.SlowTask; +import se.code77.jq.util.TestConfig; + +import static se.code77.jq.util.Assert.assertData; +import static se.code77.jq.util.Assert.assertEquals; +import static se.code77.jq.util.Assert.assertFalse; +import static se.code77.jq.util.Assert.assertNoData; +import static se.code77.jq.util.Assert.assertNotNull; +import static se.code77.jq.util.Assert.assertPending; +import static se.code77.jq.util.Assert.assertRejected; +import static se.code77.jq.util.Assert.assertResolved; +import static se.code77.jq.util.Assert.assertSame; +import static se.code77.jq.util.Assert.assertThrows; +import static se.code77.jq.util.Assert.assertTrue; + +public class ValueTests extends AsyncTests { + + @Test + public void value_get() { + Value value = Value.wrap(TEST_VALUE1); + + assertEquals(TEST_VALUE1, value.get()); + } + + @Test + public void value_getWithTimeout() { + Value value = Value.wrap(TEST_VALUE1); + + assertEquals(TEST_VALUE1, value.get(1, TimeUnit.NANOSECONDS)); + } + + @Test + public void value_cancel() { + Value value = Value.wrap(TEST_VALUE1); + + assertEquals(false, value.cancel(true)); + assertEquals(false, value.cancel(false)); + } + + @Test + public void value_isCancelled() { + Value value = Value.wrap(TEST_VALUE1); + + assertEquals(false, value.isCancelled()); + assertEquals(false, value.cancel(true)); + assertEquals(false, value.isCancelled()); + } + + @Test + public void value_isDone() { + Value value = Value.wrap(TEST_VALUE1); + + assertEquals(true, value.isDone()); + assertEquals(false, value.cancel(true)); + assertEquals(true, value.isDone()); + } +}