/**
* 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.mockito.Matchers.any;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.notNull;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.mockito.InOrder;
import rx.Observable;
import rx.Observer;
import rx.Scheduler;
import rx.Scheduler.Worker;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.ConnectableObservable;
import rx.schedulers.TestScheduler;
import rx.subjects.PublishSubject;
public class OperatorReplayTest {
@Test
public void testBufferedReplay() {
PublishSubject<Integer> source = PublishSubject.create();
ConnectableObservable<Integer> co = source.replay(3);
co.connect();
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
source.onNext(2);
source.onNext(3);
inOrder.verify(observer1, times(1)).onNext(1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
source.onNext(4);
source.onCompleted();
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
}
@Test
public void testBufferedWindowReplay() {
PublishSubject<Integer> source = PublishSubject.create();
TestScheduler scheduler = new TestScheduler();
ConnectableObservable<Integer> co = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler);
co.connect();
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS);
source.onNext(2);
scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS);
source.onNext(3);
scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS);
inOrder.verify(observer1, times(1)).onNext(1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
source.onNext(4);
source.onNext(5);
scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onNext(5);
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onNext(5);
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
}
@Test
public void testWindowedReplay() {
TestScheduler scheduler = new TestScheduler();
PublishSubject<Integer> source = PublishSubject.create();
ConnectableObservable<Integer> co = source.replay(100, TimeUnit.MILLISECONDS, scheduler);
co.connect();
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onNext(2);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onNext(3);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onCompleted();
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
inOrder.verify(observer1, times(1)).onNext(1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onNext(3);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
}
@Test
public void testReplaySelector() {
final Func1<Integer, Integer> dbl = new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t1) {
return t1 * 2;
}
};
Func1<Observable<Integer>, Observable<Integer>> selector = new Func1<Observable<Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(Observable<Integer> t1) {
return t1.map(dbl);
}
};
PublishSubject<Integer> source = PublishSubject.create();
Observable<Integer> co = source.replay(selector);
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
source.onNext(2);
source.onNext(3);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onNext(6);
source.onNext(4);
source.onCompleted();
inOrder.verify(observer1, times(1)).onNext(8);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
}
@Test
public void testBufferedReplaySelector() {
final Func1<Integer, Integer> dbl = new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t1) {
return t1 * 2;
}
};
Func1<Observable<Integer>, Observable<Integer>> selector = new Func1<Observable<Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(Observable<Integer> t1) {
return t1.map(dbl);
}
};
PublishSubject<Integer> source = PublishSubject.create();
Observable<Integer> co = source.replay(selector, 3);
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
source.onNext(2);
source.onNext(3);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onNext(6);
source.onNext(4);
source.onCompleted();
inOrder.verify(observer1, times(1)).onNext(8);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
}
@Test
public void testWindowedReplaySelector() {
final Func1<Integer, Integer> dbl = new Func1<Integer, Integer>() {
@Override
public Integer call(Integer t1) {
return t1 * 2;
}
};
Func1<Observable<Integer>, Observable<Integer>> selector = new Func1<Observable<Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(Observable<Integer> t1) {
return t1.map(dbl);
}
};
TestScheduler scheduler = new TestScheduler();
PublishSubject<Integer> source = PublishSubject.create();
Observable<Integer> co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler);
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onNext(2);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onNext(3);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onCompleted();
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onNext(6);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onError(any(Throwable.class));
}
}
@Test
public void testBufferedReplayError() {
PublishSubject<Integer> source = PublishSubject.create();
ConnectableObservable<Integer> co = source.replay(3);
co.connect();
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
source.onNext(2);
source.onNext(3);
inOrder.verify(observer1, times(1)).onNext(1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
source.onNext(4);
source.onError(new RuntimeException("Forced failure"));
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class));
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onCompleted();
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
inOrder.verify(observer1, times(1)).onNext(4);
inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class));
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onCompleted();
}
}
@Test
public void testWindowedReplayError() {
TestScheduler scheduler = new TestScheduler();
PublishSubject<Integer> source = PublishSubject.create();
ConnectableObservable<Integer> co = source.replay(100, TimeUnit.MILLISECONDS, scheduler);
co.connect();
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
source.onNext(1);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onNext(2);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onNext(3);
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
source.onError(new RuntimeException("Forced failure"));
scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS);
inOrder.verify(observer1, times(1)).onNext(1);
inOrder.verify(observer1, times(1)).onNext(2);
inOrder.verify(observer1, times(1)).onNext(3);
inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class));
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onCompleted();
}
{
@SuppressWarnings("unchecked")
Observer<Object> observer1 = mock(Observer.class);
InOrder inOrder = inOrder(observer1);
co.subscribe(observer1);
inOrder.verify(observer1, times(1)).onNext(3);
inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class));
inOrder.verifyNoMoreInteractions();
verify(observer1, never()).onCompleted();
}
}
@Test
public void testSynchronousDisconnect() {
final AtomicInteger effectCounter = new AtomicInteger();
Observable<Integer> source = Observable.just(1, 2, 3, 4)
.doOnNext(new Action1<Integer>() {
@Override
public void call(Integer v) {
effectCounter.incrementAndGet();
System.out.println("Sideeffect #" + v);
}
});
Observable<Integer> result = source.replay(
new Func1<Observable<Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(Observable<Integer> o) {
return o.take(2);
}
});
for (int i = 1; i < 3; i++) {
effectCounter.set(0);
System.out.printf("- %d -%n", i);
result.subscribe(new Action1<Integer>() {
@Override
public void call(Integer t1) {
System.out.println(t1);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable t1) {
t1.printStackTrace();
}
},
new Action0() {
@Override
public void call() {
System.out.println("Done");
}
});
assertEquals(2, effectCounter.get());
}
}
/**
* test the basic expectation of OperatorMulticast via replay
*/
@Test
public void testIssue2191_UnsubscribeSource() {
// setup mocks
Action1 sourceNext = mock(Action1.class);
Action0 sourceCompleted = mock(Action0.class);
Action0 sourceUnsubscribed = mock(Action0.class);
Observer spiedSubscriberBeforeConnect = mock(Observer.class);
Observer spiedSubscriberAfterConnect = mock(Observer.class);
// Observable under test
Observable<Integer> source = Observable.just(1,2);
ConnectableObservable<Integer> replay = source
.doOnNext(sourceNext)
.doOnUnsubscribe(sourceUnsubscribed)
.doOnCompleted(sourceCompleted)
.replay();
replay.subscribe(spiedSubscriberBeforeConnect);
replay.subscribe(spiedSubscriberBeforeConnect);
replay.connect();
replay.subscribe(spiedSubscriberAfterConnect);
replay.subscribe(spiedSubscriberAfterConnect);
// verify interactions
verify(sourceNext, times(1)).call(1);
verify(sourceNext, times(1)).call(2);
verify(sourceCompleted, times(1)).call();
verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4);
verifyObserverMock(spiedSubscriberAfterConnect, 2, 4);
verify(sourceUnsubscribed, times(1)).call();
verifyNoMoreInteractions(sourceNext);
verifyNoMoreInteractions(sourceCompleted);
verifyNoMoreInteractions(sourceUnsubscribed);
verifyNoMoreInteractions(spiedSubscriberBeforeConnect);
verifyNoMoreInteractions(spiedSubscriberAfterConnect);
}
/**
* Specifically test interaction with a Scheduler with subscribeOn
*
* @throws Exception
*/
@Test
public void testIssue2191_SchedulerUnsubscribe() throws Exception {
// setup mocks
Action1 sourceNext = mock(Action1.class);
Action0 sourceCompleted = mock(Action0.class);
Action0 sourceUnsubscribed = mock(Action0.class);
final Scheduler mockScheduler = mock(Scheduler.class);
final Subscription mockSubscription = mock(Subscription.class);
Worker spiedWorker = workerSpy(mockSubscription);
Observer mockObserverBeforeConnect = mock(Observer.class);
Observer mockObserverAfterConnect = mock(Observer.class);
when(mockScheduler.createWorker()).thenReturn(spiedWorker);
// Observable under test
ConnectableObservable<Integer> replay = Observable.just(1, 2, 3)
.doOnNext(sourceNext)
.doOnUnsubscribe(sourceUnsubscribed)
.doOnCompleted(sourceCompleted)
.subscribeOn(mockScheduler).replay();
replay.subscribe(mockObserverBeforeConnect);
replay.subscribe(mockObserverBeforeConnect);
replay.connect();
replay.subscribe(mockObserverAfterConnect);
replay.subscribe(mockObserverAfterConnect);
// verify interactions
verify(sourceNext, times(1)).call(1);
verify(sourceNext, times(1)).call(2);
verify(sourceNext, times(1)).call(3);
verify(sourceCompleted, times(1)).call();
verify(mockScheduler, times(1)).createWorker();
verify(spiedWorker, times(1)).schedule((Action0)notNull());
verifyObserverMock(mockObserverBeforeConnect, 2, 6);
verifyObserverMock(mockObserverAfterConnect, 2, 6);
verify(spiedWorker, times(1)).isUnsubscribed();
verify(spiedWorker, times(1)).unsubscribe();
verify(sourceUnsubscribed, times(1)).call();
verifyNoMoreInteractions(sourceNext);
verifyNoMoreInteractions(sourceCompleted);
verifyNoMoreInteractions(sourceUnsubscribed);
verifyNoMoreInteractions(spiedWorker);
verifyNoMoreInteractions(mockSubscription);
verifyNoMoreInteractions(mockScheduler);
verifyNoMoreInteractions(mockObserverBeforeConnect);
verifyNoMoreInteractions(mockObserverAfterConnect);
}
/**
* Specifically test interaction with a Scheduler with subscribeOn
*
* @throws Exception
*/
@Test
public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception {
// setup mocks
Action1 sourceNext = mock(Action1.class);
Action0 sourceCompleted = mock(Action0.class);
Action1 sourceError = mock(Action1.class);
Action0 sourceUnsubscribed = mock(Action0.class);
final Scheduler mockScheduler = mock(Scheduler.class);
final Subscription mockSubscription = mock(Subscription.class);
Worker spiedWorker = workerSpy(mockSubscription);
Observer mockObserverBeforeConnect = mock(Observer.class);
Observer mockObserverAfterConnect = mock(Observer.class);
when(mockScheduler.createWorker()).thenReturn(spiedWorker);
// Observable under test
Func1<Integer, Integer> mockFunc = mock(Func1.class);
IllegalArgumentException illegalArgumentException = new IllegalArgumentException();
when(mockFunc.call(1)).thenReturn(1);
when(mockFunc.call(2)).thenThrow(illegalArgumentException);
ConnectableObservable<Integer> replay = Observable.just(1, 2, 3).map(mockFunc)
.doOnNext(sourceNext)
.doOnUnsubscribe(sourceUnsubscribed)
.doOnCompleted(sourceCompleted)
.doOnError(sourceError)
.subscribeOn(mockScheduler).replay();
replay.subscribe(mockObserverBeforeConnect);
replay.subscribe(mockObserverBeforeConnect);
replay.connect();
replay.subscribe(mockObserverAfterConnect);
replay.subscribe(mockObserverAfterConnect);
// verify interactions
verify(mockScheduler, times(1)).createWorker();
verify(spiedWorker, times(1)).schedule((Action0)notNull());
verify(sourceNext, times(1)).call(1);
verify(sourceError, times(1)).call(illegalArgumentException);
verifyObserver(mockObserverBeforeConnect, 2, 2, illegalArgumentException);
verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException);
verify(spiedWorker, times(1)).isUnsubscribed();
verify(spiedWorker, times(1)).unsubscribe();
verify(sourceUnsubscribed, times(1)).call();
verifyNoMoreInteractions(sourceNext);
verifyNoMoreInteractions(sourceCompleted);
verifyNoMoreInteractions(sourceError);
verifyNoMoreInteractions(sourceUnsubscribed);
verifyNoMoreInteractions(spiedWorker);
verifyNoMoreInteractions(mockSubscription);
verifyNoMoreInteractions(mockScheduler);
verifyNoMoreInteractions(mockObserverBeforeConnect);
verifyNoMoreInteractions(mockObserverAfterConnect);
}
private static void verifyObserverMock(Observer mock, int numSubscriptions, int numItemsExpected) {
verify(mock, times(numItemsExpected)).onNext(notNull());
verify(mock, times(numSubscriptions)).onCompleted();
verifyNoMoreInteractions(mock);
}
private static void verifyObserver(Observer mock, int numSubscriptions, int numItemsExpected, Throwable error) {
verify(mock, times(numItemsExpected)).onNext(notNull());
verify(mock, times(numSubscriptions)).onError(error);
verifyNoMoreInteractions(mock);
}
public static Worker workerSpy(final Subscription mockSubscription) {
return spy(new InprocessWorker(mockSubscription));
}
private static class InprocessWorker extends Worker {
private final Subscription mockSubscription;
public boolean unsubscribed;
public InprocessWorker(Subscription mockSubscription) {
this.mockSubscription = mockSubscription;
}
@Override
public Subscription schedule(Action0 action) {
action.call();
return mockSubscription; // this subscription is returned but discarded
}
@Override
public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) {
action.call();
return mockSubscription;
}
@Override
public void unsubscribe() {
unsubscribed = true;
}
@Override
public boolean isUnsubscribed() {
return unsubscribed;
}
}
}