/**
* 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.mockito.InOrder;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observer;
import rx.Subscriber;
import rx.Subscription;
import rx.internal.util.RxRingBuffer;
import rx.observers.TestSubscriber;
import rx.schedulers.Schedulers;
import rx.schedulers.TestScheduler;
import rx.subscriptions.BooleanSubscription;
public class OperatorConcatTest {
@Test
public void testConcat() {
@SuppressWarnings("unchecked")
Observer<String> observer = mock(Observer.class);
final String[] o = { "1", "3", "5", "7" };
final String[] e = { "2", "4", "6" };
final Observable<String> odds = Observable.from(o);
final Observable<String> even = Observable.from(e);
Observable<String> concat = Observable.concat(odds, even);
concat.subscribe(observer);
verify(observer, times(7)).onNext(anyString());
}
@Test
public void testConcatWithList() {
@SuppressWarnings("unchecked")
Observer<String> observer = mock(Observer.class);
final String[] o = { "1", "3", "5", "7" };
final String[] e = { "2", "4", "6" };
final Observable<String> odds = Observable.from(o);
final Observable<String> even = Observable.from(e);
final List<Observable<String>> list = new ArrayList<Observable<String>>();
list.add(odds);
list.add(even);
Observable<String> concat = Observable.concat(Observable.from(list));
concat.subscribe(observer);
verify(observer, times(7)).onNext(anyString());
}
@Test
public void testConcatObservableOfObservables() {
@SuppressWarnings("unchecked")
Observer<String> observer = mock(Observer.class);
final String[] o = { "1", "3", "5", "7" };
final String[] e = { "2", "4", "6" };
final Observable<String> odds = Observable.from(o);
final Observable<String> even = Observable.from(e);
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(odds);
observer.onNext(even);
observer.onCompleted();
}
});
Observable<String> concat = Observable.concat(observableOfObservables);
concat.subscribe(observer);
verify(observer, times(7)).onNext(anyString());
}
/**
* Simple concat of 2 asynchronous observables ensuring it emits in correct order.
*/
@SuppressWarnings("unchecked")
@Test
public void testSimpleAsyncConcat() {
Observer<String> observer = mock(Observer.class);
TestObservable<String> o1 = new TestObservable<String>("one", "two", "three");
TestObservable<String> o2 = new TestObservable<String>("four", "five", "six");
Observable.concat(Observable.create(o1), Observable.create(o2)).subscribe(observer);
try {
// wait for async observables to complete
o1.t.join();
o2.t.join();
} catch (Throwable e) {
throw new RuntimeException("failed waiting on threads");
}
InOrder inOrder = inOrder(observer);
inOrder.verify(observer, times(1)).onNext("one");
inOrder.verify(observer, times(1)).onNext("two");
inOrder.verify(observer, times(1)).onNext("three");
inOrder.verify(observer, times(1)).onNext("four");
inOrder.verify(observer, times(1)).onNext("five");
inOrder.verify(observer, times(1)).onNext("six");
}
/**
* Test an async Observable that emits more async Observables
*/
@SuppressWarnings("unchecked")
@Test
public void testNestedAsyncConcat() throws Throwable {
Observer<String> observer = mock(Observer.class);
final TestObservable<String> o1 = new TestObservable<String>("one", "two", "three");
final TestObservable<String> o2 = new TestObservable<String>("four", "five", "six");
final TestObservable<String> o3 = new TestObservable<String>("seven", "eight", "nine");
final CountDownLatch allowThird = new CountDownLatch(1);
final AtomicReference<Thread> parent = new AtomicReference<Thread>();
final CountDownLatch parentHasStarted = new CountDownLatch(1);
Observable<Observable<String>> observableOfObservables = Observable.create(new Observable.OnSubscribe<Observable<String>>() {
@Override
public void call(final Subscriber<? super Observable<String>> observer) {
final BooleanSubscription s = new BooleanSubscription();
observer.add(s);
parent.set(new Thread(new Runnable() {
@Override
public void run() {
try {
// emit first
if (!s.isUnsubscribed()) {
System.out.println("Emit o1");
observer.onNext(Observable.create(o1));
}
// emit second
if (!s.isUnsubscribed()) {
System.out.println("Emit o2");
observer.onNext(Observable.create(o2));
}
// wait until sometime later and emit third
try {
allowThird.await();
} catch (InterruptedException e) {
observer.onError(e);
}
if (!s.isUnsubscribed()) {
System.out.println("Emit o3");
observer.onNext(Observable.create(o3));
}
} catch (Throwable e) {
observer.onError(e);
} finally {
System.out.println("Done parent Observable");
observer.onCompleted();
}
}
}));
parent.get().start();
parentHasStarted.countDown();
}
});
Observable.concat(observableOfObservables).subscribe(observer);
// wait for parent to start
parentHasStarted.await();
try {
// wait for first 2 async observables to complete
System.out.println("Thread1 is starting ... waiting for it to complete ...");
o1.waitForThreadDone();
System.out.println("Thread2 is starting ... waiting for it to complete ...");
o2.waitForThreadDone();
} catch (Throwable e) {
throw new RuntimeException("failed waiting on threads", e);
}
InOrder inOrder = inOrder(observer);
inOrder.verify(observer, times(1)).onNext("one");
inOrder.verify(observer, times(1)).onNext("two");
inOrder.verify(observer, times(1)).onNext("three");
inOrder.verify(observer, times(1)).onNext("four");
inOrder.verify(observer, times(1)).onNext("five");
inOrder.verify(observer, times(1)).onNext("six");
// we shouldn't have the following 3 yet
inOrder.verify(observer, never()).onNext("seven");
inOrder.verify(observer, never()).onNext("eight");
inOrder.verify(observer, never()).onNext("nine");
// we should not be completed yet
verify(observer, never()).onCompleted();
verify(observer, never()).onError(any(Throwable.class));
// now allow the third
allowThird.countDown();
try {
// wait for 3rd to complete
o3.waitForThreadDone();
} catch (Throwable e) {
throw new RuntimeException("failed waiting on threads", e);
}
inOrder.verify(observer, times(1)).onNext("seven");
inOrder.verify(observer, times(1)).onNext("eight");
inOrder.verify(observer, times(1)).onNext("nine");
inOrder.verify(observer, times(1)).onCompleted();
verify(observer, never()).onError(any(Throwable.class));
}
@SuppressWarnings("unchecked")
@Test
public void testBlockedObservableOfObservables() {
Observer<String> observer = mock(Observer.class);
final String[] o = { "1", "3", "5", "7" };
final String[] e = { "2", "4", "6" };
final Observable<String> odds = Observable.from(o);
final Observable<String> even = Observable.from(e);
final CountDownLatch callOnce = new CountDownLatch(1);
final CountDownLatch okToContinue = new CountDownLatch(1);
TestObservable<Observable<String>> observableOfObservables = new TestObservable<Observable<String>>(callOnce, okToContinue, odds, even);
Observable<String> concatF = Observable.concat(Observable.create(observableOfObservables));
concatF.subscribe(observer);
try {
//Block main thread to allow observables to serve up o1.
callOnce.await();
} catch (Throwable ex) {
ex.printStackTrace();
fail(ex.getMessage());
}
// The concated observable should have served up all of the odds.
verify(observer, times(1)).onNext("1");
verify(observer, times(1)).onNext("3");
verify(observer, times(1)).onNext("5");
verify(observer, times(1)).onNext("7");
try {
// unblock observables so it can serve up o2 and complete
okToContinue.countDown();
observableOfObservables.t.join();
} catch (Throwable ex) {
ex.printStackTrace();
fail(ex.getMessage());
}
// The concatenated observable should now have served up all the evens.
verify(observer, times(1)).onNext("2");
verify(observer, times(1)).onNext("4");
verify(observer, times(1)).onNext("6");
}
@Test
public void testConcatConcurrentWithInfinity() {
final TestObservable<String> w1 = new TestObservable<String>("one", "two", "three");
//This observable will send "hello" MAX_VALUE time.
final TestObservable<String> w2 = new TestObservable<String>("hello", Integer.MAX_VALUE);
@SuppressWarnings("unchecked")
Observer<String> observer = mock(Observer.class);
@SuppressWarnings("unchecked")
TestObservable<Observable<String>> observableOfObservables = new TestObservable<Observable<String>>(Observable.create(w1), Observable.create(w2));
Observable<String> concatF = Observable.concat(Observable.create(observableOfObservables));
concatF.take(50).subscribe(observer);
//Wait for the thread to start up.
try {
w1.waitForThreadDone();
w2.waitForThreadDone();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
InOrder inOrder = inOrder(observer);
inOrder.verify(observer, times(1)).onNext("one");
inOrder.verify(observer, times(1)).onNext("two");
inOrder.verify(observer, times(1)).onNext("three");
inOrder.verify(observer, times(47)).onNext("hello");
verify(observer, times(1)).onCompleted();
verify(observer, never()).onError(any(Throwable.class));
}
@Test
public void testConcatNonBlockingObservables() {
final CountDownLatch okToContinueW1 = new CountDownLatch(1);
final CountDownLatch okToContinueW2 = new CountDownLatch(1);
final TestObservable<String> w1 = new TestObservable<String>(null, okToContinueW1, "one", "two", "three");
final TestObservable<String> w2 = new TestObservable<String>(null, okToContinueW2, "four", "five", "six");
@SuppressWarnings("unchecked")
Observer<String> observer = mock(Observer.class);
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(Observable.create(w1));
observer.onNext(Observable.create(w2));
observer.onCompleted();
}
});
Observable<String> concat = Observable.concat(observableOfObservables);
concat.subscribe(observer);
verify(observer, times(0)).onCompleted();
try {
// release both threads
okToContinueW1.countDown();
okToContinueW2.countDown();
// wait for both to finish
w1.t.join();
w2.t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
InOrder inOrder = inOrder(observer);
inOrder.verify(observer, times(1)).onNext("one");
inOrder.verify(observer, times(1)).onNext("two");
inOrder.verify(observer, times(1)).onNext("three");
inOrder.verify(observer, times(1)).onNext("four");
inOrder.verify(observer, times(1)).onNext("five");
inOrder.verify(observer, times(1)).onNext("six");
verify(observer, times(1)).onCompleted();
}
/**
* Test unsubscribing the concatenated Observable in a single thread.
*/
@Test
public void testConcatUnsubscribe() {
final CountDownLatch callOnce = new CountDownLatch(1);
final CountDownLatch okToContinue = new CountDownLatch(1);
final TestObservable<String> w1 = new TestObservable<String>("one", "two", "three");
final TestObservable<String> w2 = new TestObservable<String>(callOnce, okToContinue, "four", "five", "six");
@SuppressWarnings("unchecked")
final Observer<String> observer = mock(Observer.class);
final Observable<String> concat = Observable.concat(Observable.create(w1), Observable.create(w2));
try {
// Subscribe
Subscription s1 = concat.subscribe(observer);
//Block main thread to allow observable "w1" to complete and observable "w2" to call onNext once.
callOnce.await();
// Unsubcribe
s1.unsubscribe();
//Unblock the observable to continue.
okToContinue.countDown();
w1.t.join();
w2.t.join();
} catch (Throwable e) {
e.printStackTrace();
fail(e.getMessage());
}
InOrder inOrder = inOrder(observer);
inOrder.verify(observer, times(1)).onNext("one");
inOrder.verify(observer, times(1)).onNext("two");
inOrder.verify(observer, times(1)).onNext("three");
inOrder.verify(observer, times(1)).onNext("four");
inOrder.verify(observer, never()).onNext("five");
inOrder.verify(observer, never()).onNext("six");
inOrder.verify(observer, never()).onCompleted();
}
/**
* All observables will be running in different threads so subscribe() is unblocked. CountDownLatch is only used in order to call unsubscribe() in a predictable manner.
*/
@Test
public void testConcatUnsubscribeConcurrent() {
final CountDownLatch callOnce = new CountDownLatch(1);
final CountDownLatch okToContinue = new CountDownLatch(1);
final TestObservable<String> w1 = new TestObservable<String>("one", "two", "three");
final TestObservable<String> w2 = new TestObservable<String>(callOnce, okToContinue, "four", "five", "six");
@SuppressWarnings("unchecked")
Observer<String> observer = mock(Observer.class);
@SuppressWarnings("unchecked")
TestObservable<Observable<String>> observableOfObservables = new TestObservable<Observable<String>>(Observable.create(w1), Observable.create(w2));
Observable<String> concatF = Observable.concat(Observable.create(observableOfObservables));
Subscription s1 = concatF.subscribe(observer);
try {
//Block main thread to allow observable "w1" to complete and observable "w2" to call onNext exactly once.
callOnce.await();
//"four" from w2 has been processed by onNext()
s1.unsubscribe();
//"five" and "six" will NOT be processed by onNext()
//Unblock the observable to continue.
okToContinue.countDown();
w1.t.join();
w2.t.join();
} catch (Throwable e) {
e.printStackTrace();
fail(e.getMessage());
}
InOrder inOrder = inOrder(observer);
inOrder.verify(observer, times(1)).onNext("one");
inOrder.verify(observer, times(1)).onNext("two");
inOrder.verify(observer, times(1)).onNext("three");
inOrder.verify(observer, times(1)).onNext("four");
inOrder.verify(observer, never()).onNext("five");
inOrder.verify(observer, never()).onNext("six");
verify(observer, never()).onCompleted();
verify(observer, never()).onError(any(Throwable.class));
}
private static class TestObservable<T> implements Observable.OnSubscribe<T> {
private final Subscription s = new Subscription() {
@Override
public void unsubscribe() {
subscribed = false;
}
@Override
public boolean isUnsubscribed() {
return !subscribed;
}
};
private final List<T> values;
private Thread t = null;
private int count = 0;
private boolean subscribed = true;
private final CountDownLatch once;
private final CountDownLatch okToContinue;
private final CountDownLatch threadHasStarted = new CountDownLatch(1);
private final T seed;
private final int size;
public TestObservable(T... values) {
this(null, null, values);
}
public TestObservable(CountDownLatch once, CountDownLatch okToContinue, T... values) {
this.values = Arrays.asList(values);
this.size = this.values.size();
this.once = once;
this.okToContinue = okToContinue;
this.seed = null;
}
public TestObservable(T seed, int size) {
values = null;
once = null;
okToContinue = null;
this.seed = seed;
this.size = size;
}
@Override
public void call(final Subscriber<? super T> observer) {
observer.add(s);
t = new Thread(new Runnable() {
@Override
public void run() {
try {
while (count < size && subscribed) {
if (null != values)
observer.onNext(values.get(count));
else
observer.onNext(seed);
count++;
//Unblock the main thread to call unsubscribe.
if (null != once)
once.countDown();
//Block until the main thread has called unsubscribe.
if (null != okToContinue)
okToContinue.await(5, TimeUnit.SECONDS);
}
if (subscribed)
observer.onCompleted();
} catch (InterruptedException e) {
e.printStackTrace();
fail(e.getMessage());
}
}
});
t.start();
threadHasStarted.countDown();
}
void waitForThreadDone() throws InterruptedException {
threadHasStarted.await();
t.join();
}
}
@Test
public void testMultipleObservers() {
@SuppressWarnings("unchecked")
Observer<Object> o1 = mock(Observer.class);
@SuppressWarnings("unchecked")
Observer<Object> o2 = mock(Observer.class);
TestScheduler s = new TestScheduler();
Observable<Long> timer = Observable.interval(500, TimeUnit.MILLISECONDS, s).take(2);
Observable<Long> o = Observable.concat(timer, timer);
o.subscribe(o1);
o.subscribe(o2);
InOrder inOrder1 = inOrder(o1);
InOrder inOrder2 = inOrder(o2);
s.advanceTimeBy(500, TimeUnit.MILLISECONDS);
inOrder1.verify(o1, times(1)).onNext(0L);
inOrder2.verify(o2, times(1)).onNext(0L);
s.advanceTimeBy(500, TimeUnit.MILLISECONDS);
inOrder1.verify(o1, times(1)).onNext(1L);
inOrder2.verify(o2, times(1)).onNext(1L);
s.advanceTimeBy(500, TimeUnit.MILLISECONDS);
inOrder1.verify(o1, times(1)).onNext(0L);
inOrder2.verify(o2, times(1)).onNext(0L);
s.advanceTimeBy(500, TimeUnit.MILLISECONDS);
inOrder1.verify(o1, times(1)).onNext(1L);
inOrder2.verify(o2, times(1)).onNext(1L);
inOrder1.verify(o1, times(1)).onCompleted();
inOrder2.verify(o2, times(1)).onCompleted();
verify(o1, never()).onError(any(Throwable.class));
verify(o2, never()).onError(any(Throwable.class));
}
@Test
public void concatVeryLongObservableOfObservables() {
final int n = 10000;
Observable<Observable<Integer>> source = Observable.create(new OnSubscribe<Observable<Integer>>() {
@Override
public void call(Subscriber<? super Observable<Integer>> s) {
for (int i = 0; i < n; i++) {
if (s.isUnsubscribed()) {
return;
}
s.onNext(Observable.just(i));
}
s.onCompleted();
}
});
Observable<List<Integer>> result = Observable.concat(source).toList();
@SuppressWarnings("unchecked")
Observer<List<Integer>> o = mock(Observer.class);
InOrder inOrder = inOrder(o);
result.subscribe(o);
List<Integer> list = new ArrayList<Integer>(n);
for (int i = 0; i < n; i++) {
list.add(i);
}
inOrder.verify(o).onNext(list);
inOrder.verify(o).onCompleted();
verify(o, never()).onError(any(Throwable.class));
}
@Test
public void concatVeryLongObservableOfObservablesTakeHalf() {
final int n = 10000;
Observable<Observable<Integer>> source = Observable.create(new OnSubscribe<Observable<Integer>>() {
@Override
public void call(Subscriber<? super Observable<Integer>> s) {
for (int i = 0; i < n; i++) {
if (s.isUnsubscribed()) {
return;
}
s.onNext(Observable.just(i));
}
s.onCompleted();
}
});
Observable<List<Integer>> result = Observable.concat(source).take(n / 2).toList();
@SuppressWarnings("unchecked")
Observer<List<Integer>> o = mock(Observer.class);
InOrder inOrder = inOrder(o);
result.subscribe(o);
List<Integer> list = new ArrayList<Integer>(n);
for (int i = 0; i < n / 2; i++) {
list.add(i);
}
inOrder.verify(o).onNext(list);
inOrder.verify(o).onCompleted();
verify(o, never()).onError(any(Throwable.class));
}
@Test
public void testConcatOuterBackpressure() {
assertEquals(1,
(int) Observable.<Integer> empty()
.concatWith(Observable.just(1))
.take(1)
.toBlocking().single());
}
@Test
public void testInnerBackpressureWithAlignedBoundaries() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable.range(0, RxRingBuffer.SIZE * 2)
.concatWith(Observable.range(0, RxRingBuffer.SIZE * 2))
.observeOn(Schedulers.computation()) // observeOn has a backpressured RxRingBuffer
.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(RxRingBuffer.SIZE * 4, ts.getOnNextEvents().size());
}
/*
* Testing without counts aligned with buffer sizes because concat must prevent the subscription
* to the next Observable if request == 0 which can happen at the end of a subscription
* if the request size == emitted size. It needs to delay subscription until the next request when aligned,
* when not aligned, it just subscribesNext with the outstanding request amount.
*/
@Test
public void testInnerBackpressureWithoutAlignedBoundaries() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable.range(0, (RxRingBuffer.SIZE * 2) + 10)
.concatWith(Observable.range(0, (RxRingBuffer.SIZE * 2) + 10))
.observeOn(Schedulers.computation()) // observeOn has a backpressured RxRingBuffer
.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals((RxRingBuffer.SIZE * 4) + 20, ts.getOnNextEvents().size());
}
// https://github.com/ReactiveX/RxJava/issues/1818
@Test
public void testConcatWithNonCompliantSourceDoubleOnComplete() {
Observable<String> o = Observable.create(new OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> s) {
s.onNext("hello");
s.onCompleted();
s.onCompleted();
}
});
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable.concat(o, o).subscribe(ts);
ts.awaitTerminalEvent(500, TimeUnit.MILLISECONDS);
ts.assertTerminalEvent();
ts.assertNoErrors();
ts.assertReceivedOnNext(Arrays.asList("hello", "hello"));
}
}