/** * 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.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import io.reactivex.annotations.Nullable; import org.junit.Test; import org.mockito.InOrder; import io.reactivex.*; import io.reactivex.Observable; import io.reactivex.Observer; import io.reactivex.disposables.*; import io.reactivex.exceptions.TestException; import io.reactivex.functions.*; import io.reactivex.internal.fuseable.*; import io.reactivex.internal.operators.observable.ObservableObserveOn.ObserveOnObserver; import io.reactivex.internal.schedulers.ImmediateThinScheduler; import io.reactivex.observers.*; import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.schedulers.*; import io.reactivex.subjects.*; public class ObservableObserveOnTest { /** * This is testing a no-op path since it uses Schedulers.immediate() which will not do scheduling. */ @Test public void testObserveOn() { Observer<Integer> observer = TestHelper.mockObserver(); Observable.just(1, 2, 3).observeOn(ImmediateThinScheduler.INSTANCE).subscribe(observer); verify(observer, times(1)).onNext(1); verify(observer, times(1)).onNext(2); verify(observer, times(1)).onNext(3); verify(observer, times(1)).onComplete(); } @Test public void testOrdering() throws InterruptedException { // Observable<String> obs = Observable.just("one", null, "two", "three", "four"); // FIXME null values not allowed Observable<String> obs = Observable.just("one", "null", "two", "three", "four"); Observer<String> observer = TestHelper.mockObserver(); InOrder inOrder = inOrder(observer); TestObserver<String> ts = new TestObserver<String>(observer); obs.observeOn(Schedulers.computation()).subscribe(ts); ts.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS); if (ts.errors().size() > 0) { for (Throwable t : ts.errors()) { t.printStackTrace(); } fail("failed with exception"); } inOrder.verify(observer, times(1)).onNext("one"); inOrder.verify(observer, times(1)).onNext("null"); 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)).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test public void testThreadName() throws InterruptedException { System.out.println("Main Thread: " + Thread.currentThread().getName()); // FIXME null values not allowed // Observable<String> obs = Observable.just("one", null, "two", "three", "four"); Observable<String> obs = Observable.just("one", "null", "two", "three", "four"); Observer<String> observer = TestHelper.mockObserver(); final String parentThreadName = Thread.currentThread().getName(); final CountDownLatch completedLatch = new CountDownLatch(1); // assert subscribe is on main thread obs = obs.doOnNext(new Consumer<String>() { @Override public void accept(String s) { String threadName = Thread.currentThread().getName(); System.out.println("Source ThreadName: " + threadName + " Expected => " + parentThreadName); assertEquals(parentThreadName, threadName); } }); // assert observe is on new thread obs.observeOn(Schedulers.newThread()).doOnNext(new Consumer<String>() { @Override public void accept(String t1) { String threadName = Thread.currentThread().getName(); boolean correctThreadName = threadName.startsWith("RxNewThreadScheduler"); System.out.println("ObserveOn ThreadName: " + threadName + " Correct => " + correctThreadName); assertTrue(correctThreadName); } }).doAfterTerminate(new Action() { @Override public void run() { completedLatch.countDown(); } }).subscribe(observer); if (!completedLatch.await(1000, TimeUnit.MILLISECONDS)) { fail("timed out waiting"); } verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(5)).onNext(any(String.class)); verify(observer, times(1)).onComplete(); } @Test public void observeOnTheSameSchedulerTwice() { Scheduler scheduler = ImmediateThinScheduler.INSTANCE; Observable<Integer> o = Observable.just(1, 2, 3); Observable<Integer> o2 = o.observeOn(scheduler); @SuppressWarnings("unchecked") DefaultObserver<Object> observer1 = mock(DefaultObserver.class); @SuppressWarnings("unchecked") DefaultObserver<Object> observer2 = mock(DefaultObserver.class); InOrder inOrder1 = inOrder(observer1); InOrder inOrder2 = inOrder(observer2); o2.subscribe(observer1); o2.subscribe(observer2); inOrder1.verify(observer1, times(1)).onNext(1); inOrder1.verify(observer1, times(1)).onNext(2); inOrder1.verify(observer1, times(1)).onNext(3); inOrder1.verify(observer1, times(1)).onComplete(); verify(observer1, never()).onError(any(Throwable.class)); inOrder1.verifyNoMoreInteractions(); inOrder2.verify(observer2, times(1)).onNext(1); inOrder2.verify(observer2, times(1)).onNext(2); inOrder2.verify(observer2, times(1)).onNext(3); inOrder2.verify(observer2, times(1)).onComplete(); verify(observer2, never()).onError(any(Throwable.class)); inOrder2.verifyNoMoreInteractions(); } @Test public void observeSameOnMultipleSchedulers() { TestScheduler scheduler1 = new TestScheduler(); TestScheduler scheduler2 = new TestScheduler(); Observable<Integer> o = Observable.just(1, 2, 3); Observable<Integer> o1 = o.observeOn(scheduler1); Observable<Integer> o2 = o.observeOn(scheduler2); Observer<Object> observer1 = TestHelper.mockObserver(); Observer<Object> observer2 = TestHelper.mockObserver(); InOrder inOrder1 = inOrder(observer1); InOrder inOrder2 = inOrder(observer2); o1.subscribe(observer1); o2.subscribe(observer2); scheduler1.advanceTimeBy(1, TimeUnit.SECONDS); scheduler2.advanceTimeBy(1, TimeUnit.SECONDS); inOrder1.verify(observer1, times(1)).onNext(1); inOrder1.verify(observer1, times(1)).onNext(2); inOrder1.verify(observer1, times(1)).onNext(3); inOrder1.verify(observer1, times(1)).onComplete(); verify(observer1, never()).onError(any(Throwable.class)); inOrder1.verifyNoMoreInteractions(); inOrder2.verify(observer2, times(1)).onNext(1); inOrder2.verify(observer2, times(1)).onNext(2); inOrder2.verify(observer2, times(1)).onNext(3); inOrder2.verify(observer2, times(1)).onComplete(); verify(observer2, never()).onError(any(Throwable.class)); inOrder2.verifyNoMoreInteractions(); } /** * Confirm that running on a NewThreadScheduler uses the same thread for the entire stream. */ @Test public void testObserveOnWithNewThreadScheduler() { final AtomicInteger count = new AtomicInteger(); final int _multiple = 99; Observable.range(1, 100000).map(new Function<Integer, Integer>() { @Override public Integer apply(Integer t1) { return t1 * _multiple; } }).observeOn(Schedulers.newThread()) .blockingForEach(new Consumer<Integer>() { @Override public void accept(Integer t1) { assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); // FIXME toBlocking methods run on the current thread String name = Thread.currentThread().getName(); assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); } }); } /** * Confirm that running on a ThreadPoolScheduler allows multiple threads but is still ordered. */ @Test public void testObserveOnWithThreadPoolScheduler() { final AtomicInteger count = new AtomicInteger(); final int _multiple = 99; Observable.range(1, 100000).map(new Function<Integer, Integer>() { @Override public Integer apply(Integer t1) { return t1 * _multiple; } }).observeOn(Schedulers.computation()) .blockingForEach(new Consumer<Integer>() { @Override public void accept(Integer t1) { assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); // FIXME toBlocking methods run on the caller's thread String name = Thread.currentThread().getName(); assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); } }); } /** * Attempts to confirm that when pauses exist between events, the ScheduledObserver * does not lose or reorder any events since the scheduler will not block, but will * be re-scheduled when it receives new events after each pause. * * * This is non-deterministic in proving success, but if it ever fails (non-deterministically) * it is a sign of potential issues as thread-races and scheduling should not affect output. */ @Test public void testObserveOnOrderingConcurrency() { final AtomicInteger count = new AtomicInteger(); final int _multiple = 99; Observable.range(1, 10000).map(new Function<Integer, Integer>() { @Override public Integer apply(Integer t1) { if (randomIntFrom0to100() > 98) { try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } return t1 * _multiple; } }).observeOn(Schedulers.computation()) .blockingForEach(new Consumer<Integer>() { @Override public void accept(Integer t1) { assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); // assertTrue(name.startsWith("RxComputationThreadPool")); // FIXME toBlocking now runs its methods on the caller thread String name = Thread.currentThread().getName(); assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); } }); } @Test public void testNonBlockingOuterWhileBlockingOnNext() throws InterruptedException { final CountDownLatch completedLatch = new CountDownLatch(1); final CountDownLatch nextLatch = new CountDownLatch(1); final AtomicLong completeTime = new AtomicLong(); // use subscribeOn to make async, observeOn to move Observable.range(1, 2).subscribeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(new DefaultObserver<Integer>() { @Override public void onComplete() { System.out.println("onComplete"); completeTime.set(System.nanoTime()); completedLatch.countDown(); } @Override public void onError(Throwable e) { } @Override public void onNext(Integer t) { // don't let this thing finish yet try { if (!nextLatch.await(1000, TimeUnit.MILLISECONDS)) { throw new RuntimeException("it shouldn't have timed out"); } } catch (InterruptedException e) { throw new RuntimeException("it shouldn't have failed"); } } }); long afterSubscribeTime = System.nanoTime(); System.out.println("After subscribe: " + completedLatch.getCount()); assertEquals(1, completedLatch.getCount()); nextLatch.countDown(); completedLatch.await(1000, TimeUnit.MILLISECONDS); assertTrue(completeTime.get() > afterSubscribeTime); System.out.println("onComplete nanos after subscribe: " + (completeTime.get() - afterSubscribeTime)); } private static int randomIntFrom0to100() { // XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml long x = System.nanoTime(); x ^= (x << 21); x ^= (x >>> 35); x ^= (x << 4); return Math.abs((int) x % 100); } @Test public void testDelayedErrorDeliveryWhenSafeSubscriberUnsubscribes() { TestScheduler testScheduler = new TestScheduler(); Observable<Integer> source = Observable.concat(Observable.<Integer> error(new TestException()), Observable.just(1)); @SuppressWarnings("unchecked") DefaultObserver<Integer> o = mock(DefaultObserver.class); InOrder inOrder = inOrder(o); source.observeOn(testScheduler).subscribe(o); inOrder.verify(o, never()).onError(any(TestException.class)); testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); inOrder.verify(o).onError(any(TestException.class)); inOrder.verify(o, never()).onNext(anyInt()); inOrder.verify(o, never()).onComplete(); } @Test public void testAfterUnsubscribeCalledThenObserverOnNextNeverCalled() { final TestScheduler testScheduler = new TestScheduler(); final Observer<Integer> observer = TestHelper.mockObserver(); TestObserver<Integer> ts = new TestObserver<Integer>(observer); Observable.just(1, 2, 3) .observeOn(testScheduler) .subscribe(ts); ts.dispose(); testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); final InOrder inOrder = inOrder(observer); inOrder.verify(observer, never()).onNext(anyInt()); inOrder.verify(observer, never()).onError(any(Exception.class)); inOrder.verify(observer, never()).onComplete(); } @Test public void testBackpressureWithTakeBefore() { final AtomicInteger generated = new AtomicInteger(); Observable<Integer> o = Observable.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; } }; } }); TestObserver<Integer> ts = new TestObserver<Integer>(); o .take(7) .observeOn(Schedulers.newThread()) .subscribe(ts); ts.awaitTerminalEvent(); ts.assertValues(0, 1, 2, 3, 4, 5, 6); assertEquals(7, generated.get()); } @Test public void testAsyncChild() { TestObserver<Integer> ts = new TestObserver<Integer>(); Observable.range(0, 100000).observeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); } @Test public void delayError() { Observable.range(1, 5).concatWith(Observable.<Integer>error(new TestException())) .observeOn(Schedulers.computation(), true) .doOnNext(new Consumer<Integer>() { @Override public void accept(Integer v) throws Exception { if (v == 1) { Thread.sleep(100); } } }) .test() .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class, 1, 2, 3, 4, 5); } @Test public void trampolineScheduler() { Observable.just(1) .observeOn(Schedulers.trampoline()) .test() .assertResult(1); } @Test public void dispose() { TestHelper.checkDisposed(PublishSubject.create().observeOn(new TestScheduler())); } @Test public void doubleOnSubscribe() { TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { @Override public ObservableSource<Object> apply(Observable<Object> o) throws Exception { return o.observeOn(new TestScheduler()); } }); } @Test public void badSource() { List<Throwable> errors = TestHelper.trackPluginErrors(); try { TestScheduler scheduler = new TestScheduler(); TestObserver<Integer> to = new Observable<Integer>() { @Override protected void subscribeActual(Observer<? super Integer> observer) { observer.onSubscribe(Disposables.empty()); observer.onComplete(); observer.onNext(1); observer.onError(new TestException()); observer.onComplete(); } } .observeOn(scheduler) .test(); scheduler.triggerActions(); to.assertResult(); TestHelper.assertUndeliverable(errors, 0, TestException.class); } finally { RxJavaPlugins.reset(); } } @Test public void inputSyncFused() { Observable.range(1, 5) .observeOn(Schedulers.single()) .test() .awaitDone(5, TimeUnit.SECONDS) .assertResult(1, 2, 3, 4, 5); } @Test public void inputAsyncFused() { UnicastSubject<Integer> us = UnicastSubject.create(); TestObserver<Integer> to = us.observeOn(Schedulers.single()).test(); TestHelper.emit(us, 1, 2, 3, 4, 5); to .awaitDone(5, TimeUnit.SECONDS) .assertResult(1, 2, 3, 4, 5); } @Test public void inputAsyncFusedError() { UnicastSubject<Integer> us = UnicastSubject.create(); TestObserver<Integer> to = us.observeOn(Schedulers.single()).test(); us.onError(new TestException()); to .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); } @Test public void inputAsyncFusedErrorDelayed() { UnicastSubject<Integer> us = UnicastSubject.create(); TestObserver<Integer> to = us.observeOn(Schedulers.single(), true).test(); us.onError(new TestException()); to .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); } @Test public void outputFused() { TestObserver<Integer> to = ObserverFusion.newTest(QueueDisposable.ANY); Observable.range(1, 5).hide() .observeOn(Schedulers.single()) .subscribe(to); ObserverFusion.assertFusion(to, QueueDisposable.ASYNC) .awaitDone(5, TimeUnit.SECONDS) .assertResult(1, 2, 3, 4, 5); } @Test public void outputFusedReject() { TestObserver<Integer> to = ObserverFusion.newTest(QueueDisposable.SYNC); Observable.range(1, 5).hide() .observeOn(Schedulers.single()) .subscribe(to); ObserverFusion.assertFusion(to, QueueDisposable.NONE) .awaitDone(5, TimeUnit.SECONDS) .assertResult(1, 2, 3, 4, 5); } @Test public void inputOutputAsyncFusedError() { TestObserver<Integer> to = ObserverFusion.newTest(QueueDisposable.ANY); UnicastSubject<Integer> us = UnicastSubject.create(); us.observeOn(Schedulers.single()) .subscribe(to); us.onError(new TestException()); to .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); ObserverFusion.assertFusion(to, QueueDisposable.ASYNC) .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); } @Test public void inputOutputAsyncFusedErrorDelayed() { TestObserver<Integer> to = ObserverFusion.newTest(QueueDisposable.ANY); UnicastSubject<Integer> us = UnicastSubject.create(); us.observeOn(Schedulers.single(), true) .subscribe(to); us.onError(new TestException()); to .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); ObserverFusion.assertFusion(to, QueueDisposable.ASYNC) .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); } @Test public void outputFusedCancelReentrant() throws Exception { final UnicastSubject<Integer> us = UnicastSubject.create(); final CountDownLatch cdl = new CountDownLatch(1); us.observeOn(Schedulers.single()) .subscribe(new Observer<Integer>() { Disposable d; int count; @Override public void onSubscribe(Disposable d) { this.d = d; ((QueueDisposable<?>)d).requestFusion(QueueDisposable.ANY); } @Override public void onNext(Integer value) { if (++count == 1) { us.onNext(2); d.dispose(); cdl.countDown(); } } @Override public void onError(Throwable e) { } @Override public void onComplete() { } }); us.onNext(1); cdl.await(); } @Test public void nonFusedPollThrows() { new Observable<Integer>() { @Override protected void subscribeActual(Observer<? super Integer> observer) { observer.onSubscribe(Disposables.empty()); @SuppressWarnings("unchecked") ObserveOnObserver<Integer> oo = (ObserveOnObserver<Integer>)observer; oo.queue = new SimpleQueue<Integer>() { @Override public boolean offer(Integer value) { return false; } @Override public boolean offer(Integer v1, Integer v2) { return false; } @Nullable @Override public Integer poll() throws Exception { throw new TestException(); } @Override public boolean isEmpty() { return false; } @Override public void clear() { } }; oo.clear(); oo.schedule(); } } .observeOn(Schedulers.single()) .test() .awaitDone(5, TimeUnit.SECONDS) .assertFailure(TestException.class); } }