/** * 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.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; 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.Arrays; import java.util.Iterator; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.junit.Test; import org.mockito.InOrder; import rx.Notification; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observer; import rx.Scheduler; import rx.Subscriber; import rx.Subscription; import rx.exceptions.MissingBackpressureException; import rx.exceptions.TestException; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; public class OperatorObserveOnTest { /** * This is testing a no-op path since it uses Schedulers.immediate() which will not do scheduling. */ @Test @SuppressWarnings("unchecked") public void testObserveOn() { Observer<Integer> observer = mock(Observer.class); Observable.just(1, 2, 3).observeOn(Schedulers.immediate()).subscribe(observer); verify(observer, times(1)).onNext(1); verify(observer, times(1)).onNext(2); verify(observer, times(1)).onNext(3); verify(observer, times(1)).onCompleted(); } @Test @SuppressWarnings("unchecked") public void testOrdering() throws InterruptedException { Observable<String> obs = Observable.just("one", null, "two", "three", "four"); Observer<String> observer = mock(Observer.class); InOrder inOrder = inOrder(observer); TestSubscriber<String> ts = new TestSubscriber<String>(observer); obs.observeOn(Schedulers.computation()).subscribe(ts); ts.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS); if (ts.getOnErrorEvents().size() > 0) { for (Throwable t : ts.getOnErrorEvents()) { 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)).onCompleted(); inOrder.verifyNoMoreInteractions(); } @Test @SuppressWarnings("unchecked") public void testThreadName() throws InterruptedException { System.out.println("Main Thread: " + Thread.currentThread().getName()); Observable<String> obs = Observable.just("one", null, "two", "three", "four"); Observer<String> observer = mock(Observer.class); final String parentThreadName = Thread.currentThread().getName(); final CountDownLatch completedLatch = new CountDownLatch(1); // assert subscribe is on main thread obs = obs.doOnNext(new Action1<String>() { @Override public void call(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 Action1<String>() { @Override public void call(String t1) { String threadName = Thread.currentThread().getName(); boolean correctThreadName = threadName.startsWith("RxNewThreadScheduler"); System.out.println("ObserveOn ThreadName: " + threadName + " Correct => " + correctThreadName); assertTrue(correctThreadName); } }).finallyDo(new Action0() { @Override public void call() { 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)).onCompleted(); } @Test public void observeOnTheSameSchedulerTwice() { Scheduler scheduler = Schedulers.immediate(); Observable<Integer> o = Observable.just(1, 2, 3); Observable<Integer> o2 = o.observeOn(scheduler); @SuppressWarnings("unchecked") Observer<Object> observer1 = mock(Observer.class); @SuppressWarnings("unchecked") Observer<Object> observer2 = mock(Observer.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)).onCompleted(); 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)).onCompleted(); 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); @SuppressWarnings("unchecked") Observer<Object> observer1 = mock(Observer.class); @SuppressWarnings("unchecked") Observer<Object> observer2 = mock(Observer.class); 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)).onCompleted(); 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)).onCompleted(); 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 Func1<Integer, Integer>() { @Override public Integer call(Integer t1) { return t1 * _multiple; } }).observeOn(Schedulers.newThread()) .toBlocking().forEach(new Action1<Integer>() { @Override public void call(Integer t1) { assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); assertTrue(Thread.currentThread().getName().startsWith("RxNewThreadScheduler")); } }); } /** * 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 Func1<Integer, Integer>() { @Override public Integer call(Integer t1) { return t1 * _multiple; } }).observeOn(Schedulers.computation()) .toBlocking().forEach(new Action1<Integer>() { @Override public void call(Integer t1) { assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); assertTrue(Thread.currentThread().getName().startsWith("RxComputationThreadPool")); } }); } /** * 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 Func1<Integer, Integer>() { @Override public Integer call(Integer t1) { if (randomIntFrom0to100() > 98) { try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } return t1 * _multiple; } }).observeOn(Schedulers.computation()) .toBlocking().forEach(new Action1<Integer>() { @Override public void call(Integer t1) { assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); assertTrue(Thread.currentThread().getName().startsWith("RxComputationThreadPool")); } }); } @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 Observer<Integer>() { @Override public void onCompleted() { System.out.println("onCompleted"); 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") Observer<Integer> o = mock(Observer.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()).onCompleted(); } @Test public void testAfterUnsubscribeCalledThenObserverOnNextNeverCalled() { final TestScheduler testScheduler = new TestScheduler(); @SuppressWarnings("unchecked") final Observer<Integer> observer = mock(Observer.class); final Subscription subscription = Observable.just(1, 2, 3) .observeOn(testScheduler) .subscribe(observer); subscription.unsubscribe(); 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()).onCompleted(); } @Test public void testBackpressureWithTakeAfter() { final AtomicInteger generated = new AtomicInteger(); Observable<Integer> observable = Observable.from(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; } }; } }); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { @Override public void onNext(Integer t) { System.err.println("c t = " + t + " thread " + Thread.currentThread()); super.onNext(t); try { Thread.sleep(10); } catch (InterruptedException e) { } } }; observable .observeOn(Schedulers.newThread()) .take(3) .subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); System.err.println(testSubscriber.getOnNextEvents()); testSubscriber.assertReceivedOnNext(Arrays.asList(0, 1, 2)); // it should be between the take num and requested batch size across the async boundary System.out.println("Generated: " + generated.get()); assertTrue(generated.get() >= 3 && generated.get() <= RxRingBuffer.SIZE); } @Test public void testBackpressureWithTakeAfterAndMultipleBatches() { int numForBatches = RxRingBuffer.SIZE * 3 + 1; // should be 4 batches == ((3*n)+1) items final AtomicInteger generated = new AtomicInteger(); Observable<Integer> observable = Observable.from(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; } }; } }); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { @Override public void onNext(Integer t) { // System.err.println("c t = " + t + " thread " + Thread.currentThread()); super.onNext(t); } }; observable .observeOn(Schedulers.newThread()) .take(numForBatches) .subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); System.err.println(testSubscriber.getOnNextEvents()); // it should be between the take num and requested batch size across the async boundary System.out.println("Generated: " + generated.get()); assertTrue(generated.get() >= numForBatches && generated.get() <= numForBatches + RxRingBuffer.SIZE); } @Test public void testBackpressureWithTakeBefore() { final AtomicInteger generated = new AtomicInteger(); Observable<Integer> observable = Observable.from(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; } }; } }); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>(); observable .take(7) .observeOn(Schedulers.newThread()) .subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); testSubscriber.assertReceivedOnNext(Arrays.asList(0, 1, 2, 3, 4, 5, 6)); assertEquals(7, generated.get()); } @Test public void testQueueFullEmitsError() { final CountDownLatch latch = new CountDownLatch(1); Observable<Integer> observable = Observable.create(new OnSubscribe<Integer>() { @Override public void call(Subscriber<? super Integer> o) { for (int i = 0; i < RxRingBuffer.SIZE + 10; i++) { o.onNext(i); } latch.countDown(); o.onCompleted(); } }); TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>(new Observer<Integer>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(Integer t) { // force it to be slow and wait until we have queued everything try { latch.await(500, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } }); observable.observeOn(Schedulers.newThread()).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); List<Throwable> errors = testSubscriber.getOnErrorEvents(); assertEquals(1, errors.size()); System.out.println("Errors: " + errors); Throwable t = errors.get(0); if (t instanceof MissingBackpressureException) { // success, we expect this } else { if (t.getCause() instanceof MissingBackpressureException) { // this is also okay } else { fail("Expecting MissingBackpressureException"); } } } @Test public void testAsyncChild() { TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); Observable.range(0, 100000).observeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); } @Test public void testOnErrorCutsAheadOfOnNext() { for (int i = 0; i < 50; i++) { final PublishSubject<Long> subject = PublishSubject.create(); final AtomicLong counter = new AtomicLong(); TestSubscriber<Long> ts = new TestSubscriber<Long>(new Observer<Long>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(Long t) { // simulate slow consumer to force backpressure failure try { Thread.sleep(1); } catch (InterruptedException e) { } } }); subject.observeOn(Schedulers.computation()).subscribe(ts); // this will blow up with backpressure while (counter.get() < 102400) { subject.onNext(counter.get()); counter.incrementAndGet(); } ts.awaitTerminalEvent(); assertEquals(1, ts.getOnErrorEvents().size()); assertTrue(ts.getOnErrorEvents().get(0) instanceof MissingBackpressureException); // assert that the values are sequential, that cutting in didn't allow skipping some but emitting others. // example [0, 1, 2] not [0, 1, 4] List<Long> onNextEvents = ts.getOnNextEvents(); assertTrue(onNextEvents.isEmpty() || onNextEvents.size() == onNextEvents.get(onNextEvents.size() - 1) + 1); // we should emit the error without emitting the full buffer size assertTrue(onNextEvents.size() < RxRingBuffer.SIZE); } } /** * Make sure we get a MissingBackpressureException propagated through when we have a fast temporal (hot) producer. */ @Test public void testHotOperatorBackpressure() { TestSubscriber<String> ts = new TestSubscriber<String>(); Observable.timer(0, 1, TimeUnit.MICROSECONDS) .observeOn(Schedulers.computation()) .map(new Func1<Long, String>() { @Override public String call(Long t1) { System.out.println(t1); try { Thread.sleep(100); } catch (InterruptedException e) { } return t1 + " slow value"; } }).subscribe(ts); ts.awaitTerminalEvent(); System.out.println("Errors: " + ts.getOnErrorEvents()); assertEquals(1, ts.getOnErrorEvents().size()); assertEquals(MissingBackpressureException.class, ts.getOnErrorEvents().get(0).getClass()); } @Test public void testErrorPropagatesWhenNoOutstandingRequests() { Observable<Long> timer = Observable.timer(0, 1, TimeUnit.MICROSECONDS) .doOnEach(new Action1<Notification<? super Long>>() { @Override public void call(Notification<? super Long> n) { // System.out.println("BEFORE " + n); } }) .observeOn(Schedulers.newThread()) .doOnEach(new Action1<Notification<? super Long>>() { @Override public void call(Notification<? super Long> n) { try { Thread.sleep(100); } catch (InterruptedException e) { } // System.out.println("AFTER " + n); } }); TestSubscriber<Long> ts = new TestSubscriber<Long>(); Observable.combineLatest(timer, Observable.<Integer> never(), new Func2<Long, Integer, Long>() { @Override public Long call(Long t1, Integer t2) { return t1; } }).take(RxRingBuffer.SIZE * 2).subscribe(ts); ts.awaitTerminalEvent(); assertEquals(1, ts.getOnErrorEvents().size()); assertEquals(MissingBackpressureException.class, ts.getOnErrorEvents().get(0).getClass()); } }