/**
* 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.observable;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.*;
import java.util.concurrent.*;
import org.junit.*;
import org.mockito.InOrder;
import io.reactivex.*;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposables;
import io.reactivex.exceptions.*;
import io.reactivex.observers.*;
public class ObservableMergeDelayErrorTest {
Observer<String> stringObserver;
@Before
public void before() {
stringObserver = TestHelper.mockObserver();
}
@Test
public void testErrorDelayed1() {
final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called
final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three"));
Observable<String> m = Observable.mergeDelayError(o1, o2);
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");
// despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError
// inner Observable errors are considered terminal for that source
// verify(stringObserver, times(1)).onNext("six");
// inner Observable errors are considered terminal for that source
}
@Test
public void testErrorDelayed2() {
final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called
final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight", null));
final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine"));
Observable<String> m = Observable.mergeDelayError(o1, o2, o3, o4);
m.subscribe(stringObserver);
verify(stringObserver, times(1)).onError(any(CompositeException.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");
// despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError
// inner Observable errors are considered terminal for that source
// verify(stringObserver, times(1)).onNext("six");
verify(stringObserver, times(1)).onNext("seven");
verify(stringObserver, times(1)).onNext("eight");
verify(stringObserver, times(1)).onNext("nine");
}
@Test
public void testErrorDelayed3() {
final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", "five", "six"));
final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight", null));
final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine"));
Observable<String> m = Observable.mergeDelayError(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(1)).onNext("five");
verify(stringObserver, times(1)).onNext("six");
verify(stringObserver, times(1)).onNext("seven");
verify(stringObserver, times(1)).onNext("eight");
verify(stringObserver, times(1)).onNext("nine");
}
@Test
public void testErrorDelayed4() {
final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", "five", "six"));
final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight"));
final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine", null));
Observable<String> m = Observable.mergeDelayError(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(1)).onNext("five");
verify(stringObserver, times(1)).onNext("six");
verify(stringObserver, times(1)).onNext("seven");
verify(stringObserver, times(1)).onNext("eight");
verify(stringObserver, times(1)).onNext("nine");
}
@Test
public void testErrorDelayed4WithThreading() {
final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three");
final TestAsyncErrorObservable o2 = new TestAsyncErrorObservable("four", "five", "six");
final TestAsyncErrorObservable o3 = new TestAsyncErrorObservable("seven", "eight");
// throw the error at the very end so no onComplete will be called after it
final TestAsyncErrorObservable o4 = new TestAsyncErrorObservable("nine", null);
Observable<String> m = Observable.mergeDelayError(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2), Observable.unsafeCreate(o3), Observable.unsafeCreate(o4));
m.subscribe(stringObserver);
try {
o1.t.join();
o2.t.join();
o3.t.join();
o4.t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
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(1)).onNext("five");
verify(stringObserver, times(1)).onNext("six");
verify(stringObserver, times(1)).onNext("seven");
verify(stringObserver, times(1)).onNext("eight");
verify(stringObserver, times(1)).onNext("nine");
verify(stringObserver, times(1)).onError(any(NullPointerException.class));
verify(stringObserver, never()).onComplete();
}
@Test
public void testCompositeErrorDelayed1() {
final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called
final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", null));
Observable<String> m = Observable.mergeDelayError(o1, o2);
m.subscribe(stringObserver);
verify(stringObserver, times(1)).onError(any(Throwable.class));
verify(stringObserver, never()).onComplete();
verify(stringObserver, times(1)).onNext("one");
verify(stringObserver, times(1)).onNext("two");
verify(stringObserver, times(0)).onNext("three");
verify(stringObserver, times(1)).onNext("four");
verify(stringObserver, times(0)).onNext("five");
// despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError
// inner Observable errors are considered terminal for that source
// verify(stringObserver, times(1)).onNext("six");
}
@Test
public void testCompositeErrorDelayed2() {
final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called
final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", null));
Observable<String> m = Observable.mergeDelayError(o1, o2);
CaptureObserver w = new CaptureObserver();
m.subscribe(w);
assertNotNull(w.e);
assertEquals(2, ((CompositeException)w.e).size());
// if (w.e instanceof CompositeException) {
// assertEquals(2, ((CompositeException) w.e).getExceptions().size());
// w.e.printStackTrace();
// } else {
// fail("Expecting CompositeException");
// }
}
/**
* The unit tests below are from OperationMerge and should ensure the normal merge functionality is correct.
*/
@Test
public void testMergeObservableOfObservables() {
final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable());
final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable());
Observable<Observable<String>> observableOfObservables = Observable.unsafeCreate(new ObservableSource<Observable<String>>() {
@Override
public void subscribe(Observer<? super Observable<String>> observer) {
observer.onSubscribe(Disposables.empty());
// simulate what would happen in an Observable
observer.onNext(o1);
observer.onNext(o2);
observer.onComplete();
}
});
Observable<String> m = Observable.mergeDelayError(observableOfObservables);
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 Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable());
final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable());
Observable<String> m = Observable.mergeDelayError(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 Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable());
final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable());
List<Observable<String>> listOfObservables = new ArrayList<Observable<String>>();
listOfObservables.add(o1);
listOfObservables.add(o2);
Observable<String> m = Observable.mergeDelayError(Observable.fromIterable(listOfObservables));
m.subscribe(stringObserver);
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(1)).onComplete();
verify(stringObserver, times(2)).onNext("hello");
}
@Test
public void testMergeArrayWithThreading() {
final TestASynchronousObservable o1 = new TestASynchronousObservable();
final TestASynchronousObservable o2 = new TestASynchronousObservable();
Observable<String> m = Observable.mergeDelayError(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2));
m.subscribe(stringObserver);
try {
o1.t.join();
o2.t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(2)).onNext("hello");
verify(stringObserver, times(1)).onComplete();
}
@Test(timeout = 1000L)
public void testSynchronousError() {
final Observable<Observable<String>> o1 = Observable.error(new RuntimeException("unit test"));
final CountDownLatch latch = new CountDownLatch(1);
Observable.mergeDelayError(o1).subscribe(new DefaultObserver<String>() {
@Override
public void onComplete() {
fail("Expected onError path");
}
@Override
public void onError(Throwable e) {
latch.countDown();
}
@Override
public void onNext(String s) {
fail("Expected onError path");
}
});
try {
latch.await();
} catch (InterruptedException ex) {
fail("interrupted");
}
}
private static class TestSynchronousObservable implements ObservableSource<String> {
@Override
public void subscribe(Observer<? super String> observer) {
observer.onSubscribe(Disposables.empty());
observer.onNext("hello");
observer.onComplete();
}
}
private static class TestASynchronousObservable implements ObservableSource<String> {
Thread t;
@Override
public void subscribe(final Observer<? super String> observer) {
observer.onSubscribe(Disposables.empty());
t = new Thread(new Runnable() {
@Override
public void run() {
observer.onNext("hello");
observer.onComplete();
}
});
t.start();
}
}
private static class TestErrorObservable implements ObservableSource<String> {
String[] valuesToReturn;
TestErrorObservable(String... values) {
valuesToReturn = values;
}
@Override
public void subscribe(Observer<? super String> observer) {
observer.onSubscribe(Disposables.empty());
boolean errorThrown = false;
for (String s : valuesToReturn) {
if (s == null) {
System.out.println("throwing exception");
observer.onError(new NullPointerException());
errorThrown = true;
// purposefully not returning here so it will continue calling onNext
// so that we also test that we handle bad sequences like this
} else {
observer.onNext(s);
}
}
if (!errorThrown) {
observer.onComplete();
}
}
}
private static class TestAsyncErrorObservable implements ObservableSource<String> {
String[] valuesToReturn;
TestAsyncErrorObservable(String... values) {
valuesToReturn = values;
}
Thread t;
@Override
public void subscribe(final Observer<? super String> observer) {
observer.onSubscribe(Disposables.empty());
t = new Thread(new Runnable() {
@Override
public void run() {
for (String s : valuesToReturn) {
if (s == null) {
System.out.println("throwing exception");
try {
Thread.sleep(100);
} catch (Throwable e) {
}
observer.onError(new NullPointerException());
return;
} else {
observer.onNext(s);
}
}
System.out.println("subscription complete");
observer.onComplete();
}
});
t.start();
}
}
private static class CaptureObserver extends DefaultObserver<String> {
volatile Throwable e;
@Override
public void onComplete() {
}
@Override
public void onError(Throwable e) {
this.e = e;
}
@Override
public void onNext(String args) {
}
}
@Test
@Ignore("Subscribers should not throw")
public void testMergeSourceWhichDoesntPropagateExceptionBack() {
Observable<Integer> source = Observable.unsafeCreate(new ObservableSource<Integer>() {
@Override
public void subscribe(Observer<? super Integer> t1) {
t1.onSubscribe(Disposables.empty());
try {
t1.onNext(0);
} catch (Throwable swallow) {
}
t1.onNext(1);
t1.onComplete();
}
});
Observable<Integer> result = Observable.mergeDelayError(source, Observable.just(2));
final Observer<Integer> o = TestHelper.mockObserver();
InOrder inOrder = inOrder(o);
result.subscribe(new DefaultObserver<Integer>() {
int calls;
@Override
public void onNext(Integer t) {
if (calls++ == 0) {
throw new TestException();
}
o.onNext(t);
}
@Override
public void onError(Throwable e) {
o.onError(e);
}
@Override
public void onComplete() {
o.onComplete();
}
});
/*
* If the child onNext throws, why would we keep accepting values from
* other sources?
*/
inOrder.verify(o).onNext(2);
inOrder.verify(o, never()).onNext(0);
inOrder.verify(o, never()).onNext(1);
inOrder.verify(o, never()).onNext(anyInt());
inOrder.verify(o).onError(any(TestException.class));
verify(o, never()).onComplete();
}
@Test
public void testErrorInParentObservable() {
TestObserver<Integer> ts = new TestObserver<Integer>();
Observable.mergeDelayError(
Observable.just(Observable.just(1), Observable.just(2))
.startWith(Observable.<Integer> error(new RuntimeException()))
).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertTerminated();
ts.assertValues(1, 2);
assertEquals(1, ts.errorCount());
}
@Test
public void testErrorInParentObservableDelayed() throws Exception {
for (int i = 0; i < 50; i++) {
final TestASynchronous1sDelayedObservable o1 = new TestASynchronous1sDelayedObservable();
final TestASynchronous1sDelayedObservable o2 = new TestASynchronous1sDelayedObservable();
Observable<Observable<String>> parentObservable = Observable.unsafeCreate(new ObservableSource<Observable<String>>() {
@Override
public void subscribe(Observer<? super Observable<String>> op) {
op.onSubscribe(Disposables.empty());
op.onNext(Observable.unsafeCreate(o1));
op.onNext(Observable.unsafeCreate(o2));
op.onError(new NullPointerException("throwing exception in parent"));
}
});
Observer<String> stringObserver = TestHelper.mockObserver();
TestObserver<String> ts = new TestObserver<String>(stringObserver);
Observable<String> m = Observable.mergeDelayError(parentObservable);
m.subscribe(ts);
System.out.println("testErrorInParentObservableDelayed | " + i);
ts.awaitTerminalEvent(2000, TimeUnit.MILLISECONDS);
ts.assertTerminated();
verify(stringObserver, times(2)).onNext("hello");
verify(stringObserver, times(1)).onError(any(NullPointerException.class));
verify(stringObserver, never()).onComplete();
}
}
private static class TestASynchronous1sDelayedObservable implements ObservableSource<String> {
Thread t;
@Override
public void subscribe(final Observer<? super String> observer) {
observer.onSubscribe(Disposables.empty());
t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
observer.onError(e);
}
observer.onNext("hello");
observer.onComplete();
}
});
t.start();
}
}
@SuppressWarnings("unchecked")
@Test
public void mergeIterableDelayError() {
Observable.mergeDelayError(Arrays.asList(Observable.just(1), Observable.just(2)))
.test()
.assertResult(1, 2);
}
@SuppressWarnings("unchecked")
@Test
public void mergeArrayDelayError() {
Observable.mergeArrayDelayError(Observable.just(1), Observable.just(2))
.test()
.assertResult(1, 2);
}
@SuppressWarnings("unchecked")
@Test
public void mergeIterableDelayErrorWithError() {
Observable.mergeDelayError(
Arrays.asList(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())),
Observable.just(2)))
.test()
.assertFailure(TestException.class, 1, 2);
}
@Test
public void mergeDelayError() {
Observable.mergeDelayError(
Observable.just(Observable.just(1),
Observable.just(2)))
.test()
.assertResult(1, 2);
}
@Test
public void mergeDelayErrorWithError() {
Observable.mergeDelayError(
Observable.just(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())),
Observable.just(2)))
.test()
.assertFailure(TestException.class, 1, 2);
}
@Test
public void mergeDelayErrorMaxConcurrency() {
Observable.mergeDelayError(
Observable.just(Observable.just(1),
Observable.just(2)), 1)
.test()
.assertResult(1, 2);
}
@Test
public void mergeDelayErrorWithErrorMaxConcurrency() {
Observable.mergeDelayError(
Observable.just(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())),
Observable.just(2)), 1)
.test()
.assertFailure(TestException.class, 1, 2);
}
@SuppressWarnings("unchecked")
@Test
public void mergeIterableDelayErrorMaxConcurrency() {
Observable.mergeDelayError(
Arrays.asList(Observable.just(1),
Observable.just(2)), 1)
.test()
.assertResult(1, 2);
}
@SuppressWarnings("unchecked")
@Test
public void mergeIterableDelayErrorWithErrorMaxConcurrency() {
Observable.mergeDelayError(
Arrays.asList(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())),
Observable.just(2)), 1)
.test()
.assertFailure(TestException.class, 1, 2);
}
@Test
public void mergeDelayError3() {
Observable.mergeDelayError(
Observable.just(1),
Observable.just(2),
Observable.just(3)
)
.test()
.assertResult(1, 2, 3);
}
@Test
public void mergeDelayError3WithError() {
Observable.mergeDelayError(
Observable.just(1),
Observable.just(2).concatWith(Observable.<Integer>error(new TestException())),
Observable.just(3)
)
.test()
.assertFailure(TestException.class, 1, 2, 3);
}
}