/* * 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 * * https://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.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; import javax.annotation.Nullable; import org.assertj.core.api.Assertions; import org.junit.Test; import org.mockito.Mockito; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.core.publisher.Operators.CancelledSubscription; import reactor.core.publisher.Operators.DeferredSubscription; import reactor.core.publisher.Operators.EmptySubscription; import reactor.core.publisher.Operators.MonoSubscriber; import reactor.core.publisher.Operators.MultiSubscriptionSubscriber; import reactor.core.publisher.Operators.ScalarSubscription; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; import reactor.test.util.RaceTestUtils; import reactor.util.context.Context; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; public class OperatorsTest { volatile long testRequest; static final AtomicLongFieldUpdater<OperatorsTest> TEST_REQUEST = AtomicLongFieldUpdater.newUpdater(OperatorsTest.class, "testRequest"); @Test public void addAndGetAtomicField() { TEST_REQUEST.set(this, 0L); RaceTestUtils.race(0L, s -> Operators.addCap(TEST_REQUEST, this, 1), a -> a >= 100_000L, (a, b) -> a == b ); TEST_REQUEST.set(this, 0L); assertThat(Operators.addCap(TEST_REQUEST, this, -1_000_000L)) .isEqualTo(0); TEST_REQUEST.set(this, 1L); assertThat(Operators.addCap(TEST_REQUEST, this, Long.MAX_VALUE)) .isEqualTo(1L); assertThat(Operators.addCap(TEST_REQUEST, this, 0)) .isEqualTo(Long.MAX_VALUE); } @Test public void addCap() { assertThat(Operators.addCap(1, 2)).isEqualTo(3); assertThat(Operators.addCap(1, Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); assertThat(Operators.addCap(0, -1)).isEqualTo(Long.MAX_VALUE); } @Test public void constructor(){ assertThat(new Operators(){}).isNotNull(); } @Test public void castAsQueueSubscription() { Fuseable.QueueSubscription<String> qs = new Fuseable.SynchronousSubscription<String>() { @Override public String poll() { return null; } @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public void clear() { } @Override public void request(long n) { } @Override public void cancel() { } }; Subscription s = Operators.cancelledSubscription(); assertThat(Operators.as(qs)).isEqualTo(qs); assertThat(Operators.as(s)).isNull(); } @Test public void cancelledSubscription(){ Operators.CancelledSubscription es = (Operators.CancelledSubscription)Operators.cancelledSubscription(); assertThat((Object)es).isEqualTo(Operators.CancelledSubscription.INSTANCE); //Noop es.cancel(); es.request(-1); } @Test public void noopFluxCancelled(){ OperatorDisposables.DISPOSED.dispose(); //noop } @Test public void drainSubscriber() { AtomicBoolean requested = new AtomicBoolean(); AtomicBoolean errored = new AtomicBoolean(); Hooks.onErrorDropped(e -> { assertThat(Exceptions.isErrorCallbackNotImplemented(e)).isTrue(); assertThat(e.getCause()).hasMessage("test"); errored.set(true); }); Flux.from(s -> { assertThat(s).isEqualTo(Operators.drainSubscriber()); s.onSubscribe(new Subscription() { @Override public void request(long n) { assertThat(n).isEqualTo(Long.MAX_VALUE); requested.set(true); } @Override public void cancel() { } }); s.onNext("ignored"); //dropped s.onComplete(); //dropped s.onError(new Exception("test")); }) .subscribe(Operators.drainSubscriber()); assertThat(requested.get()).isTrue(); assertThat(errored.get()).isTrue(); } @Test public void scanCancelledSubscription() { CancelledSubscription test = CancelledSubscription.INSTANCE; assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test public void shouldBeSerialIfRacy() { for (int i = 0; i < 10000; i++) { long[] requested = new long[] { 0 }; Subscription mockSubscription = Mockito.mock(Subscription.class); Mockito.doAnswer(a -> requested[0] += (long) a.getArgument(0)).when(mockSubscription).request(Mockito.anyLong()); DeferredSubscription deferredSubscription = new DeferredSubscription(); deferredSubscription.request(5); RaceTestUtils.race(() -> deferredSubscription.set(mockSubscription), () -> { deferredSubscription.request(10); deferredSubscription.request(10); deferredSubscription.request(10); }); deferredSubscription.request(15); Assertions.assertThat(requested[0]).isEqualTo(50L); } } @Test public void scanDeferredSubscription() { DeferredSubscription test = new DeferredSubscription(); test.s = Operators.emptySubscription(); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(test.s); test.requested = 123; assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(123); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test public void scanEmptySubscription() { EmptySubscription test = EmptySubscription.INSTANCE; assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); } @Test public void scanMonoSubscriber() { CoreSubscriber<Integer> actual = new LambdaSubscriber<>(null, null, null, null); MonoSubscriber<Integer, Integer> test = new MonoSubscriber<>(actual); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.complete(4); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test public void scanMultiSubscriptionSubscriber() { CoreSubscriber<Integer> actual = new LambdaSubscriber<>(null, null, null, null); MultiSubscriptionSubscriber<Integer, Integer> test = new MultiSubscriptionSubscriber<Integer, Integer>(actual) { @Override public void onNext(Integer t) { } }; Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); test.request(34); assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(34); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test public void scanScalarSubscription() { CoreSubscriber<Integer> actual = new LambdaSubscriber<>(null, null, null, null); ScalarSubscription<Integer> test = new ScalarSubscription<>(actual, 5); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.poll(); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test public void onNextErrorModeLocalStrategy() { List<Object> nextDropped = new ArrayList<>(); List<Object> errorDropped = new ArrayList<>(); Hooks.onNextDropped(nextDropped::add); Hooks.onErrorDropped(errorDropped::add); Hooks.onNextError(OnNextFailureStrategy.STOP); Context c = Context.of(OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.RESUME_DROP); Exception error = new IllegalStateException("boom"); DeferredSubscription s = new Operators.DeferredSubscription(); assertThat(s.isCancelled()).as("s initially cancelled").isFalse(); Throwable e = Operators.onNextError("foo", error, c, s); assertThat(e).isNull(); assertThat(nextDropped).containsExactly("foo"); assertThat(errorDropped).containsExactly(error); assertThat(s.isCancelled()).as("s cancelled").isFalse(); } @Test public void pollErrorModeLocalStrategy() { List<Object> nextDropped = new ArrayList<>(); List<Object> errorDropped = new ArrayList<>(); Hooks.onNextDropped(nextDropped::add); Hooks.onErrorDropped(errorDropped::add); Context c = Context.of(OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.RESUME_DROP); Exception error = new IllegalStateException("boom"); assertThat(Hooks.onNextErrorHook).as("no global hook").isNull(); RuntimeException e = Operators.onNextPollError("foo", error, c); assertThat(e).isNull(); assertThat(nextDropped).containsExactly("foo"); assertThat(errorDropped).containsExactly(error); } @Test public void onErrorDroppedLocal() { AtomicReference<Throwable> hookState = new AtomicReference<>(); Consumer<Throwable> localHook = hookState::set; Context c = Context.of(Hooks.KEY_ON_ERROR_DROPPED, localHook); Operators.onErrorDropped(new IllegalArgumentException("boom"), c); assertThat(hookState.get()).isInstanceOf(IllegalArgumentException.class) .hasMessage("boom"); } @Test public void onNextDroppedLocal() { AtomicReference<Object> hookState = new AtomicReference<>(); Consumer<Object> localHook = hookState::set; Context c = Context.of(Hooks.KEY_ON_NEXT_DROPPED, localHook); Operators.onNextDropped("foo", c); assertThat(hookState.get()).isEqualTo("foo"); } @Test public void onOperatorErrorLocal() { BiFunction<Throwable, Object, Throwable> localHook = (e, v) -> new IllegalStateException("boom_" + v, e); Context c = Context.of(Hooks.KEY_ON_OPERATOR_ERROR, localHook); IllegalArgumentException failure = new IllegalArgumentException("foo"); final Throwable throwable = Operators.onOperatorError(null, failure, "foo", c); assertThat(throwable).isInstanceOf(IllegalStateException.class) .hasMessage("boom_foo") .hasCause(failure); } @Test public void onRejectedExecutionWithoutDataSignalDelegatesToErrorLocal() { BiFunction<Throwable, Object, Throwable> localHook = (e, v) -> new IllegalStateException("boom_" + v, e); Context c = Context.of(Hooks.KEY_ON_OPERATOR_ERROR, localHook); IllegalArgumentException failure = new IllegalArgumentException("foo"); final Throwable throwable = Operators.onRejectedExecution(failure, null, null, null, c); assertThat(throwable).isInstanceOf(IllegalStateException.class) .hasMessage("boom_null") .hasNoSuppressedExceptions(); assertThat(throwable.getCause()).isInstanceOf(RejectedExecutionException.class) .hasMessage("Scheduler unavailable") .hasCause(failure); } @Test public void onRejectedExecutionWithDataSignalDelegatesToErrorLocal() { BiFunction<Throwable, Object, Throwable> localHook = (e, v) -> new IllegalStateException("boom_" + v, e); Context c = Context.of(Hooks.KEY_ON_OPERATOR_ERROR, localHook); IllegalArgumentException failure = new IllegalArgumentException("foo"); final Throwable throwable = Operators.onRejectedExecution(failure, null, null, "bar", c); assertThat(throwable).isInstanceOf(IllegalStateException.class) .hasMessage("boom_bar") .hasNoSuppressedExceptions(); assertThat(throwable.getCause()).isInstanceOf(RejectedExecutionException.class) .hasMessage("Scheduler unavailable") .hasCause(failure); } @Test public void onRejectedExecutionLocalTakesPrecedenceOverOnOperatorError() { BiFunction<Throwable, Object, Throwable> localOperatorErrorHook = (e, v) -> new IllegalStateException("boom_" + v, e); BiFunction<Throwable, Object, Throwable> localReeHook = (e, v) -> new IllegalStateException("rejected_" + v, e); Context c = Context.of( Hooks.KEY_ON_OPERATOR_ERROR, localOperatorErrorHook, Hooks.KEY_ON_REJECTED_EXECUTION, localReeHook); IllegalArgumentException failure = new IllegalArgumentException("foo"); final Throwable throwable = Operators.onRejectedExecution(failure, null, null, "bar", c); assertThat(throwable).isInstanceOf(IllegalStateException.class) .hasMessage("rejected_bar") .hasNoSuppressedExceptions(); assertThat(throwable.getCause()).isInstanceOf(RejectedExecutionException.class) .hasMessage("Scheduler unavailable") .hasCause(failure); } @Test public void testOnRejectedWithReactorRee() { Exception originalCause = new Exception("boom"); RejectedExecutionException original = Exceptions.failWithRejected(originalCause); Exception suppressed = new Exception("suppressed"); RuntimeException test = Operators.onRejectedExecution(original, null, suppressed, null, Context.empty()); assertThat(test) .isSameAs(original) .hasSuppressedException(suppressed); } @Test public void testOnRejectedWithOutsideRee() { RejectedExecutionException original = new RejectedExecutionException("outside"); Exception suppressed = new Exception("suppressed"); RuntimeException test = Operators.onRejectedExecution(original, null, suppressed, null, Context.empty()); assertThat(test) .isNotSameAs(original) .isInstanceOf(RejectedExecutionException.class) .hasMessage("Scheduler unavailable") .hasCause(original) .hasSuppressedException(suppressed); } @Test public void unboundedOrPrefetch() { assertThat(Operators.unboundedOrPrefetch(10)) .as("bounded") .isEqualTo(10L); assertThat(Operators.unboundedOrPrefetch(Integer.MAX_VALUE)) .as("unbounded") .isEqualTo(Long.MAX_VALUE); } @Test public void unboundedOrLimit() { assertThat(Operators.unboundedOrLimit(100)) .as("prefetch - (prefetch >> 2)") .isEqualTo(75); assertThat(Operators.unboundedOrLimit(Integer.MAX_VALUE)) .as("unbounded") .isEqualTo(Integer.MAX_VALUE); } @Test public void unboundedOrLimitLowTide() { assertThat(Operators.unboundedOrLimit(100, 100)) .as("same lowTide") .isEqualTo(75); assertThat(Operators.unboundedOrLimit(100, 110)) .as("too big lowTide") .isEqualTo(75); assertThat(Operators.unboundedOrLimit(Integer.MAX_VALUE, Integer.MAX_VALUE)) .as("MAX_VALUE and same lowTide") .isEqualTo(Integer.MAX_VALUE); assertThat(Operators.unboundedOrLimit(100, 20)) .as("smaller lowTide") .isEqualTo(20); assertThat(Operators.unboundedOrLimit(Integer.MAX_VALUE, 110)) .as("smaller lowTide and MAX_VALUE") .isEqualTo(Integer.MAX_VALUE); assertThat(Operators.unboundedOrLimit(100, 0)) .as("0 lowTide and 100") .isEqualTo(100); assertThat(Operators.unboundedOrLimit(Integer.MAX_VALUE, 0)) .as("0 lowTide and MAX_VALUE") .isEqualTo(Integer.MAX_VALUE); assertThat(Operators.unboundedOrLimit(100, -1)) .as("-1 lowTide and 100") .isEqualTo(100); assertThat(Operators.unboundedOrLimit(Integer.MAX_VALUE, -1)) .as("-1 lowTide and MAX_VALUE") .isEqualTo(Integer.MAX_VALUE); } @Test public void onNextFailureWithStrategyMatchingDoesntCancel() { Context context = Context.of(OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, new OnNextFailureStrategy() { @Override public boolean test(Throwable error, @Nullable Object value) { return true; } @Nullable @Override public Throwable process(Throwable error, @Nullable Object value, Context context) { return null; } }); Operators.DeferredSubscription s = new Operators.DeferredSubscription(); Throwable t = Operators.onNextError("foo", new NullPointerException("bar"), context, s); assertThat(t).as("exception processed").isNull(); assertThat(s.isCancelled()).as("subscription cancelled").isFalse(); } @Test public void onNextFailureWithStrategyNotMatchingDoesCancel() { Context context = Context.of(OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, new OnNextFailureStrategy() { @Override public boolean test(Throwable error, @Nullable Object value) { return false; } @Override public Throwable process(Throwable error, @Nullable Object value, Context context) { return error; } }); Operators.DeferredSubscription s = new Operators.DeferredSubscription(); Throwable t = Operators.onNextError("foo", new NullPointerException("bar"), context, s); assertThat(t).as("exception processed") .isNotNull() .isInstanceOf(NullPointerException.class) .hasNoSuppressedExceptions() .hasNoCause(); assertThat(s.isCancelled()).as("subscription cancelled").isTrue(); } @Test public void onNextFailureWithStrategyMatchingButNotNullDoesCancel() { Context context = Context.of(OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, new OnNextFailureStrategy() { @Override public boolean test(Throwable error, @Nullable Object value) { return true; } @Override public Throwable process(Throwable error, @Nullable Object value, Context context) { return error; } }); Operators.DeferredSubscription s = new Operators.DeferredSubscription(); Throwable t = Operators.onNextError("foo", new NullPointerException("bar"), context, s); assertThat(t).as("exception processed") .isNotNull() .isInstanceOf(NullPointerException.class) .hasNoSuppressedExceptions() .hasNoCause(); assertThat(s.isCancelled()).as("subscription cancelled").isTrue(); } @Test public void liftVsLiftPublisher() { Publisher<Object> notScannable = new Flux<Object>() { @Override public void subscribe(CoreSubscriber<? super Object> actual) { } }; AtomicReference<Scannable> scannableRef = new AtomicReference<>(); AtomicReference<Publisher> rawRef = new AtomicReference<>(); @SuppressWarnings("unchecked") Function<Publisher<Object>, Publisher<Object>> lift = (Function<Publisher<Object>, Publisher<Object>>) Operators.lift((sc, sub) -> { scannableRef.set(sc); return sub; }); @SuppressWarnings("unchecked") Function<Publisher<Object>, Publisher<Object>> liftPublisher = (Function<Publisher<Object>, Publisher<Object>>) Operators.liftPublisher((pub, sub) -> { rawRef.set(pub); return sub; }); Publisher<Object> lifted = lift.apply(notScannable); Publisher<Object> liftedRaw = liftPublisher.apply(notScannable); assertThat(scannableRef).hasValue(null); assertThat(rawRef).hasValue(null); lifted.subscribe(new BaseSubscriber<Object>() {}); liftedRaw.subscribe(new BaseSubscriber<Object>() {}); assertThat(scannableRef.get()) .isNotNull() .isNotSameAs(notScannable) .matches(s -> !s.isScanAvailable(), "not scannable"); assertThat(rawRef.get()) .isNotNull() .isSameAs(notScannable); } @Test public void liftVsLiftRawWithPredicate() { Publisher<Object> notScannable = new Flux<Object>() { @Override public void subscribe(CoreSubscriber<? super Object> actual) { } }; AtomicReference<Scannable> scannableRef = new AtomicReference<>(); AtomicReference<Scannable> scannableFilterRef = new AtomicReference<>(); AtomicReference<Publisher> rawRef = new AtomicReference<>(); AtomicReference<Publisher> rawFilterRef = new AtomicReference<>(); @SuppressWarnings("unchecked") Function<Publisher<Object>, Publisher<Object>> lift = (Function<Publisher<Object>, Publisher<Object>>) Operators.lift(sc -> { scannableFilterRef.set(sc); return true; }, (sc, sub) -> { scannableRef.set(sc); return sub; }); @SuppressWarnings("unchecked") Function<Publisher<Object>, Publisher<Object>> liftRaw = (Function<Publisher<Object>, Publisher<Object>>) Operators.liftPublisher(pub -> { rawFilterRef.set(pub); return true; }, (pub, sub) -> { rawRef.set(pub); return sub; }); Publisher<Object> lifted = lift.apply(notScannable); Publisher<Object> liftedRaw = liftRaw.apply(notScannable); assertThat(scannableRef).hasValue(null); assertThat(scannableFilterRef).doesNotHaveValue(null); assertThat(rawRef).hasValue(null); assertThat(rawFilterRef).doesNotHaveValue(null); lifted.subscribe(new BaseSubscriber<Object>() {}); liftedRaw.subscribe(new BaseSubscriber<Object>() {}); assertThat(scannableRef.get()) .isNotNull() .isNotSameAs(notScannable) .matches(s -> !s.isScanAvailable(), "not scannable") .isSameAs(scannableFilterRef.get()); assertThat(rawRef.get()) .isNotNull() .isSameAs(notScannable) .isSameAs(rawFilterRef.get()); } @Test public void discardAdapterRejectsNull() { assertThatNullPointerException().isThrownBy(() -> Operators.discardLocalAdapter(null, obj -> {})) .as("type null check") .withMessage("onDiscard must be based on a type"); assertThatNullPointerException().isThrownBy(() -> Operators.discardLocalAdapter(String.class, null)) .as("discardHook null check") .withMessage("onDiscard must be provided a discardHook Consumer"); } @Test public void discardAdapterIsAdditive() { List<String> discardOrder = Collections.synchronizedList(new ArrayList<>(2)); Function<Context, Context> first = Operators.discardLocalAdapter(Number.class, i -> discardOrder.add("FIRST")); Function<Context, Context> second = Operators.discardLocalAdapter(Integer.class, i -> discardOrder.add("SECOND")); Context ctx = first.apply(second.apply(Context.empty())); Consumer<Object> test = ctx.getOrDefault(Hooks.KEY_ON_DISCARD, o -> {}); assertThat(test).isNotNull(); test.accept(1); assertThat(discardOrder).as("consumers were combined").containsExactly("FIRST", "SECOND"); } @Test public void convertNonConditionalToConditionalSubscriberTest() { Object elementToSend = new Object(); ArrayList<Object> captured = new ArrayList<>(); BaseSubscriber<Object> actual = new BaseSubscriber<Object>() { @Override protected void hookOnNext(Object value) { captured.add(value); } }; Fuseable.ConditionalSubscriber<? super Object> conditionalSubscriber = Operators.toConditionalSubscriber(actual); Assertions.assertThat(conditionalSubscriber).isNotEqualTo(actual); Assertions.assertThat(conditionalSubscriber.tryOnNext(elementToSend)).isTrue(); Assertions.assertThat(captured).containsExactly(elementToSend); } @Test public void convertConditionalToConditionalShouldReturnTheSameInstance() { @SuppressWarnings("unchecked") Fuseable.ConditionalSubscriber<String> original = Mockito.mock(Fuseable.ConditionalSubscriber.class); Assertions.assertThat(Operators.toConditionalSubscriber(original)) .isSameAs(original); } @Test public void discardQueueWithClearContinuesOnExtractionError() { AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Queue<List<Integer>> q = new ArrayBlockingQueue<>(5); q.add(Collections.singletonList(1)); q.add(Collections.singletonList(2)); q.add(Arrays.asList(3, 30)); q.add(Collections.singletonList(4)); q.add(Collections.singletonList(5)); Operators.onDiscardQueueWithClear(q, hookContext, o -> { List<Integer> l = o; if (l.size() == 2) throw new IllegalStateException("boom in extraction"); return l.stream(); }); assertThat(discardedCount).hasValue(4); } @Test public void discardQueueWithClearContinuesOnExtractedElementNotDiscarded() { AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Queue<List<Integer>> q = new ArrayBlockingQueue<>(5); q.add(Collections.singletonList(1)); q.add(Collections.singletonList(2)); q.add(Collections.singletonList(3)); q.add(Collections.singletonList(4)); q.add(Collections.singletonList(5)); Operators.onDiscardQueueWithClear(q, hookContext, Collection::stream); assertThat(discardedCount).as("discarded 1 2 4 5").hasValue(4); } @Test public void discardQueueWithClearContinuesOnRawQueueElementNotDiscarded() { AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Queue<Integer> q = new ArrayBlockingQueue<>(5); q.add(1); q.add(2); q.add(3); q.add(4); q.add(5); Operators.onDiscardQueueWithClear(q, hookContext, null); assertThat(discardedCount).as("discarded 1 2 4 5").hasValue(4); } @Test public void discardQueueWithClearStopsOnQueuePollingError() { AtomicInteger discardedCount = new AtomicInteger(); @SuppressWarnings("unchecked") Queue<Integer> q = Mockito.mock(Queue.class); Mockito.when(q.poll()) .thenReturn(1, 2) .thenThrow(new IllegalStateException("poll boom")) .thenReturn(4, 5) .thenReturn(null); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> discardedCount.incrementAndGet()).apply(Context.empty()); Operators.onDiscardQueueWithClear(q, hookContext, null); assertThat(discardedCount).as("discarding stops").hasValue(2); } @Test public void discardStreamContinuesWhenElementFailsToBeDiscarded() { Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Operators.onDiscardMultiple(stream, hookContext); assertThat(discardedCount).hasValue(4); } @Test public void discardStreamStopsOnIterationError() { Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); //noinspection ResultOfMethodCallIgnored stream.count(); //consumes the Stream on purpose AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> discardedCount.incrementAndGet()).apply(Context.empty()); Operators.onDiscardMultiple(stream, hookContext); assertThat(discardedCount).hasValue(0); } @Test public void discardCollectionContinuesWhenIteratorElementFailsToBeDiscarded() { List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5); AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Operators.onDiscardMultiple(elements, hookContext); assertThat(discardedCount).hasValue(4); } @Test public void discardCollectionStopsOnIterationError() { List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5); Iterator<Integer> trueIterator = elements.iterator(); Iterator<Integer> failingIterator = new Iterator<Integer>() { @Override public boolean hasNext() { return trueIterator.hasNext(); } @Override public Integer next() { Integer n = trueIterator.next(); if (n >= 3) throw new IllegalStateException("Iterator boom"); return n; } }; @SuppressWarnings("unchecked") List<Integer> mock = Mockito.mock(List.class); Mockito.when(mock.iterator()).thenReturn(failingIterator); AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Operators.onDiscardMultiple(mock, hookContext); assertThat(discardedCount).hasValue(2); } @Test public void discardCollectionStopsOnIsEmptyError() { @SuppressWarnings("unchecked") List<Integer> mock = Mockito.mock(List.class); Mockito.when(mock.isEmpty()).thenThrow(new IllegalStateException("isEmpty boom")); AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> discardedCount.incrementAndGet()) .apply(Context.empty()); Operators.onDiscardMultiple(mock, hookContext); assertThat(discardedCount).hasValue(0); } @Test public void discardIteratorContinuesWhenIteratorElementFailsToBeDiscarded() { List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5); AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Operators.onDiscardMultiple(elements.iterator(), true, hookContext); assertThat(discardedCount).hasValue(4); } @Test public void discardIteratorStopsOnIterationError() { List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5); Iterator<Integer> trueIterator = elements.iterator(); Iterator<Integer> failingIterator = new Iterator<Integer>() { @Override public boolean hasNext() { return trueIterator.hasNext(); } @Override public Integer next() { Integer n = trueIterator.next(); if (n >= 3) throw new IllegalStateException("Iterator boom"); return n; } }; AtomicInteger discardedCount = new AtomicInteger(); Context hookContext = Operators.discardLocalAdapter(Integer.class, i -> { if (i == 3) throw new IllegalStateException("boom"); discardedCount.incrementAndGet(); }).apply(Context.empty()); Operators.onDiscardMultiple(failingIterator, true, hookContext); assertThat(discardedCount).hasValue(2); } // see https://github.com/reactor/reactor-core/issues/2152 @Test public void reportThrowInSubscribeWithFuseableErrorResumed() { AssertSubscriber<Integer> assertSubscriber = AssertSubscriber.create(); FluxOnErrorResume.ResumeSubscriber<Integer> resumeSubscriber = new FluxOnErrorResume.ResumeSubscriber<>( assertSubscriber, t -> Mono.just(123)); FluxMapFuseable.MapFuseableSubscriber<String, Integer> fuseableSubscriber = new FluxMapFuseable.MapFuseableSubscriber<>( resumeSubscriber, String::length); Operators.reportThrowInSubscribe(fuseableSubscriber, new RuntimeException("boom")); assertSubscriber.assertNoError().awaitAndAssertNextValues(123); } }