org.reactivestreams.tck.TestEnvironment Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of reactive-streams-tck Show documentation
Show all versions of reactive-streams-tck Show documentation
A Protocol for Asynchronous Non-Blocking Data Sequence
The newest version!
/***************************************************
* Licensed under MIT No Attribution (SPDX: MIT-0) *
***************************************************/
package org.reactivestreams.tck;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.reactivestreams.tck.flow.support.Optional;
import org.reactivestreams.tck.flow.support.SubscriberBufferOverflowException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
public class TestEnvironment {
public static final int TEST_BUFFER_SIZE = 16;
private static final String DEFAULT_TIMEOUT_MILLIS_ENV = "DEFAULT_TIMEOUT_MILLIS";
private static final long DEFAULT_TIMEOUT_MILLIS = 100;
private static final String DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS_ENV = "DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS";
private static final String DEFAULT_POLL_TIMEOUT_MILLIS_ENV = "DEFAULT_POLL_TIMEOUT_MILLIS_ENV";
private final long defaultTimeoutMillis;
private final long defaultPollTimeoutMillis;
private final long defaultNoSignalsTimeoutMillis;
private final boolean printlnDebug;
private CopyOnWriteArrayList asyncErrors = new CopyOnWriteArrayList();
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
* @param defaultTimeoutMillis default timeout to be used in all expect* methods
* @param defaultNoSignalsTimeoutMillis default timeout to be used when no further signals are expected anymore
* @param defaultPollTimeoutMillis default amount of time to poll for events if {@code defaultTimeoutMillis} isn't
* preempted by an asynchronous event.
* @param printlnDebug if true, signals such as OnNext / Request / OnComplete etc will be printed to standard output,
*/
public TestEnvironment(long defaultTimeoutMillis, long defaultNoSignalsTimeoutMillis, long defaultPollTimeoutMillis,
boolean printlnDebug) {
this.defaultTimeoutMillis = defaultTimeoutMillis;
this.defaultPollTimeoutMillis = defaultPollTimeoutMillis;
this.defaultNoSignalsTimeoutMillis = defaultNoSignalsTimeoutMillis;
this.printlnDebug = printlnDebug;
}
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
* @param defaultTimeoutMillis default timeout to be used in all expect* methods
* @param defaultNoSignalsTimeoutMillis default timeout to be used when no further signals are expected anymore
* @param printlnDebug if true, signals such as OnNext / Request / OnComplete etc will be printed to standard output,
*/
public TestEnvironment(long defaultTimeoutMillis, long defaultNoSignalsTimeoutMillis, boolean printlnDebug) {
this(defaultTimeoutMillis, defaultNoSignalsTimeoutMillis, defaultTimeoutMillis, printlnDebug);
}
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
*
* @param defaultTimeoutMillis default timeout to be used in all expect* methods
* @param defaultNoSignalsTimeoutMillis default timeout to be used when no further signals are expected anymore
* @param defaultPollTimeoutMillis default amount of time to poll for events if {@code defaultTimeoutMillis} isn't
* preempted by an asynchronous event.
*/
public TestEnvironment(long defaultTimeoutMillis, long defaultNoSignalsTimeoutMillis, long defaultPollTimeoutMillis) {
this(defaultTimeoutMillis, defaultNoSignalsTimeoutMillis, defaultPollTimeoutMillis, false);
}
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
*
* @param defaultTimeoutMillis default timeout to be used in all expect* methods
* @param defaultNoSignalsTimeoutMillis default timeout to be used when no further signals are expected anymore
*/
public TestEnvironment(long defaultTimeoutMillis, long defaultNoSignalsTimeoutMillis) {
this(defaultTimeoutMillis, defaultNoSignalsTimeoutMillis, defaultTimeoutMillis);
}
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
*
* @param defaultTimeoutMillis default timeout to be used in all expect* methods
*/
public TestEnvironment(long defaultTimeoutMillis) {
this(defaultTimeoutMillis, defaultTimeoutMillis, defaultTimeoutMillis);
}
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
*
* The default timeout for all expect* methods will be obtained by either the env variable {@code DEFAULT_TIMEOUT_MILLIS}
* or the default value ({@link TestEnvironment#DEFAULT_TIMEOUT_MILLIS}) will be used.
*
* @param printlnDebug if true, signals such as OnNext / Request / OnComplete etc will be printed to standard output,
* often helpful to pinpoint simple race conditions etc.
*/
public TestEnvironment(boolean printlnDebug) {
this(envDefaultTimeoutMillis(), envDefaultNoSignalsTimeoutMillis(), envDefaultPollTimeoutMillis(), printlnDebug);
}
/**
* Tests must specify the timeout for expected outcome of asynchronous
* interactions. Longer timeout does not invalidate the correctness of
* the implementation, but can in some cases result in longer time to
* run the tests.
*
* The default timeout for all expect* methods will be obtained by either the env variable {@code DEFAULT_TIMEOUT_MILLIS}
* or the default value ({@link TestEnvironment#DEFAULT_TIMEOUT_MILLIS}) will be used.
*/
public TestEnvironment() {
this(envDefaultTimeoutMillis(), envDefaultNoSignalsTimeoutMillis());
}
/** This timeout is used when waiting for a signal to arrive. */
public long defaultTimeoutMillis() {
return defaultTimeoutMillis;
}
/**
* This timeout is used when asserting that no further signals are emitted.
* Note that this timeout default
*/
public long defaultNoSignalsTimeoutMillis() {
return defaultNoSignalsTimeoutMillis;
}
/**
* The default amount of time to poll for events if {@code defaultTimeoutMillis} isn't preempted by an asynchronous
* event.
*/
public long defaultPollTimeoutMillis() {
return defaultPollTimeoutMillis;
}
/**
* Tries to parse the env variable {@code DEFAULT_TIMEOUT_MILLIS} as long and returns the value if present OR its default value.
*
* @throws java.lang.IllegalArgumentException when unable to parse the env variable
*/
public static long envDefaultTimeoutMillis() {
final String envMillis = System.getenv(DEFAULT_TIMEOUT_MILLIS_ENV);
if (envMillis == null) return DEFAULT_TIMEOUT_MILLIS;
else try {
return Long.parseLong(envMillis);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(String.format("Unable to parse %s env value [%s] as long!", DEFAULT_TIMEOUT_MILLIS_ENV, envMillis), ex);
}
}
/**
* Tries to parse the env variable {@code DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS} as long and returns the value if present OR its default value.
*
* @throws java.lang.IllegalArgumentException when unable to parse the env variable
*/
public static long envDefaultNoSignalsTimeoutMillis() {
final String envMillis = System.getenv(DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS_ENV);
if (envMillis == null) return envDefaultTimeoutMillis();
else try {
return Long.parseLong(envMillis);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(String.format("Unable to parse %s env value [%s] as long!", DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS_ENV, envMillis), ex);
}
}
/**
* Tries to parse the env variable {@code DEFAULT_POLL_TIMEOUT_MILLIS_ENV} as long and returns the value if present OR its default value.
*
* @throws java.lang.IllegalArgumentException when unable to parse the env variable
*/
public static long envDefaultPollTimeoutMillis() {
final String envMillis = System.getenv(DEFAULT_POLL_TIMEOUT_MILLIS_ENV);
if (envMillis == null) return envDefaultTimeoutMillis();
else try {
return Long.parseLong(envMillis);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(String.format("Unable to parse %s env value [%s] as long!", DEFAULT_POLL_TIMEOUT_MILLIS_ENV, envMillis), ex);
}
}
/**
* To flop means to "fail asynchronously", either by onErroring or by failing some TCK check triggered asynchronously.
* This method does *NOT* fail the test - it's up to inspections of the error to fail the test if required.
*
* Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution.
* To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly
* from the environment using {@code env.dropAsyncError()}.
*
* To clear asyncErrors you can call {@link org.reactivestreams.tck.TestEnvironment#clearAsyncErrors()}
*/
public void flop(String msg) {
try {
fail(msg);
} catch (Throwable t) {
asyncErrors.add(t);
}
}
/**
* To flop means to "fail asynchronously", either by onErroring or by failing some TCK check triggered asynchronously.
* This method does *NOT* fail the test - it's up to inspections of the error to fail the test if required.
*
* This overload keeps the passed in throwable as the asyncError, instead of creating an AssertionError for this.
*
* Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution.
* To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly
* from the environment using {@code env.dropAsyncError()}.
*
* To clear asyncErrors you can call {@link org.reactivestreams.tck.TestEnvironment#clearAsyncErrors()}
*/
public void flop(Throwable thr, String msg) {
try {
fail(msg, thr);
} catch (Throwable t) {
asyncErrors.add(thr);
}
}
/**
* To flop means to "fail asynchronously", either by onErroring or by failing some TCK check triggered asynchronously.
* This method does *NOT* fail the test - it's up to inspections of the error to fail the test if required.
*
* This overload keeps the passed in throwable as the asyncError, instead of creating an AssertionError for this.
*
* Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution.
* To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly
* from the environment using {@code env.dropAsyncError()}.
*
* To clear asyncErrors you can call {@link org.reactivestreams.tck.TestEnvironment#clearAsyncErrors()}
*/
public void flop(Throwable thr) {
try {
fail(thr.getMessage(), thr);
} catch (Throwable t) {
asyncErrors.add(thr);
}
}
/**
* To flop means to "fail asynchronously", either by onErroring or by failing some TCK check triggered asynchronously.
*
* This method DOES fail the test right away (it tries to, by throwing an AssertionException),
* in such it is different from {@link org.reactivestreams.tck.TestEnvironment#flop} which only records the error.
*
* Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution.
* To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly
* from the environment using {@code env.dropAsyncError()}.
*
* To clear asyncErrors you can call {@link org.reactivestreams.tck.TestEnvironment#clearAsyncErrors()}
*/
public T flopAndFail(String msg) {
try {
fail(msg);
} catch (Throwable t) {
asyncErrors.add(t);
fail(msg, t);
}
return null; // unreachable, the previous block will always exit by throwing
}
public void subscribe(Publisher pub, TestSubscriber sub) throws InterruptedException {
subscribe(pub, sub, defaultTimeoutMillis);
}
public void subscribe(Publisher pub, TestSubscriber sub, long timeoutMillis) throws InterruptedException {
pub.subscribe(sub);
sub.subscription.expectCompletion(timeoutMillis, String.format("Could not subscribe %s to Publisher %s", sub, pub));
verifyNoAsyncErrorsNoDelay();
}
public ManualSubscriber newBlackholeSubscriber(Publisher pub) throws InterruptedException {
ManualSubscriberWithSubscriptionSupport sub = new BlackholeSubscriberWithSubscriptionSupport(this);
subscribe(pub, sub, defaultTimeoutMillis());
return sub;
}
public ManualSubscriber newManualSubscriber(Publisher pub) throws InterruptedException {
return newManualSubscriber(pub, defaultTimeoutMillis());
}
public ManualSubscriber newManualSubscriber(Publisher pub, long timeoutMillis) throws InterruptedException {
ManualSubscriberWithSubscriptionSupport sub = new ManualSubscriberWithSubscriptionSupport(this);
subscribe(pub, sub, timeoutMillis);
return sub;
}
public void clearAsyncErrors() {
asyncErrors.clear();
}
public Throwable dropAsyncError() {
try {
return asyncErrors.remove(0);
} catch (IndexOutOfBoundsException ex) {
return null;
}
}
/**
* Waits for {@link TestEnvironment#defaultNoSignalsTimeoutMillis()} and then verifies that no asynchronous errors
* were signalled pior to, or during that time (by calling {@code flop()}).
*/
public void verifyNoAsyncErrors() {
verifyNoAsyncErrors(defaultNoSignalsTimeoutMillis());
}
/**
* This version of {@code verifyNoAsyncErrors} should be used when errors still could be signalled
* asynchronously during {@link TestEnvironment#defaultTimeoutMillis()} time.
*
* It will immediatly check if any async errors were signaled (using {@link TestEnvironment#flop(String)},
* and if no errors encountered wait for another default timeout as the errors may yet be signalled.
* The initial check is performed in order to fail-fast in case of an already failed test.
*/
public void verifyNoAsyncErrors(long delay) {
try {
verifyNoAsyncErrorsNoDelay();
Thread.sleep(delay);
verifyNoAsyncErrorsNoDelay();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* Verifies that no asynchronous errors were signalled pior to calling this method (by calling {@code flop()}).
* This version of verifyNoAsyncError does not wait before checking for asynchronous errors, and is to be used
* for example in tight loops etc.
*/
public void verifyNoAsyncErrorsNoDelay() {
for (Throwable e : asyncErrors) {
if (e instanceof AssertionError) {
throw (AssertionError) e;
} else {
fail(String.format("Async error during test execution: %s", e.getMessage()), e);
}
}
}
/** If {@code TestEnvironment#printlnDebug} is true, print debug message to std out. */
public void debug(String msg) {
if (debugEnabled()) {
System.out.printf("[TCK-DEBUG] %s%n", msg);
}
}
public final boolean debugEnabled() {
return printlnDebug;
}
/**
* Looks for given {@code method} method in stack trace.
* Can be used to answer questions like "was this method called from onComplete?".
*
* @return the caller's StackTraceElement at which he the looked for method was found in the call stack, EMPTY otherwise
*/
public Optional findCallerMethodInStackTrace(String method) {
final Throwable thr = new Throwable(); // gets the stacktrace
for (StackTraceElement stackElement : thr.getStackTrace()) {
if (stackElement.getMethodName().equals(method)) {
return Optional.of(stackElement);
}
}
return Optional.empty();
}
// ---- classes ----
/**
* {@link Subscriber} implementation which can be steered by test code and asserted on.
*/
public static class ManualSubscriber extends TestSubscriber {
Receptacle received;
public ManualSubscriber(TestEnvironment env) {
super(env);
received = new Receptacle(this.env);
}
@Override
public void onNext(T element) {
try {
received.add(element);
} catch (IllegalStateException ex) {
// error message refinement
throw new SubscriberBufferOverflowException(
String.format("Received more than bufferSize (%d) onNext signals. " +
"The Publisher probably emited more signals than expected!",
received.QUEUE_SIZE), ex);
}
}
@Override
public void onComplete() {
received.complete();
}
public void request(long elements) {
subscription.value().request(elements);
}
public T requestNextElement() throws InterruptedException {
return requestNextElement(env.defaultTimeoutMillis());
}
public T requestNextElement(long timeoutMillis) throws InterruptedException {
return requestNextElement(timeoutMillis, "Did not receive expected element");
}
public T requestNextElement(String errorMsg) throws InterruptedException {
return requestNextElement(env.defaultTimeoutMillis(), errorMsg);
}
public T requestNextElement(long timeoutMillis, String errorMsg) throws InterruptedException {
request(1);
return nextElement(timeoutMillis, errorMsg);
}
public Optional requestNextElementOrEndOfStream() throws InterruptedException {
return requestNextElementOrEndOfStream(env.defaultTimeoutMillis(), "Did not receive expected stream completion");
}
public Optional requestNextElementOrEndOfStream(String errorMsg) throws InterruptedException {
return requestNextElementOrEndOfStream(env.defaultTimeoutMillis(), errorMsg);
}
public Optional requestNextElementOrEndOfStream(long timeoutMillis) throws InterruptedException {
return requestNextElementOrEndOfStream(timeoutMillis, "Did not receive expected stream completion");
}
public Optional requestNextElementOrEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
request(1);
return nextElementOrEndOfStream(timeoutMillis, errorMsg);
}
public void requestEndOfStream() throws InterruptedException {
requestEndOfStream(env.defaultTimeoutMillis(), "Did not receive expected stream completion");
}
public void requestEndOfStream(long timeoutMillis) throws InterruptedException {
requestEndOfStream(timeoutMillis, "Did not receive expected stream completion");
}
public void requestEndOfStream(String errorMsg) throws InterruptedException {
requestEndOfStream(env.defaultTimeoutMillis(), errorMsg);
}
public void requestEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
request(1);
expectCompletion(timeoutMillis, errorMsg);
}
public List requestNextElements(long elements) throws InterruptedException {
request(elements);
return nextElements(elements, env.defaultTimeoutMillis());
}
public List requestNextElements(long elements, long timeoutMillis) throws InterruptedException {
request(elements);
return nextElements(elements, timeoutMillis, String.format("Did not receive %d expected elements", elements));
}
public List requestNextElements(long elements, long timeoutMillis, String errorMsg) throws InterruptedException {
request(elements);
return nextElements(elements, timeoutMillis, errorMsg);
}
public T nextElement() throws InterruptedException {
return nextElement(env.defaultTimeoutMillis());
}
public T nextElement(long timeoutMillis) throws InterruptedException {
return nextElement(timeoutMillis, "Did not receive expected element");
}
public T nextElement(String errorMsg) throws InterruptedException {
return nextElement(env.defaultTimeoutMillis(), errorMsg);
}
public T nextElement(long timeoutMillis, String errorMsg) throws InterruptedException {
return received.next(timeoutMillis, errorMsg);
}
public Optional nextElementOrEndOfStream() throws InterruptedException {
return nextElementOrEndOfStream(env.defaultTimeoutMillis(), "Did not receive expected stream completion");
}
public Optional nextElementOrEndOfStream(long timeoutMillis) throws InterruptedException {
return nextElementOrEndOfStream(timeoutMillis, "Did not receive expected stream completion");
}
public Optional nextElementOrEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
return received.nextOrEndOfStream(timeoutMillis, errorMsg);
}
public List nextElements(long elements) throws InterruptedException {
return nextElements(elements, env.defaultTimeoutMillis(), "Did not receive expected element or completion");
}
public List nextElements(long elements, String errorMsg) throws InterruptedException {
return nextElements(elements, env.defaultTimeoutMillis(), errorMsg);
}
public List nextElements(long elements, long timeoutMillis) throws InterruptedException {
return nextElements(elements, timeoutMillis, "Did not receive expected element or completion");
}
public List nextElements(long elements, long timeoutMillis, String errorMsg) throws InterruptedException {
return received.nextN(elements, timeoutMillis, errorMsg);
}
public void expectNext(T expected) throws InterruptedException {
expectNext(expected, env.defaultTimeoutMillis());
}
public void expectNext(T expected, long timeoutMillis) throws InterruptedException {
T received = nextElement(timeoutMillis, "Did not receive expected element on downstream");
if (!received.equals(expected)) {
env.flop(String.format("Expected element %s on downstream but received %s", expected, received));
}
}
public void expectCompletion() throws InterruptedException {
expectCompletion(env.defaultTimeoutMillis(), "Did not receive expected stream completion");
}
public void expectCompletion(long timeoutMillis) throws InterruptedException {
expectCompletion(timeoutMillis, "Did not receive expected stream completion");
}
public void expectCompletion(String errorMsg) throws InterruptedException {
expectCompletion(env.defaultTimeoutMillis(), errorMsg);
}
public void expectCompletion(long timeoutMillis, String errorMsg) throws InterruptedException {
received.expectCompletion(timeoutMillis, errorMsg);
}
public void expectErrorWithMessage(Class expected, String requiredMessagePart) throws Exception {
expectErrorWithMessage(expected, Collections.singletonList(requiredMessagePart), env.defaultTimeoutMillis(), env.defaultPollTimeoutMillis());
}
public void expectErrorWithMessage(Class expected, List requiredMessagePartAlternatives) throws Exception {
expectErrorWithMessage(expected, requiredMessagePartAlternatives, env.defaultTimeoutMillis(), env.defaultPollTimeoutMillis());
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public void expectErrorWithMessage(Class expected, String requiredMessagePart, long timeoutMillis) throws Exception {
expectErrorWithMessage(expected, Collections.singletonList(requiredMessagePart), timeoutMillis);
}
public void expectErrorWithMessage(Class expected, List requiredMessagePartAlternatives, long timeoutMillis) throws Exception {
expectErrorWithMessage(expected, requiredMessagePartAlternatives, timeoutMillis, timeoutMillis);
}
public void expectErrorWithMessage(Class expected, List requiredMessagePartAlternatives,
long totalTimeoutMillis, long pollTimeoutMillis) throws Exception {
final E err = expectError(expected, totalTimeoutMillis, pollTimeoutMillis);
final String message = err.getMessage();
boolean contains = false;
for (String requiredMessagePart : requiredMessagePartAlternatives)
if (message.contains(requiredMessagePart)) contains = true; // not short-circuting loop, it is expected to
assertTrue(contains,
String.format("Got expected exception [%s] but missing message part [%s], was: %s",
err.getClass(), "anyOf: " + requiredMessagePartAlternatives, err.getMessage()));
}
public E expectError(Class expected) throws Exception {
return expectError(expected, env.defaultTimeoutMillis());
}
public E expectError(Class expected, long timeoutMillis) throws Exception {
return expectError(expected, timeoutMillis, env.defaultPollTimeoutMillis());
}
public E expectError(Class expected, String errorMsg) throws Exception {
return expectError(expected, env.defaultTimeoutMillis(), errorMsg);
}
public E expectError(Class expected, long timeoutMillis, String errorMsg) throws Exception {
return expectError(expected, timeoutMillis, env.defaultPollTimeoutMillis(), errorMsg);
}
public E expectError(Class expected, long totalTimeoutMillis, long pollTimeoutMillis) throws Exception {
return expectError(expected, totalTimeoutMillis, pollTimeoutMillis, String.format("Expected onError(%s)", expected.getName()));
}
public E expectError(Class expected, long totalTimeoutMillis, long pollTimeoutMillis,
String errorMsg) throws Exception {
return received.expectError(expected, totalTimeoutMillis, pollTimeoutMillis, errorMsg);
}
public void expectNone() throws InterruptedException {
expectNone(env.defaultNoSignalsTimeoutMillis());
}
public void expectNone(String errMsgPrefix) throws InterruptedException {
expectNone(env.defaultNoSignalsTimeoutMillis(), errMsgPrefix);
}
public void expectNone(long withinMillis) throws InterruptedException {
expectNone(withinMillis, "Did not expect an element but got element");
}
public void expectNone(long withinMillis, String errMsgPrefix) throws InterruptedException {
received.expectNone(withinMillis, errMsgPrefix);
}
}
public static class ManualSubscriberWithSubscriptionSupport extends ManualSubscriber {
public ManualSubscriberWithSubscriptionSupport(TestEnvironment env) {
super(env);
}
@Override
public void onNext(T element) {
if (env.debugEnabled()) {
env.debug(String.format("%s::onNext(%s)", this, element));
}
if (subscription.isCompleted()) {
super.onNext(element);
} else {
env.flop(String.format("Subscriber::onNext(%s) called before Subscriber::onSubscribe", element));
}
}
@Override
public void onComplete() {
if (env.debugEnabled()) {
env.debug(this + "::onComplete()");
}
if (subscription.isCompleted()) {
super.onComplete();
} else {
env.flop("Subscriber::onComplete() called before Subscriber::onSubscribe");
}
}
@Override
public void onSubscribe(Subscription s) {
if (env.debugEnabled()) {
env.debug(String.format("%s::onSubscribe(%s)", this, s));
}
if (!subscription.isCompleted()) {
subscription.complete(s);
} else {
env.flop("Subscriber::onSubscribe called on an already-subscribed Subscriber");
}
}
@Override
public void onError(Throwable cause) {
if (env.debugEnabled()) {
env.debug(String.format("%s::onError(%s)", this, cause));
}
if (subscription.isCompleted()) {
super.onError(cause);
} else {
env.flop(cause, String.format("Subscriber::onError(%s) called before Subscriber::onSubscribe", cause));
}
}
}
/**
* Similar to {@link org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport}
* but does not accumulate values signalled via onNext
, thus it can not be used to assert
* values signalled to this subscriber. Instead it may be used to quickly drain a given publisher.
*/
public static class BlackholeSubscriberWithSubscriptionSupport
extends ManualSubscriberWithSubscriptionSupport {
public BlackholeSubscriberWithSubscriptionSupport(TestEnvironment env) {
super(env);
}
@Override
public void onNext(T element) {
if (env.debugEnabled()) {
env.debug(String.format("%s::onNext(%s)", this, element));
}
if (!subscription.isCompleted()) {
env.flop(String.format("Subscriber::onNext(%s) called before Subscriber::onSubscribe", element));
}
}
@Override
public T nextElement(long timeoutMillis, String errorMsg) throws InterruptedException {
throw new RuntimeException("Can not expect elements from BlackholeSubscriber, use ManualSubscriber instead!");
}
@Override
public List nextElements(long elements, long timeoutMillis, String errorMsg) throws InterruptedException {
throw new RuntimeException("Can not expect elements from BlackholeSubscriber, use ManualSubscriber instead!");
}
}
public static class TestSubscriber implements Subscriber {
final Promise subscription;
protected final TestEnvironment env;
public TestSubscriber(TestEnvironment env) {
this.env = env;
subscription = new Promise(env);
}
@Override
public void onError(Throwable cause) {
env.flop(cause, String.format("Unexpected Subscriber::onError(%s)", cause));
}
@Override
public void onComplete() {
env.flop("Unexpected Subscriber::onComplete()");
}
@Override
public void onNext(T element) {
env.flop(String.format("Unexpected Subscriber::onNext(%s)", element));
}
@Override
public void onSubscribe(Subscription subscription) {
env.flop(String.format("Unexpected Subscriber::onSubscribe(%s)", subscription));
}
public void cancel() {
if (subscription.isCompleted()) {
subscription.value().cancel();
} else {
env.flop("Cannot cancel a subscription before having received it");
}
}
}
public static class ManualPublisher implements Publisher {
protected final TestEnvironment env;
protected long pendingDemand = 0L;
protected Promise> subscriber;
protected final Receptacle requests;
protected final Latch cancelled;
public ManualPublisher(TestEnvironment env) {
this.env = env;
requests = new Receptacle(env);
cancelled = new Latch(env);
subscriber = new Promise>(this.env);
}
@Override
public void subscribe(Subscriber super T> s) {
if (!subscriber.isCompleted()) {
subscriber.completeImmediatly(s);
Subscription subs = new Subscription() {
@Override
public void request(long elements) {
requests.add(elements);
}
@Override
public void cancel() {
cancelled.close();
}
};
s.onSubscribe(subs);
} else {
env.flop("TestPublisher doesn't support more than one Subscriber");
}
}
public void sendNext(T element) {
if (subscriber.isCompleted()) {
subscriber.value().onNext(element);
} else {
env.flop("Cannot sendNext before having a Subscriber");
}
}
public void sendCompletion() {
if (subscriber.isCompleted()) {
subscriber.value().onComplete();
} else {
env.flop("Cannot sendCompletion before having a Subscriber");
}
}
public void sendError(Throwable cause) {
if (subscriber.isCompleted()) {
subscriber.value().onError(cause);
} else {
env.flop("Cannot sendError before having a Subscriber");
}
}
public long expectRequest() throws InterruptedException {
return expectRequest(env.defaultTimeoutMillis());
}
public long expectRequest(long timeoutMillis) throws InterruptedException {
long requested = requests.next(timeoutMillis, "Did not receive expected `request` call");
if (requested <= 0) {
return env.flopAndFail(String.format("Requests cannot be zero or negative but received request(%s)", requested));
} else {
pendingDemand += requested;
return requested;
}
}
public long expectRequest(long timeoutMillis, String errorMessageAddendum) throws InterruptedException {
long requested = requests.next(timeoutMillis, String.format("Did not receive expected `request` call. %s", errorMessageAddendum));
if (requested <= 0) {
return env.flopAndFail(String.format("Requests cannot be zero or negative but received request(%s)", requested));
} else {
pendingDemand += requested;
return requested;
}
}
public void expectExactRequest(long expected) throws InterruptedException {
expectExactRequest(expected, env.defaultTimeoutMillis());
}
public void expectExactRequest(long expected, long timeoutMillis) throws InterruptedException {
long requested = expectRequest(timeoutMillis);
if (requested != expected) {
env.flop(String.format("Received `request(%d)` on upstream but expected `request(%d)`", requested, expected));
}
pendingDemand += requested;
}
public void expectNoRequest() throws InterruptedException {
expectNoRequest(env.defaultTimeoutMillis());
}
public void expectNoRequest(long timeoutMillis) throws InterruptedException {
requests.expectNone(timeoutMillis, "Received an unexpected call to: request: ");
}
public void expectCancelling() throws InterruptedException {
expectCancelling(env.defaultTimeoutMillis());
}
public void expectCancelling(long timeoutMillis) throws InterruptedException {
cancelled.expectClose(timeoutMillis, "Did not receive expected cancelling of upstream subscription");
}
public boolean isCancelled() throws InterruptedException {
return cancelled.isClosed();
}
}
/**
* Like a CountDownLatch, but resettable and with some convenience methods
*/
public static class Latch {
private final TestEnvironment env;
volatile private CountDownLatch countDownLatch = new CountDownLatch(1);
public Latch(TestEnvironment env) {
this.env = env;
}
public void reOpen() {
countDownLatch = new CountDownLatch(1);
}
public boolean isClosed() {
return countDownLatch.getCount() == 0;
}
public void close() {
countDownLatch.countDown();
}
public void assertClosed(String openErrorMsg) {
if (!isClosed()) {
env.flop(new ExpectedClosedLatchException(openErrorMsg));
}
}
public void assertOpen(String closedErrorMsg) {
if (isClosed()) {
env.flop(new ExpectedOpenLatchException(closedErrorMsg));
}
}
public void expectClose(String notClosedErrorMsg) throws InterruptedException {
expectClose(env.defaultTimeoutMillis(), notClosedErrorMsg);
}
public void expectClose(long timeoutMillis, String notClosedErrorMsg) throws InterruptedException {
countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
if (countDownLatch.getCount() > 0) {
env.flop(String.format("%s within %d ms", notClosedErrorMsg, timeoutMillis));
}
}
static final class ExpectedOpenLatchException extends RuntimeException {
public ExpectedOpenLatchException(String message) {
super(message);
}
}
static final class ExpectedClosedLatchException extends RuntimeException {
public ExpectedClosedLatchException(String message) {
super(message);
}
}
}
// simple promise for *one* value, which cannot be reset
public static class Promise {
private final TestEnvironment env;
public static Promise completed(TestEnvironment env, T value) {
Promise promise = new Promise(env);
promise.completeImmediatly(value);
return promise;
}
public Promise(TestEnvironment env) {
this.env = env;
}
private ArrayBlockingQueue abq = new ArrayBlockingQueue(1);
private AtomicReference _value = new AtomicReference();
public T value() {
final T value = _value.get();
if (value != null) {
return value;
} else {
env.flop("Cannot access promise value before completion");
return null;
}
}
public boolean isCompleted() {
return _value.get() != null;
}
/**
* Allows using expectCompletion to await for completion of the value and complete it _then_
*/
public void complete(T value) {
if (_value.compareAndSet(null, value)) {
// we add the value to the queue such to wake up any expectCompletion which was triggered before complete() was called
abq.add(value);
} else {
env.flop(String.format("Cannot complete a promise more than once! Present value: %s, attempted to set: %s", _value.get(), value));
}
}
/**
* Same as complete.
*
* Keeping this method for binary compatibility.
*/
public void completeImmediatly(T value) {
complete(value);
}
public void expectCompletion(long timeoutMillis, String errorMsg) throws InterruptedException {
if (!isCompleted()) {
T val = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (val == null) {
env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
}
}
}
}
// a "Promise" for multiple values, which also supports "end-of-stream reached"
public static class Receptacle {
final int QUEUE_SIZE = 2 * TEST_BUFFER_SIZE;
private final TestEnvironment env;
private final ArrayBlockingQueue> abq = new ArrayBlockingQueue>(QUEUE_SIZE);
private final Latch completedLatch;
Receptacle(TestEnvironment env) {
this.env = env;
this.completedLatch = new Latch(env);
}
public void add(T value) {
completedLatch.assertOpen(String.format("Unexpected element %s received after stream completed", value));
abq.add(Optional.of(value));
}
public void complete() {
completedLatch.assertOpen("Unexpected additional complete signal received!");
completedLatch.close();
abq.add(Optional.empty());
}
public T next(long timeoutMillis, String errorMsg) throws InterruptedException {
Optional value = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (value == null) {
return env.flopAndFail(String.format("%s within %d ms", errorMsg, timeoutMillis));
} else if (value.isDefined()) {
return value.get();
} else {
return env.flopAndFail("Expected element but got end-of-stream");
}
}
public Optional nextOrEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
Optional value = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (value == null) {
env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
return Optional.empty();
}
return value;
}
/**
* @param timeoutMillis total timeout time for awaiting all {@code elements} number of elements
*/
public List nextN(long elements, long timeoutMillis, String errorMsg) throws InterruptedException {
List result = new LinkedList();
long remaining = elements;
long deadline = System.currentTimeMillis() + timeoutMillis;
while (remaining > 0) {
long remainingMillis = deadline - System.currentTimeMillis();
result.add(next(remainingMillis, errorMsg));
remaining--;
}
return result;
}
public void expectCompletion(long timeoutMillis, String errorMsg) throws InterruptedException {
Optional value = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (value == null) {
env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
} else if (value.isDefined()) {
env.flop(String.format("Expected end-of-stream but got element [%s]", value.get()));
} // else, ok
}
/**
* @deprecated Deprecated in favor of {@link #expectError(Class, long, long, String)}.
*/
@Deprecated
public E expectError(Class clazz, long timeoutMillis, String errorMsg) throws Exception {
return expectError(clazz, timeoutMillis, timeoutMillis, errorMsg);
}
@SuppressWarnings("unchecked")
final E expectError(Class clazz, final long totalTimeoutMillis,
long pollTimeoutMillis,
String errorMsg) throws Exception {
long totalTimeoutRemainingNs = MILLISECONDS.toNanos(totalTimeoutMillis);
long timeStampANs = System.nanoTime();
long timeStampBNs;
for (;;) {
Thread.sleep(Math.min(pollTimeoutMillis, NANOSECONDS.toMillis(totalTimeoutRemainingNs)));
if (env.asyncErrors.isEmpty()) {
timeStampBNs = System.nanoTime();
totalTimeoutRemainingNs =- timeStampBNs - timeStampANs;
timeStampANs = timeStampBNs;
if (totalTimeoutRemainingNs <= 0) {
return env.flopAndFail(String.format("%s within %d ms", errorMsg, totalTimeoutMillis));
}
} else {
// ok, there was an expected error
Throwable thrown = env.asyncErrors.remove(0);
if (clazz.isInstance(thrown)) {
return (E) thrown;
} else {
return env.flopAndFail(String.format("%s within %d ms; Got %s but expected %s",
errorMsg, totalTimeoutMillis, thrown.getClass().getCanonicalName(), clazz.getCanonicalName()));
}
}
}
}
public void expectNone(long withinMillis, String errorMsgPrefix) throws InterruptedException {
Thread.sleep(withinMillis);
Optional value = abq.poll();
if (value == null) {
// ok
} else if (value.isDefined()) {
env.flop(String.format("%s [%s]", errorMsgPrefix, value.get()));
} else {
env.flop("Expected no element but got end-of-stream");
}
}
}
}