/** * Copyright (c) 2016-present, RxJava Contributors. * * 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 io.reactivex.internal.operators.flowable; import static java.util.Arrays.asList; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import org.junit.*; import org.reactivestreams.*; import io.reactivex.*; import io.reactivex.Scheduler.Worker; import io.reactivex.exceptions.TestException; import io.reactivex.functions.*; import io.reactivex.internal.functions.Functions; import io.reactivex.internal.subscriptions.*; import io.reactivex.internal.util.*; import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.processors.PublishProcessor; import io.reactivex.schedulers.*; import io.reactivex.subscribers.*; public class FlowableMergeTest { Subscriber<String> stringObserver; int count; @Before public void before() { stringObserver = TestHelper.mockSubscriber(); for (Thread t : Thread.getAllStackTraces().keySet()) { if (t.getName().startsWith("RxNewThread")) { count++; } } } @After public void after() { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } for (Thread t : Thread.getAllStackTraces().keySet()) { if (t.getName().startsWith("RxNewThread")) { --count; } } if (count != 0) { throw new IllegalStateException("NewThread leak!"); } } @Test public void testMergeFlowableOfFlowables() { final Flowable<String> o1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); final Flowable<String> o2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); Flowable<Flowable<String>> FlowableOfFlowables = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { @Override public void subscribe(Subscriber<? super Flowable<String>> observer) { observer.onSubscribe(new BooleanSubscription()); // simulate what would happen in a Flowable observer.onNext(o1); observer.onNext(o2); observer.onComplete(); } }); Flowable<String> m = Flowable.merge(FlowableOfFlowables); m.subscribe(stringObserver); verify(stringObserver, never()).onError(any(Throwable.class)); verify(stringObserver, times(1)).onComplete(); verify(stringObserver, times(2)).onNext("hello"); } @Test public void testMergeArray() { final Flowable<String> o1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); final Flowable<String> o2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); Flowable<String> m = Flowable.merge(o1, o2); m.subscribe(stringObserver); verify(stringObserver, never()).onError(any(Throwable.class)); verify(stringObserver, times(2)).onNext("hello"); verify(stringObserver, times(1)).onComplete(); } @Test public void testMergeList() { final Flowable<String> o1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); final Flowable<String> o2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); List<Flowable<String>> listOfFlowables = new ArrayList<Flowable<String>>(); listOfFlowables.add(o1); listOfFlowables.add(o2); Flowable<String> m = Flowable.merge(listOfFlowables); m.subscribe(stringObserver); verify(stringObserver, never()).onError(any(Throwable.class)); verify(stringObserver, times(1)).onComplete(); verify(stringObserver, times(2)).onNext("hello"); } @Test(timeout = 1000) public void testUnSubscribeFlowableOfFlowables() throws InterruptedException { final AtomicBoolean unsubscribed = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); Flowable<Flowable<Long>> source = Flowable.unsafeCreate(new Publisher<Flowable<Long>>() { @Override public void subscribe(final Subscriber<? super Flowable<Long>> observer) { // verbose on purpose so I can track the inside of it final Subscription s = new Subscription() { @Override public void request(long n) { } @Override public void cancel() { System.out.println("*** unsubscribed"); unsubscribed.set(true); } }; observer.onSubscribe(s); new Thread(new Runnable() { @Override public void run() { while (!unsubscribed.get()) { observer.onNext(Flowable.just(1L, 2L)); } System.out.println("Done looping after unsubscribe: " + unsubscribed.get()); observer.onComplete(); // mark that the thread is finished latch.countDown(); } }).start(); } }); final AtomicInteger count = new AtomicInteger(); Flowable.merge(source).take(6).blockingForEach(new Consumer<Long>() { @Override public void accept(Long v) { System.out.println("Value: " + v); int c = count.incrementAndGet(); if (c > 6) { fail("Should be only 6"); } } }); latch.await(1000, TimeUnit.MILLISECONDS); System.out.println("unsubscribed: " + unsubscribed.get()); assertTrue(unsubscribed.get()); } @Test public void testMergeArrayWithThreading() { final TestASynchronousFlowable o1 = new TestASynchronousFlowable(); final TestASynchronousFlowable o2 = new TestASynchronousFlowable(); Flowable<String> m = Flowable.merge(Flowable.unsafeCreate(o1), Flowable.unsafeCreate(o2)); TestSubscriber<String> ts = new TestSubscriber<String>(stringObserver); m.subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); verify(stringObserver, never()).onError(any(Throwable.class)); verify(stringObserver, times(2)).onNext("hello"); verify(stringObserver, times(1)).onComplete(); } @Test public void testSynchronizationOfMultipleSequencesLoop() throws Throwable { for (int i = 0; i < 100; i++) { System.out.println("testSynchronizationOfMultipleSequencesLoop > " + i); testSynchronizationOfMultipleSequences(); } } @Test public void testSynchronizationOfMultipleSequences() throws Throwable { final TestASynchronousFlowable o1 = new TestASynchronousFlowable(); final TestASynchronousFlowable o2 = new TestASynchronousFlowable(); // use this latch to cause onNext to wait until we're ready to let it go final CountDownLatch endLatch = new CountDownLatch(1); final AtomicInteger concurrentCounter = new AtomicInteger(); final AtomicInteger totalCounter = new AtomicInteger(); final AtomicReference<Throwable> error = new AtomicReference<Throwable>(); Flowable<String> m = Flowable.merge(Flowable.unsafeCreate(o1), Flowable.unsafeCreate(o2)); m.subscribe(new DefaultSubscriber<String>() { @Override public void onComplete() { } @Override public void onError(Throwable e) { error.set(e); } @Override public void onNext(String v) { totalCounter.incrementAndGet(); concurrentCounter.incrementAndGet(); try { // avoid deadlocking the main thread if (Thread.currentThread().getName().equals("TestASynchronousFlowable")) { // wait here until we're done asserting endLatch.await(); } } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException("failed", e); } finally { concurrentCounter.decrementAndGet(); } } }); // wait for both Flowables to send (one should be blocked) o1.onNextBeingSent.await(); o2.onNextBeingSent.await(); // I can't think of a way to know for sure that both threads have or are trying to send onNext // since I can't use a CountDownLatch for "after" onNext since I want to catch during it // but I can't know for sure onNext is invoked // so I'm unfortunately reverting to using a Thread.sleep to allow the process scheduler time // to make sure after o1.onNextBeingSent and o2.onNextBeingSent are hit that the following // onNext is invoked. int timeout = 20; while (timeout-- > 0 && concurrentCounter.get() != 1) { Thread.sleep(100); } try { // in try/finally so threads are released via latch countDown even if assertion fails if (error.get() != null) { throw ExceptionHelper.wrapOrThrow(error.get()); } assertEquals(1, concurrentCounter.get()); } finally { // release so it can finish endLatch.countDown(); } try { o1.t.join(); o2.t.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } assertEquals(2, totalCounter.get()); assertEquals(0, concurrentCounter.get()); } /** * Unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge. */ @Test public void testError1() { // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior final Flowable<String> o1 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" final Flowable<String> o2 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); // we expect to lose all of these since o1 is done first and fails Flowable<String> m = Flowable.merge(o1, o2); m.subscribe(stringObserver); verify(stringObserver, times(1)).onError(any(NullPointerException.class)); verify(stringObserver, never()).onComplete(); verify(stringObserver, times(0)).onNext("one"); verify(stringObserver, times(0)).onNext("two"); verify(stringObserver, times(0)).onNext("three"); verify(stringObserver, times(1)).onNext("four"); verify(stringObserver, times(0)).onNext("five"); verify(stringObserver, times(0)).onNext("six"); } /** * Unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge. */ @Test public void testError2() { // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior final Flowable<String> o1 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); final Flowable<String> o2 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" final Flowable<String> o3 = Flowable.unsafeCreate(new TestErrorFlowable("seven", "eight", null));// we expect to lose all of these since o2 is done first and fails final Flowable<String> o4 = Flowable.unsafeCreate(new TestErrorFlowable("nine"));// we expect to lose all of these since o2 is done first and fails Flowable<String> m = Flowable.merge(o1, o2, o3, o4); m.subscribe(stringObserver); verify(stringObserver, times(1)).onError(any(NullPointerException.class)); verify(stringObserver, never()).onComplete(); verify(stringObserver, times(1)).onNext("one"); verify(stringObserver, times(1)).onNext("two"); verify(stringObserver, times(1)).onNext("three"); verify(stringObserver, times(1)).onNext("four"); verify(stringObserver, times(0)).onNext("five"); verify(stringObserver, times(0)).onNext("six"); verify(stringObserver, times(0)).onNext("seven"); verify(stringObserver, times(0)).onNext("eight"); verify(stringObserver, times(0)).onNext("nine"); } @Test @Ignore("Subscribe should not throw") public void testThrownErrorHandling() { TestSubscriber<String> ts = new TestSubscriber<String>(); Flowable<String> o1 = Flowable.unsafeCreate(new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> s) { throw new RuntimeException("fail"); } }); Flowable.merge(o1, o1).subscribe(ts); ts.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS); ts.assertTerminated(); System.out.println("Error: " + ts.errors()); } private static class TestSynchronousFlowable implements Publisher<String> { @Override public void subscribe(Subscriber<? super String> observer) { observer.onSubscribe(new BooleanSubscription()); observer.onNext("hello"); observer.onComplete(); } } private static class TestASynchronousFlowable implements Publisher<String> { Thread t; final CountDownLatch onNextBeingSent = new CountDownLatch(1); @Override public void subscribe(final Subscriber<? super String> observer) { observer.onSubscribe(new BooleanSubscription()); t = new Thread(new Runnable() { @Override public void run() { onNextBeingSent.countDown(); try { observer.onNext("hello"); // I can't use a countDownLatch to prove we are actually sending 'onNext' // since it will block if synchronized and I'll deadlock observer.onComplete(); } catch (Exception e) { observer.onError(e); } } }, "TestASynchronousFlowable"); t.start(); } } private static class TestErrorFlowable implements Publisher<String> { String[] valuesToReturn; TestErrorFlowable(String... values) { valuesToReturn = values; } @Override public void subscribe(Subscriber<? super String> observer) { observer.onSubscribe(new BooleanSubscription()); for (String s : valuesToReturn) { if (s == null) { System.out.println("throwing exception"); observer.onError(new NullPointerException()); } else { observer.onNext(s); } } observer.onComplete(); } } @Test public void testUnsubscribeAsFlowablesComplete() { TestScheduler scheduler1 = new TestScheduler(); AtomicBoolean os1 = new AtomicBoolean(false); Flowable<Long> o1 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1); TestScheduler scheduler2 = new TestScheduler(); AtomicBoolean os2 = new AtomicBoolean(false); Flowable<Long> o2 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2); TestSubscriber<Long> ts = new TestSubscriber<Long>(); Flowable.merge(o1, o2).subscribe(ts); // we haven't incremented time so nothing should be received yet ts.assertNoValues(); scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); scheduler2.advanceTimeBy(2, TimeUnit.SECONDS); ts.assertValues(0L, 1L, 2L, 0L, 1L); // not unsubscribed yet assertFalse(os1.get()); assertFalse(os2.get()); // advance to the end at which point it should complete scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); ts.assertValues(0L, 1L, 2L, 0L, 1L, 3L, 4L); assertTrue(os1.get()); assertFalse(os2.get()); // both should be completed now scheduler2.advanceTimeBy(3, TimeUnit.SECONDS); ts.assertValues(0L, 1L, 2L, 0L, 1L, 3L, 4L, 2L, 3L, 4L); assertTrue(os1.get()); assertTrue(os2.get()); ts.assertTerminated(); } @Test public void testEarlyUnsubscribe() { for (int i = 0; i < 10; i++) { TestScheduler scheduler1 = new TestScheduler(); AtomicBoolean os1 = new AtomicBoolean(false); Flowable<Long> o1 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1); TestScheduler scheduler2 = new TestScheduler(); AtomicBoolean os2 = new AtomicBoolean(false); Flowable<Long> o2 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2); TestSubscriber<Long> ts = new TestSubscriber<Long>(); Flowable.merge(o1, o2).subscribe(ts); // we haven't incremented time so nothing should be received yet ts.assertNoValues(); scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); scheduler2.advanceTimeBy(2, TimeUnit.SECONDS); ts.assertValues(0L, 1L, 2L, 0L, 1L); // not unsubscribed yet assertFalse(os1.get()); assertFalse(os2.get()); // early unsubscribe ts.dispose(); assertTrue(os1.get()); assertTrue(os2.get()); ts.assertValues(0L, 1L, 2L, 0L, 1L); // FIXME not happening anymore // ts.assertUnsubscribed(); } } private Flowable<Long> createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(final Scheduler scheduler, final AtomicBoolean unsubscribed) { return Flowable.unsafeCreate(new Publisher<Long>() { @Override public void subscribe(final Subscriber<? super Long> child) { Flowable.interval(1, TimeUnit.SECONDS, scheduler) .take(5) .subscribe(new FlowableSubscriber<Long>() { @Override public void onSubscribe(final Subscription s) { child.onSubscribe(new Subscription() { @Override public void request(long n) { s.request(n); } @Override public void cancel() { unsubscribed.set(true); s.cancel(); } }); } @Override public void onNext(Long t) { child.onNext(t); } @Override public void onError(Throwable t) { unsubscribed.set(true); child.onError(t); } @Override public void onComplete() { unsubscribed.set(true); child.onComplete(); } }); } }); } @Test//(timeout = 10000) public void testConcurrency() { Flowable<Integer> o = Flowable.range(1, 10000).subscribeOn(Schedulers.newThread()); for (int i = 0; i < 10; i++) { Flowable<Integer> merge = Flowable.merge(o.onBackpressureBuffer(), o.onBackpressureBuffer(), o.onBackpressureBuffer()); TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); merge.subscribe(ts); ts.awaitTerminalEvent(3, TimeUnit.SECONDS); ts.assertTerminated(); ts.assertNoErrors(); ts.assertComplete(); List<Integer> onNextEvents = ts.values(); assertEquals(30000, onNextEvents.size()); // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); } } @Test public void testConcurrencyWithSleeping() { Flowable<Integer> o = Flowable.unsafeCreate(new Publisher<Integer>() { @Override public void subscribe(final Subscriber<? super Integer> s) { Worker inner = Schedulers.newThread().createWorker(); final AsyncSubscription as = new AsyncSubscription(); as.setSubscription(new BooleanSubscription()); as.setResource(inner); s.onSubscribe(as); inner.schedule(new Runnable() { @Override public void run() { try { for (int i = 0; i < 100; i++) { s.onNext(1); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } catch (Exception e) { s.onError(e); } as.dispose(); s.onComplete(); } }); } }); for (int i = 0; i < 10; i++) { Flowable<Integer> merge = Flowable.merge(o, o, o); TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); merge.subscribe(ts); ts.awaitTerminalEvent(); ts.assertComplete(); List<Integer> onNextEvents = ts.values(); assertEquals(300, onNextEvents.size()); // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); } } @Test public void testConcurrencyWithBrokenOnCompleteContract() { Flowable<Integer> o = Flowable.unsafeCreate(new Publisher<Integer>() { @Override public void subscribe(final Subscriber<? super Integer> s) { Worker inner = Schedulers.newThread().createWorker(); final AsyncSubscription as = new AsyncSubscription(); as.setSubscription(new BooleanSubscription()); as.setResource(inner); s.onSubscribe(as); inner.schedule(new Runnable() { @Override public void run() { try { for (int i = 0; i < 10000; i++) { s.onNext(i); } } catch (Exception e) { s.onError(e); } as.dispose(); s.onComplete(); s.onComplete(); s.onComplete(); } }); } }); for (int i = 0; i < 10; i++) { Flowable<Integer> merge = Flowable.merge(o.onBackpressureBuffer(), o.onBackpressureBuffer(), o.onBackpressureBuffer()); TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); merge.subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); ts.assertComplete(); List<Integer> onNextEvents = ts.values(); assertEquals(30000, onNextEvents.size()); // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); } } @Test public void testBackpressureUpstream() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); Flowable<Integer> o1 = createInfiniteFlowable(generated1).subscribeOn(Schedulers.computation()); final AtomicInteger generated2 = new AtomicInteger(); Flowable<Integer> o2 = createInfiniteFlowable(generated2).subscribeOn(Schedulers.computation()); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { @Override public void onNext(Integer t) { System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); super.onNext(t); } }; Flowable.merge(o1.take(Flowable.bufferSize() * 2), o2.take(Flowable.bufferSize() * 2)).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); if (testSubscriber.errors().size() > 0) { testSubscriber.errors().get(0).printStackTrace(); } testSubscriber.assertNoErrors(); System.err.println(testSubscriber.values()); assertEquals(Flowable.bufferSize() * 4, testSubscriber.values().size()); // it should be between the take num and requested batch size across the async boundary System.out.println("Generated 1: " + generated1.get()); System.out.println("Generated 2: " + generated2.get()); assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); } @Test public void testBackpressureUpstream2InLoop() throws InterruptedException { for (int i = 0; i < 1000; i++) { System.err.flush(); System.out.println("---"); System.out.flush(); testBackpressureUpstream2(); } } @Test public void testBackpressureUpstream2() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); Flowable<Integer> o1 = createInfiniteFlowable(generated1).subscribeOn(Schedulers.computation()); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { @Override public void onNext(Integer t) { super.onNext(t); } }; Flowable.merge(o1.take(Flowable.bufferSize() * 2), Flowable.just(-99)).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); List<Integer> onNextEvents = testSubscriber.values(); System.out.println("Generated 1: " + generated1.get() + " / received: " + onNextEvents.size()); System.out.println(onNextEvents); if (testSubscriber.errors().size() > 0) { testSubscriber.errors().get(0).printStackTrace(); } testSubscriber.assertNoErrors(); assertEquals(Flowable.bufferSize() * 2 + 1, onNextEvents.size()); // it should be between the take num and requested batch size across the async boundary assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 3); } /** * This is the same as the upstreams ones, but now adds the downstream as well by using observeOn. * * This requires merge to also obey the Product.request values coming from it's child subscriber. * @throws InterruptedException if the test is interrupted */ @Test(timeout = 10000) public void testBackpressureDownstreamWithConcurrentStreams() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); Flowable<Integer> o1 = createInfiniteFlowable(generated1).subscribeOn(Schedulers.computation()); final AtomicInteger generated2 = new AtomicInteger(); Flowable<Integer> o2 = createInfiniteFlowable(generated2).subscribeOn(Schedulers.computation()); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { @Override public void onNext(Integer t) { if (t < 100) { try { // force a slow consumer Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } // System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); super.onNext(t); } }; Flowable.merge(o1.take(Flowable.bufferSize() * 2), o2.take(Flowable.bufferSize() * 2)).observeOn(Schedulers.computation()).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); if (testSubscriber.errors().size() > 0) { testSubscriber.errors().get(0).printStackTrace(); } testSubscriber.assertNoErrors(); System.err.println(testSubscriber.values()); assertEquals(Flowable.bufferSize() * 4, testSubscriber.values().size()); // it should be between the take num and requested batch size across the async boundary System.out.println("Generated 1: " + generated1.get()); System.out.println("Generated 2: " + generated2.get()); assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); } @Test public void testBackpressureBothUpstreamAndDownstreamWithSynchronousScalarFlowables() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); Flowable<Flowable<Integer>> o1 = createInfiniteFlowable(generated1) .map(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer t1) { return Flowable.just(t1); } }); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { @Override public void onNext(Integer t) { if (t < 100) { try { // force a slow consumer Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } // System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); super.onNext(t); } }; Flowable.merge(o1).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); if (testSubscriber.errors().size() > 0) { testSubscriber.errors().get(0).printStackTrace(); } testSubscriber.assertNoErrors(); System.out.println("Generated 1: " + generated1.get()); System.err.println(testSubscriber.values()); assertEquals(Flowable.bufferSize() * 2, testSubscriber.values().size()); // it should be between the take num and requested batch size across the async boundary assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); } /** * Currently there is no solution to this ... we can't exert backpressure on the outer Flowable if we * can't know if the ones we've received so far are going to emit or not, otherwise we could starve the system. * * For example, 10,000 Flowables are being merged (bad use case to begin with, but ...) and it's only one of them * that will ever emit. If backpressure only allowed the first 1,000 to be sent, we would hang and never receive an event. * * Thus, we must allow all Flowables to be sent. The ScalarSynchronousFlowable use case is an exception to this since * we can grab the value synchronously. * * @throws InterruptedException if the await is interrupted */ @Test(timeout = 5000) public void testBackpressureBothUpstreamAndDownstreamWithRegularFlowables() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); Flowable<Flowable<Integer>> o1 = createInfiniteFlowable(generated1).map(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer t1) { return Flowable.just(1, 2, 3); } }); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { int i; @Override public void onNext(Integer t) { if (i++ < 400) { try { // force a slow consumer Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } // System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); super.onNext(t); } }; Flowable.merge(o1).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); if (testSubscriber.errors().size() > 0) { testSubscriber.errors().get(0).printStackTrace(); } testSubscriber.assertNoErrors(); System.out.println("Generated 1: " + generated1.get()); System.err.println(testSubscriber.values()); System.out.println("done1 testBackpressureBothUpstreamAndDownstreamWithRegularFlowables "); assertEquals(Flowable.bufferSize() * 2, testSubscriber.values().size()); System.out.println("done2 testBackpressureBothUpstreamAndDownstreamWithRegularFlowables "); // we can't restrict this ... see comment above // assertTrue(generated1.get() >= Flowable.bufferSize() && generated1.get() <= Flowable.bufferSize() * 4); } @Test @Ignore("Null values not permitted") public void mergeWithNullValues() { System.out.println("mergeWithNullValues"); TestSubscriber<String> ts = new TestSubscriber<String>(); Flowable.merge(Flowable.just(null, "one"), Flowable.just("two", null)).subscribe(ts); ts.assertTerminated(); ts.assertNoErrors(); ts.assertValues(null, "one", "two", null); } @Test @Ignore("Null values are no longer permitted") public void mergeWithTerminalEventAfterUnsubscribe() { System.out.println("mergeWithTerminalEventAfterUnsubscribe"); TestSubscriber<String> ts = new TestSubscriber<String>(); Flowable<String> bad = Flowable.unsafeCreate(new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> s) { s.onNext("two"); // FIXME can't cancel downstream // s.unsubscribe(); // s.onComplete(); } }); Flowable.merge(Flowable.just(null, "one"), bad).subscribe(ts); ts.assertNoErrors(); ts.assertValues(null, "one", "two"); } @Test @Ignore("Null values are not permitted") public void mergingNullFlowable() { TestSubscriber<String> ts = new TestSubscriber<String>(); Flowable.merge(Flowable.just("one"), null).subscribe(ts); ts.assertNoErrors(); ts.assertValue("one"); } @Test public void merge1AsyncStreamOf1() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNAsyncStreamsOfN(1, 1).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1, ts.values().size()); } @Test public void merge1AsyncStreamOf1000() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNAsyncStreamsOfN(1, 1000).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1000, ts.values().size()); } @Test public void merge10AsyncStreamOf1000() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNAsyncStreamsOfN(10, 1000).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(10000, ts.values().size()); } @Test public void merge1000AsyncStreamOf1000() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNAsyncStreamsOfN(1000, 1000).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1000000, ts.values().size()); } @Test public void merge2000AsyncStreamOf100() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNAsyncStreamsOfN(2000, 100).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(200000, ts.values().size()); } @Test public void merge100AsyncStreamOf1() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNAsyncStreamsOfN(100, 1).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(100, ts.values().size()); } private Flowable<Integer> mergeNAsyncStreamsOfN(final int outerSize, final int innerSize) { Flowable<Flowable<Integer>> os = Flowable.range(1, outerSize) .map(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer i) { return Flowable.range(1, innerSize).subscribeOn(Schedulers.computation()); } }); return Flowable.merge(os); } @Test public void merge1SyncStreamOf1() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNSyncStreamsOfN(1, 1).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1, ts.values().size()); } @Test public void merge1SyncStreamOf1000000() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNSyncStreamsOfN(1, 1000000).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1000000, ts.values().size()); } @Test public void merge1000SyncStreamOf1000() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNSyncStreamsOfN(1000, 1000).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1000000, ts.values().size()); } @Test public void merge10000SyncStreamOf10() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNSyncStreamsOfN(10000, 10).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(100000, ts.values().size()); } @Test public void merge1000000SyncStreamOf1() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); mergeNSyncStreamsOfN(1000000, 1).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(1000000, ts.values().size()); } private Flowable<Integer> mergeNSyncStreamsOfN(final int outerSize, final int innerSize) { Flowable<Flowable<Integer>> os = Flowable.range(1, outerSize) .map(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer i) { return Flowable.range(1, innerSize); } }); return Flowable.merge(os); } private Flowable<Integer> createInfiniteFlowable(final AtomicInteger generated) { Flowable<Integer> flowable = Flowable.fromIterable(new Iterable<Integer>() { @Override public Iterator<Integer> iterator() { return new Iterator<Integer>() { @Override public void remove() { } @Override public Integer next() { return generated.getAndIncrement(); } @Override public boolean hasNext() { return true; } }; } }); return flowable; } @Test public void mergeManyAsyncSingle() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); Flowable<Flowable<Integer>> os = Flowable.range(1, 10000) .map(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(final Integer i) { return Flowable.unsafeCreate(new Publisher<Integer>() { @Override public void subscribe(Subscriber<? super Integer> s) { s.onSubscribe(new BooleanSubscription()); if (i < 500) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } s.onNext(i); s.onComplete(); } }).subscribeOn(Schedulers.computation()).cache(); } }); Flowable.merge(os).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); assertEquals(10000, ts.values().size()); } @Test public void shouldCompleteAfterApplyingBackpressure_NormalPath() { Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(Flowable.range(1, 2))); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(0L); source.subscribe(subscriber); subscriber.request(3); // 1, 2, <complete> - with request(2) we get the 1 and 2 but not the <complete> subscriber.assertValues(1, 2); subscriber.assertTerminated(); } @Test public void shouldCompleteAfterApplyingBackpressure_FastPath() { Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(Flowable.just(1))); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(0L); source.subscribe(subscriber); subscriber.request(2); // 1, <complete> - should work as per .._NormalPath above subscriber.assertValue(1); subscriber.assertTerminated(); } @Test public void shouldNotCompleteIfThereArePendingScalarSynchronousEmissionsWhenTheLastInnerSubscriberCompletes() { TestScheduler scheduler = new TestScheduler(); Flowable<Long> source = Flowable.mergeDelayError(Flowable.just(1L), Flowable.timer(1, TimeUnit.SECONDS, scheduler).skip(1)); TestSubscriber<Long> subscriber = new TestSubscriber<Long>(0L); source.subscribe(subscriber); scheduler.advanceTimeBy(1, TimeUnit.SECONDS); subscriber.assertNoValues(); subscriber.assertNotComplete(); subscriber.request(1); subscriber.assertValue(1L); // TODO: it should be acceptable to get a completion event without requests // assertEquals(Collections.<Notification<Long>>emptyList(), subscriber.getOnCompletedEvents()); // subscriber.request(1); subscriber.assertTerminated(); } @Test public void delayedErrorsShouldBeEmittedWhenCompleteAfterApplyingBackpressure_NormalPath() { Throwable exception = new Throwable(); Flowable<Integer> source = Flowable.mergeDelayError(Flowable.range(1, 2), Flowable.<Integer>error(exception)); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(0L); source.subscribe(subscriber); subscriber.request(3); // 1, 2, <error> subscriber.assertValues(1, 2); subscriber.assertTerminated(); assertEquals(asList(exception), subscriber.errors()); } @Test public void delayedErrorsShouldBeEmittedWhenCompleteAfterApplyingBackpressure_FastPath() { Throwable exception = new Throwable(); Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(1), Flowable.<Integer>error(exception)); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(0L); source.subscribe(subscriber); subscriber.request(2); // 1, <error> subscriber.assertValue(1); subscriber.assertTerminated(); assertEquals(asList(exception), subscriber.errors()); } @Test public void shouldNotCompleteWhileThereAreStillScalarSynchronousEmissionsInTheQueue() { Flowable<Integer> source = Flowable.merge(Flowable.just(1), Flowable.just(2)); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(1L); source.subscribe(subscriber); subscriber.assertValue(1); subscriber.request(1); subscriber.assertValues(1, 2); } @Test public void shouldNotReceivedDelayedErrorWhileThereAreStillScalarSynchronousEmissionsInTheQueue() { Throwable exception = new Throwable(); Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(1), Flowable.just(2), Flowable.<Integer>error(exception)); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(0L); subscriber.request(1); source.subscribe(subscriber); subscriber.assertValue(1); assertEquals(Collections.<Throwable>emptyList(), subscriber.errors()); subscriber.request(1); subscriber.assertValues(1, 2); assertEquals(asList(exception), subscriber.errors()); } @Test public void shouldNotReceivedDelayedErrorWhileThereAreStillNormalEmissionsInTheQueue() { Throwable exception = new Throwable(); Flowable<Integer> source = Flowable.mergeDelayError(Flowable.range(1, 2), Flowable.range(3, 2), Flowable.<Integer>error(exception)); TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>(0L); subscriber.request(3); source.subscribe(subscriber); subscriber.assertValues(1, 2, 3); assertEquals(Collections.<Throwable>emptyList(), subscriber.errors()); subscriber.request(2); subscriber.assertValues(1, 2, 3, 4); assertEquals(asList(exception), subscriber.errors()); } @Test public void testMergeKeepsRequesting() throws InterruptedException { //for (int i = 0; i < 5000; i++) { //System.out.println(i + "......................................................................."); final CountDownLatch latch = new CountDownLatch(1); final ConcurrentLinkedQueue<String> messages = new ConcurrentLinkedQueue<String>(); Flowable.range(1, 2) // produce many integers per second .flatMap(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(final Integer number) { return Flowable.range(1, Integer.MAX_VALUE) .doOnRequest(new LongConsumer() { @Override public void accept(long n) { messages.add(">>>>>>>> A requested[" + number + "]: " + n); } }) // pause a bit .doOnNext(pauseForMs(3)) // buffer on backpressure .onBackpressureBuffer() // do in parallel .subscribeOn(Schedulers.computation()) .doOnRequest(new LongConsumer() { @Override public void accept(long n) { messages.add(">>>>>>>> B requested[" + number + "]: " + n); } }); } }) // take a number bigger than 2* Flowable.bufferSize() (used by OperatorMerge) .take(Flowable.bufferSize() * 2 + 1) // log count .doOnNext(printCount()) // release latch .doOnComplete(new Action() { @Override public void run() { latch.countDown(); } }).subscribe(); boolean a = latch.await(2, TimeUnit.SECONDS); if (!a) { for (String s : messages) { System.out.println("DEBUG => " + s); } } assertTrue(a); //} } @Test public void testMergeRequestOverflow() throws InterruptedException { //do a non-trivial merge so that future optimisations with EMPTY don't invalidate this test Flowable<Integer> o = Flowable.fromIterable(Arrays.asList(1,2)) .mergeWith(Flowable.fromIterable(Arrays.asList(3,4))); final int expectedCount = 4; final CountDownLatch latch = new CountDownLatch(expectedCount); o.subscribeOn(Schedulers.computation()).subscribe(new DefaultSubscriber<Integer>() { @Override public void onStart() { request(1); } @Override public void onComplete() { //ignore } @Override public void onError(Throwable e) { throw new RuntimeException(e); } @Override public void onNext(Integer t) { latch.countDown(); request(2); request(Long.MAX_VALUE - 1); }}); assertTrue(latch.await(10, TimeUnit.SECONDS)); } private static Consumer<Integer> printCount() { return new Consumer<Integer>() { long count; @Override public void accept(Integer t1) { count++; System.out.println("count=" + count); } }; } private static Consumer<Integer> pauseForMs(final long time) { return new Consumer<Integer>() { @Override public void accept(Integer s) { try { Thread.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; } Function<Integer, Flowable<Integer>> toScalar = new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer v) { return Flowable.just(v); } }; Function<Integer, Flowable<Integer>> toHiddenScalar = new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer t) { return Flowable.just(t).hide(); } }; ; void runMerge(Function<Integer, Flowable<Integer>> func, TestSubscriber<Integer> ts) { List<Integer> list = new ArrayList<Integer>(); for (int i = 0; i < 1000; i++) { list.add(i); } Flowable<Integer> source = Flowable.fromIterable(list); source.flatMap(func).subscribe(ts); if (ts.values().size() != 1000) { System.out.println(ts.values()); } ts.assertTerminated(); ts.assertNoErrors(); ts.assertValueSequence(list); } @Test public void testFastMergeFullScalar() { runMerge(toScalar, new TestSubscriber<Integer>()); } @Test public void testFastMergeHiddenScalar() { runMerge(toHiddenScalar, new TestSubscriber<Integer>()); } @Test public void testSlowMergeFullScalar() { for (final int req : new int[] { 16, 32, 64, 128, 256 }) { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(req) { int remaining = req; @Override public void onNext(Integer t) { super.onNext(t); if (--remaining == 0) { remaining = req; request(req); } } }; runMerge(toScalar, ts); } } @Test public void testSlowMergeHiddenScalar() { for (final int req : new int[] { 16, 32, 64, 128, 256 }) { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(req) { int remaining = req; @Override public void onNext(Integer t) { super.onNext(t); if (--remaining == 0) { remaining = req; request(req); } } }; runMerge(toHiddenScalar, ts); } } @SuppressWarnings("unchecked") @Test public void negativeMaxConcurrent() { try { Flowable.merge(Arrays.asList(Flowable.just(1), Flowable.just(2)), -1); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertEquals("maxConcurrency > 0 required but it was -1", e.getMessage()); } } @SuppressWarnings("unchecked") @Test public void zeroMaxConcurrent() { try { Flowable.merge(Arrays.asList(Flowable.just(1), Flowable.just(2)), 0); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertEquals("maxConcurrency > 0 required but it was 0", e.getMessage()); } } @Test @Ignore("Nulls are not allowed with RS") public void mergeJustNull() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(0); Flowable.range(1, 2).flatMap(new Function<Integer, Flowable<Integer>>() { @Override public Flowable<Integer> apply(Integer t) { return Flowable.just(null); } }).subscribe(ts); ts.request(2); ts.assertValues(null, null); ts.assertNoErrors(); ts.assertComplete(); } @Test public void mergeConcurrentJustJust() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.merge(Flowable.just(Flowable.just(1)), 5).subscribe(ts); ts.assertValue(1); ts.assertNoErrors(); ts.assertComplete(); } @Test public void mergeConcurrentJustRange() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.merge(Flowable.just(Flowable.range(1, 5)), 5).subscribe(ts); ts.assertValues(1, 2, 3, 4, 5); ts.assertNoErrors(); ts.assertComplete(); } @SuppressWarnings("unchecked") @Test @Ignore("No 2-9 argument merge()") public void mergeMany() throws Exception { for (int i = 2; i < 10; i++) { Class<?>[] clazz = new Class[i]; Arrays.fill(clazz, Flowable.class); Flowable<Integer>[] obs = new Flowable[i]; Arrays.fill(obs, Flowable.just(1)); Integer[] expected = new Integer[i]; Arrays.fill(expected, 1); Method m = Flowable.class.getMethod("merge", clazz); TestSubscriber<Integer> ts = TestSubscriber.create(); ((Flowable<Integer>)m.invoke(null, (Object[])obs)).subscribe(ts); ts.assertValues(expected); ts.assertNoErrors(); ts.assertComplete(); } } @SuppressWarnings("unchecked") @Test public void mergeArrayMaxConcurrent() { TestSubscriber<Integer> ts = TestSubscriber.create(); PublishProcessor<Integer> ps1 = PublishProcessor.create(); PublishProcessor<Integer> ps2 = PublishProcessor.create(); Flowable.mergeArray(1, 128, new Flowable[] { ps1, ps2 }).subscribe(ts); assertTrue("ps1 has no subscribers?!", ps1.hasSubscribers()); assertFalse("ps2 has subscribers?!", ps2.hasSubscribers()); ps1.onNext(1); ps1.onComplete(); assertFalse("ps1 has subscribers?!", ps1.hasSubscribers()); assertTrue("ps2 has no subscribers?!", ps2.hasSubscribers()); ps2.onNext(2); ps2.onComplete(); ts.assertValues(1, 2); ts.assertNoErrors(); ts.assertComplete(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void flatMapJustJust() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.just(Flowable.just(1)).flatMap((Function)Functions.identity()).subscribe(ts); ts.assertValue(1); ts.assertNoErrors(); ts.assertComplete(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void flatMapJustRange() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.just(Flowable.range(1, 5)).flatMap((Function)Functions.identity()).subscribe(ts); ts.assertValues(1, 2, 3, 4, 5); ts.assertNoErrors(); ts.assertComplete(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void flatMapMaxConcurrentJustJust() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.just(Flowable.just(1)).flatMap((Function)Functions.identity(), 5).subscribe(ts); ts.assertValue(1); ts.assertNoErrors(); ts.assertComplete(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void flatMapMaxConcurrentJustRange() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.just(Flowable.range(1, 5)).flatMap((Function)Functions.identity(), 5).subscribe(ts); ts.assertValues(1, 2, 3, 4, 5); ts.assertNoErrors(); ts.assertComplete(); } @Test public void noInnerReordering() { TestSubscriber<Integer> ts = TestSubscriber.create(0); FlowableFlatMap.MergeSubscriber<Publisher<Integer>, Integer> ms = new FlowableFlatMap.MergeSubscriber<Publisher<Integer>, Integer>(ts, Functions.<Publisher<Integer>>identity(), false, 128, 128); ms.onSubscribe(new BooleanSubscription()); PublishProcessor<Integer> ps = PublishProcessor.create(); ms.onNext(ps); ps.onNext(1); BackpressureHelper.add(ms.requested, 2); ps.onNext(2); ms.drain(); ts.assertValues(1, 2); } @Test public void noOuterScalarReordering() { TestSubscriber<Integer> ts = TestSubscriber.create(0); FlowableFlatMap.MergeSubscriber<Publisher<Integer>, Integer> ms = new FlowableFlatMap.MergeSubscriber<Publisher<Integer>, Integer>(ts, Functions.<Publisher<Integer>>identity(), false, 128, 128); ms.onSubscribe(new BooleanSubscription()); ms.onNext(Flowable.just(1)); BackpressureHelper.add(ms.requested, 2); ms.onNext(Flowable.just(2)); ms.drain(); ts.assertValues(1, 2); } @Test public void array() { for (int i = 1; i < 100; i++) { @SuppressWarnings("unchecked") Flowable<Integer>[] sources = new Flowable[i]; Arrays.fill(sources, Flowable.just(1)); Integer[] expected = new Integer[i]; for (int j = 0; j < i; j++) { expected[j] = 1; } Flowable.mergeArray(sources) .test() .assertResult(expected); } } @SuppressWarnings("unchecked") @Test public void mergeArray() { Flowable.mergeArray(Flowable.just(1), Flowable.just(2)) .test() .assertResult(1, 2); } @Test public void mergeErrors() { List<Throwable> errors = TestHelper.trackPluginErrors(); try { Flowable<Integer> source1 = Flowable.error(new TestException("First")); Flowable<Integer> source2 = Flowable.error(new TestException("Second")); Flowable.merge(source1, source2) .test() .assertFailureAndMessage(TestException.class, "First"); assertTrue(errors.toString(), errors.isEmpty()); } finally { RxJavaPlugins.reset(); } } }