/* * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactor.core.publisher; import java.time.Duration; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; import org.junit.Test; import org.reactivestreams.Subscription; import reactor.core.Scannable; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; public class LambdaMonoSubscriberTest { @Test public void consumeOnSubscriptionNotifiesError() { AtomicReference<Throwable> errorHolder = new AtomicReference<>(null); LambdaMonoSubscriber<String> tested = new LambdaMonoSubscriber<>( value -> {}, errorHolder::set, () -> {}, subscription -> { throw new IllegalArgumentException(); }); TestSubscription testSubscription = new TestSubscription(); //the error is expected to be propagated through onError tested.onSubscribe(testSubscription); assertThat("unexpected exception in onError", errorHolder.get(), is(instanceOf(IllegalArgumentException.class))); assertThat("subscription has not been cancelled", testSubscription.isCancelled, is(true)); assertThat("unexpected request", testSubscription.requested, is(equalTo(-1L))); } @Test public void consumeOnSubscriptionThrowsFatal() { AtomicReference<Throwable> errorHolder = new AtomicReference<>(null); LambdaMonoSubscriber<String> tested = new LambdaMonoSubscriber<>( value -> {}, errorHolder::set, () -> {}, subscription -> { throw new OutOfMemoryError(); }); TestSubscription testSubscription = new TestSubscription(); //the error is expected to be thrown as it is fatal try { tested.onSubscribe(testSubscription); fail("Expected OutOfMemoryError to be thrown"); } catch (OutOfMemoryError e) { //expected } assertThat("unexpected onError", errorHolder.get(), is(nullValue())); assertThat("subscription has been cancelled despite fatal exception", testSubscription.isCancelled, is(not(true))); assertThat("unexpected request", testSubscription.requested, is(equalTo(-1L))); } @Test public void consumeOnSubscriptionReceivesSubscriptionAndRequests32() { AtomicReference<Throwable> errorHolder = new AtomicReference<>(null); AtomicReference<Subscription> subscriptionHolder = new AtomicReference<>(null); LambdaMonoSubscriber<String> tested = new LambdaMonoSubscriber<>( value -> {}, errorHolder::set, () -> { }, s -> { subscriptionHolder.set(s); s.request(32); }); TestSubscription testSubscription = new TestSubscription(); tested.onSubscribe(testSubscription); assertThat("unexpected onError", errorHolder.get(), is(nullValue())); assertThat("subscription has been cancelled", testSubscription.isCancelled, is(not(true))); assertThat("didn't consume the subscription", subscriptionHolder.get(), is(equalTo(testSubscription))); assertThat("didn't request the subscription", testSubscription.requested, is(equalTo(32L))); } @Test public void noSubscriptionConsumerTriggersRequestOfMax() { AtomicReference<Throwable> errorHolder = new AtomicReference<>(null); LambdaMonoSubscriber<String> tested = new LambdaMonoSubscriber<>( value -> {}, errorHolder::set, () -> {}, null); //defaults to initial request of max TestSubscription testSubscription = new TestSubscription(); tested.onSubscribe(testSubscription); assertThat("unexpected onError", errorHolder.get(), is(nullValue())); assertThat("subscription has been cancelled", testSubscription.isCancelled, is(not(true))); assertThat("didn't request the subscription", testSubscription.requested, is(not(equalTo(-1L)))); assertThat("didn't request max", testSubscription.requested, is(equalTo(Long.MAX_VALUE))); } @Test public void onNextConsumerExceptionBubblesUpDoesntTriggerCancellation() { AtomicReference<Throwable> errorHolder = new AtomicReference<>(null); LambdaMonoSubscriber<String> tested = new LambdaMonoSubscriber<>( value -> { throw new IllegalArgumentException(); }, errorHolder::set, () -> {}, null); TestSubscription testSubscription = new TestSubscription(); tested.onSubscribe(testSubscription); //as Mono is single-value, it cancels early on onNext. this leads to an exception //during onNext to be bubbled up as a BubbledException, not propagated through onNext try { tested.onNext("foo"); fail("Expected a bubbling Exception"); } catch (RuntimeException e) { assertThat("Expected a bubbling Exception", e.getClass().getName(), containsString("BubblingException")); assertThat("Expected cause to be the IllegalArgumentException", e.getCause(), is(instanceOf(IllegalArgumentException.class))); } assertThat("unexpected exception in onError", errorHolder.get(), is(nullValue())); assertThat("subscription has been cancelled", testSubscription.isCancelled, is(false)); } @Test public void onNextConsumerFatalDoesntTriggerCancellation() { AtomicReference<Throwable> errorHolder = new AtomicReference<>(null); LambdaMonoSubscriber<String> tested = new LambdaMonoSubscriber<>( value -> { throw new OutOfMemoryError(); }, errorHolder::set, () -> {}, null); TestSubscription testSubscription = new TestSubscription(); tested.onSubscribe(testSubscription); //the error is expected to be thrown as it is fatal try { tested.onNext("foo"); fail("Expected OutOfMemoryError to be thrown"); } catch (OutOfMemoryError e) { //expected } assertThat("unexpected onError", errorHolder.get(), is(nullValue())); assertThat("subscription has been cancelled", testSubscription.isCancelled, is(false)); } @Test public void emptyMonoState(){ assertTrue(MonoSource.wrap(s -> { assertTrue(s instanceof LambdaMonoSubscriber); LambdaMonoSubscriber<?> bfs = (LambdaMonoSubscriber<?>)s; assertTrue(bfs.scan(Scannable.Attr.PREFETCH, Integer.class) == Integer.MAX_VALUE); assertFalse(bfs.scan(Scannable.Attr.TERMINATED, Boolean.class)); bfs.onSubscribe(Operators.emptySubscription()); bfs.onSubscribe(Operators.emptySubscription()); // noop s.onComplete(); assertTrue(bfs.scan(Scannable.Attr.TERMINATED, Boolean.class)); bfs.dispose(); bfs.dispose(); }).subscribe(s -> {}, null, () -> {}).isDisposed()); assertFalse(Mono.never().subscribe(null, null, () -> {}).isDisposed()); } @Test public void errorMonoState(){ Hooks.onErrorDropped(e -> assertTrue(e.getMessage().equals("test2"))); Hooks.onNextDropped(d -> assertTrue(d.equals("test2"))); try { MonoSource.wrap(s -> { assertTrue(s instanceof LambdaMonoSubscriber); LambdaMonoSubscriber<?> bfs = (LambdaMonoSubscriber<?>) s; Operators.error(s, new Exception("test")); s.onComplete(); s.onError(new Exception("test2")); s.onNext("test2"); assertTrue(bfs.scan(Scannable.Attr.TERMINATED, Boolean.class)); bfs.dispose(); }) .subscribe(s -> { }, e -> { }, () -> { }); } finally { Hooks.resetOnErrorDropped(); Hooks.resetOnNextDropped(); } } @Test public void completeHookErrorDropped() { Hooks.onErrorDropped(e -> assertTrue(e.getMessage().equals("complete"))); try { Mono.just("foo") .subscribe(v -> {}, e -> {}, () -> { throw new IllegalStateException("complete");}); } finally { Hooks.resetOnErrorDropped(); } } @Test public void noErrorHookThrowsCallbackNotImplemented() { RuntimeException boom = new IllegalArgumentException("boom"); Assertions.assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> Mono.error(boom).subscribe(v -> {})) .withCause(boom) .hasToString("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: boom"); } @Test public void testCancel() { AtomicLong cancelCount = new AtomicLong(); Mono.delay(Duration.ofMillis(500)) .doOnCancel(cancelCount::incrementAndGet) .subscribe(v -> {}) .dispose(); Assertions.assertThat(cancelCount.get()).isEqualTo(1); } private static class TestSubscription implements Subscription { volatile boolean isCancelled = false; volatile long requested = -1L; @Override public void request(long n) { this.requested = n; } @Override public void cancel() { this.isCancelled = true; } } }