/**
* Copyright 2014 Netflix, Inc.
*
* 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 rx.internal.operators;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observer;
import rx.Subscriber;
import rx.exceptions.CompositeException;
import rx.exceptions.TestException;
import rx.observers.TestSubscriber;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.*;
public class OperatorMergeDelayErrorTest {
@Mock
Observer<String> stringObserver;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testErrorDelayed1() {
final Observable<String> o1 = Observable.create(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.create(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()).onCompleted();
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
verify(stringObserver, times(1)).onNext("six");
}
@Test
public void testErrorDelayed2() {
final Observable<String> o1 = Observable.create(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.create(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.create(new TestErrorObservable("seven", "eight", null));
final Observable<String> o4 = Observable.create(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()).onCompleted();
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
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.create(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.create(new TestErrorObservable("four", "five", "six"));
final Observable<String> o3 = Observable.create(new TestErrorObservable("seven", "eight", null));
final Observable<String> o4 = Observable.create(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()).onCompleted();
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.create(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.create(new TestErrorObservable("four", "five", "six"));
final Observable<String> o3 = Observable.create(new TestErrorObservable("seven", "eight"));
final Observable<String> o4 = Observable.create(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()).onCompleted();
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.create(o1), Observable.create(o2), Observable.create(o3), Observable.create(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()).onCompleted();
}
@Test
public void testCompositeErrorDelayed1() {
final Observable<String> o1 = Observable.create(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.create(new TestErrorObservable("one", "two", null));
Observable<String> m = Observable.mergeDelayError(o1, o2);
m.subscribe(stringObserver);
verify(stringObserver, times(1)).onError(any(CompositeException.class));
verify(stringObserver, never()).onCompleted();
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
verify(stringObserver, times(1)).onNext("six");
}
@Test
public void testCompositeErrorDelayed2() {
final Observable<String> o1 = Observable.create(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.create(new TestErrorObservable("one", "two", null));
Observable<String> m = Observable.mergeDelayError(o1, o2);
CaptureObserver w = new CaptureObserver();
m.subscribe(w);
assertNotNull(w.e);
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.create(new TestSynchronousObservable());
final Observable<String> o2 = Observable.create(new TestSynchronousObservable());
Observable<Observable<String>> observableOfObservables = Observable.create(new Observable.OnSubscribe<Observable<String>>() {
@Override
public void call(Subscriber<? super Observable<String>> observer) {
// simulate what would happen in an observable
observer.onNext(o1);
observer.onNext(o2);
observer.onCompleted();
}
});
Observable<String> m = Observable.mergeDelayError(observableOfObservables);
m.subscribe(stringObserver);
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(1)).onCompleted();
verify(stringObserver, times(2)).onNext("hello");
}
@Test
public void testMergeArray() {
final Observable<String> o1 = Observable.create(new TestSynchronousObservable());
final Observable<String> o2 = Observable.create(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)).onCompleted();
}
@Test
public void testMergeList() {
final Observable<String> o1 = Observable.create(new TestSynchronousObservable());
final Observable<String> o2 = Observable.create(new TestSynchronousObservable());
List<Observable<String>> listOfObservables = new ArrayList<Observable<String>>();
listOfObservables.add(o1);
listOfObservables.add(o2);
Observable<String> m = Observable.mergeDelayError(Observable.from(listOfObservables));
m.subscribe(stringObserver);
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(1)).onCompleted();
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.create(o1), Observable.create(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)).onCompleted();
}
@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 Subscriber<String>() {
@Override
public void onCompleted() {
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 Observable.OnSubscribe<String> {
@Override
public void call(Subscriber<? super String> observer) {
observer.onNext("hello");
observer.onCompleted();
}
}
private static class TestASynchronousObservable implements Observable.OnSubscribe<String> {
Thread t;
@Override
public void call(final Subscriber<? super String> observer) {
t = new Thread(new Runnable() {
@Override
public void run() {
observer.onNext("hello");
observer.onCompleted();
}
});
t.start();
}
}
private static class TestErrorObservable implements Observable.OnSubscribe<String> {
String[] valuesToReturn;
TestErrorObservable(String... values) {
valuesToReturn = values;
}
@Override
public void call(Subscriber<? super String> observer) {
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.onCompleted();
}
}
}
private static class TestAsyncErrorObservable implements Observable.OnSubscribe<String> {
String[] valuesToReturn;
TestAsyncErrorObservable(String... values) {
valuesToReturn = values;
}
Thread t;
@Override
public void call(final Subscriber<? super String> observer) {
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.onCompleted();
}
});
t.start();
}
}
private static class CaptureObserver implements Observer<String> {
volatile Throwable e;
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
this.e = e;
}
@Override
public void onNext(String args) {
}
}
@Test
public void testMergeSourceWhichDoesntPropagateExceptionBack() {
Observable<Integer> source = Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> t1) {
try {
t1.onNext(0);
} catch (Throwable swallow) {
}
t1.onNext(1);
t1.onCompleted();
}
});
Observable<Integer> result = Observable.mergeDelayError(source, Observable.just(2));
@SuppressWarnings("unchecked")
final Observer<Integer> o = mock(Observer.class);
InOrder inOrder = inOrder(o);
result.unsafeSubscribe(new Subscriber<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 onCompleted() {
o.onCompleted();
}
});
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()).onCompleted();
}
@Test
public void testErrorInParentObservable() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable.mergeDelayError(
Observable.just(Observable.just(1), Observable.just(2))
.startWith(Observable.<Integer> error(new RuntimeException()))
).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertTerminalEvent();
ts.assertReceivedOnNext(Arrays.asList(1, 2));
assertEquals(1, ts.getOnErrorEvents().size());
}
@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.create(new Observable.OnSubscribe<Observable<String>>() {
@Override
public void call(Subscriber<? super Observable<String>> op) {
op.onNext(Observable.create(o1));
op.onNext(Observable.create(o2));
op.onError(new NullPointerException("throwing exception in parent"));
}
});
@SuppressWarnings("unchecked")
Observer<String> stringObserver = mock(Observer.class);
TestSubscriber<String> ts = new TestSubscriber<String>(stringObserver);
Observable<String> m = Observable.mergeDelayError(parentObservable);
m.subscribe(ts);
System.out.println("testErrorInParentObservableDelayed | " + i);
ts.awaitTerminalEvent(2000, TimeUnit.MILLISECONDS);
ts.assertTerminalEvent();
verify(stringObserver, times(2)).onNext("hello");
verify(stringObserver, times(1)).onError(any(NullPointerException.class));
verify(stringObserver, never()).onCompleted();
}
}
private static class TestASynchronous1sDelayedObservable implements Observable.OnSubscribe<String> {
Thread t;
@Override
public void call(final Subscriber<? super String> observer) {
t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
observer.onError(e);
}
observer.onNext("hello");
observer.onCompleted();
}
});
t.start();
}
}
}